Skip to content

Fix venues reporting Closed while open past midnight - #581

Open
eastagiletracker wants to merge 1 commit into
asuc-octo:masterfrom
eastagiletracker:agile-board/overnight-open-hours
Open

eastagiletracker wants to merge 1 commit into
asuc-octo:masterfrom
eastagiletracker:agile-board/overnight-open-hours

Conversation

@eastagiletracker

Copy link
Copy Markdown

This PR proposes a fix for venues reporting Closed while they are still open past midnight. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/229. You can sign in with your GitHub ID to claim ownership of the project.

The defect

parseWeeklyHours files every interval under the weekday it starts on and deliberately lets it run past midnight — WeeklyHours documents exactly that: "DateIntervals are adjusted to the current week, and may extend to the next day." But isOpen and nextOpenInterval() only read the bucket for today, so once midnight passes, the interval that is currently running sits in yesterday's bucket and neither one finds it. This is the case the // TODO: Fixme, Dining halls may have open/close times that overlap multiple days. comment directly above isOpen refers to.

Date.sameDayThisWeek() compounds it. It searches forward from the start of today, so an interval that began yesterday and is still running is not merely in the wrong bucket — it gets projected a week into the future.

The user-visible result: a dining hall open Friday 21:00 to Saturday 01:00 shows the red "Closed" badge from midnight until it actually closes, and its hours row disappears from the detail card — precisely during the late-night window someone would open the app to check.

Reproducing on current master

The open-hours logic is plain Foundation, so it can be exercised directly against the four real source files. Save this as main.swift:

import Foundation

struct Venue: HasOpenTimes { var weeklyHours: WeeklyHours? }

func epoch(_ y: Int, _ mo: Int, _ d: Int, _ h: Int, _ mi: Int) -> Double {
    var c = DateComponents(); c.year = y; c.month = mo; c.day = d; c.hour = h; c.minute = mi
    return Calendar.current.date(from: c)!.timeIntervalSince1970
}

// A venue open Friday 21:00 -> Saturday 01:00.
let hours: [[String: Any]] = [["open_time": epoch(2026, 8, 14, 21, 0),
                               "close_time": epoch(2026, 8, 15, 1, 0)]]
let venue = Venue(weeklyHours: Venue.parseWeeklyHours(dict: hours))
print("isOpen:", String(describing: venue.isOpen),
      "| nextOpenInterval:", venue.nextOpenInterval() == nil ? "nil" : "present")

then build it against the repo and run it with the clock frozen at Saturday 00:30, when the venue is open:

swiftc -swift-version 5 -o repro \
  berkeley-mobile/Data/ItemProtocols/HasOpenTimes.swift \
  berkeley-mobile/Utils/WeeklyHours.swift \
  berkeley-mobile/Utils/DayOfWeek.swift \
  berkeley-mobile/Utils/Date+Extension.swift \
  main.swift

TZ=America/Los_Angeles faketime '2026-08-15 00:30:00' ./repro

On master:

isOpen: Optional(false) | nextOpenInterval: nil

With this branch:

isOpen: Optional(true) | nextOpenInterval: present

The fix

Date.sameDayThisWeek() now searches from the start of yesterday rather than the start of today, so an occurrence that began yesterday and may still be running stays in the current week instead of jumping forward a week. Anything from today onwards resolves exactly as it did before.

isOpen and nextOpenInterval() now consult the previous day's intervals alongside today's, through a small private helper. An interval that ended yesterday still reads as closed, because matching still requires the interval to actually contain the current time — pulling in the extra bucket cannot turn a closed venue open.

Nothing public changes shape: sameDayThisWeek()'s only caller in the app is parseWeeklyHours, and the weekly schedule rows are unaffected because they render each interval through timeOnly(), which uses only the hour and minute.

Verification

The project has no test target, so rather than add one blind to the Xcode project I ran the harness above across a scenario matrix, before and after, with the clock frozen for each case:

scenario master this branch
Fri 21:00–Sat 01:00, now Sat 00:30 Closed (wrong) Open
Sat 21:00–Sun 01:00, now Sun 00:30 (week wraps) Closed (wrong) Open
Sat 21:00–Sun 01:00 over the DST end weekend, now Sun 00:30 Closed (wrong) Open
Sat 21:00–Sun 03:00 spanning the 02:00 fall-back, now Sun 02:30 Closed (wrong) Open
Fri 21:00–Sat 01:00, now Fri 22:00 (before midnight) Open Open
Sat 08:00–20:00, now Sat 12:00 Open Open
Sat 08:00–20:00, now Sat 06:00 (opens later today) Closed Closed
Fri 21:00–Sat 01:00, now Sat 03:00 (after closing) Closed Closed
Fri 09:00–17:00, now Sat 12:00 (yesterday's closed interval must not leak) Closed Closed
Sat 08:00–11:00 and 17:00–21:00, now Sat 18:00 Open Open
venue with no hours at all nil nil

That is 7 passing / 4 failing on master, and 11 passing / 0 failing here — the four that flip are the overnight cases, and every previously-passing case still passes. The ninth row is the control that matters most for this change: a venue whose yesterday interval has already ended must keep reading Closed, and it does.

How this was managed

This work was tracked as Venues with hours past midnight report Closed while they are open on a board at https://eastagiletracker.com/projects/229 imported from this repository's own issues and pull requests (572 stories, 13 labels), where the story was picked up and moved through to done as the fix was written.

board

If you'd rather not receive contributions like this, reply no-more-prs on this pull request and we won't open any further ones on your repositories.


Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com

parseWeeklyHours files each interval under the weekday it starts on and
lets it run past midnight, but isOpen and nextOpenInterval() only read
today's bucket. Between midnight and closing time a venue therefore
reports Closed and its hours disappear from the detail card.

sameDayThisWeek() compounded this by searching forward from the start of
today, so an interval that began yesterday and is still running was
projected a week into the future instead of staying in the current week.

Search from the start of yesterday instead, and check the previous day's
intervals alongside today's when resolving the current interval. Days
from today onwards resolve exactly as before, and an interval that ended
yesterday still reads as closed because the interval must contain the
current time to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant