Fix game times with UTC data, restructure schedule by date

- Update games_canonical.json to use ISO 8601 UTC timestamps (game_datetime_utc)
- Fix BootstrapService timezone-aware parsing for venue-local fallback
- Fix thread-unsafe shared DateFormatter in RichGame local time display
- Bump SchemaVersion to 4 to force re-bootstrap with correct UTC data
- Restructure schedule view: group by date instead of sport, with sport
  icons on each row and date section headers showing game counts
- Fix schedule row backgrounds using Theme.cardBackground instead of black
- Sort games by UTC time with local-time tiebreaker for same-instant games

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 11:43:39 -06:00
parent e6c4b8e12b
commit 999b5a1190
12 changed files with 13387 additions and 26877 deletions

View File

@@ -413,23 +413,20 @@ final class BootstrapService {
let teams = (try? context.fetch(FetchDescriptor<CanonicalTeam>())) ?? []
let stadiumByTeamId = Dictionary(uniqueKeysWithValues: teams.map { ($0.canonicalId, $0.stadiumCanonicalId) })
// Build stadium timezone lookup for correct local time parsing
let stadiums = (try? context.fetch(FetchDescriptor<CanonicalStadium>())) ?? []
let timezoneByStadiumId: [String: TimeZone] = stadiums.reduce(into: [:]) { dict, stadium in
if let tzId = stadium.timezoneIdentifier, let tz = TimeZone(identifier: tzId) {
dict[stadium.canonicalId] = tz
}
}
for jsonGame in games {
// Deduplicate
guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
seenGameIds.insert(jsonGame.canonical_id)
// Parse datetime: prefer ISO 8601 format, fall back to date+time
let dateTime: Date?
if let iso8601String = jsonGame.game_datetime_utc {
dateTime = parseISO8601(iso8601String)
} else if let date = jsonGame.date {
dateTime = parseDateTime(date: date, time: jsonGame.time ?? "7:00p")
} else {
dateTime = nil
}
guard let dateTime else { continue }
// Resolve stadium ID first (needed for timezone lookup)
let explicitStadium = jsonGame.stadium_canonical_id?
.trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedStadiumCanonicalId: String
@@ -445,6 +442,20 @@ final class BootstrapService {
resolvedStadiumCanonicalId = "stadium_placeholder_\(jsonGame.canonical_id)"
}
// Parse datetime: prefer ISO 8601 format, fall back to date+time
// Times in JSON are venue-local, so parse in the stadium's timezone
let venueTimeZone = timezoneByStadiumId[resolvedStadiumCanonicalId]
let dateTime: Date?
if let iso8601String = jsonGame.game_datetime_utc {
dateTime = parseISO8601(iso8601String)
} else if let date = jsonGame.date {
dateTime = parseDateTime(date: date, time: jsonGame.time ?? "7:00p", timeZone: venueTimeZone)
} else {
dateTime = nil
}
guard let dateTime else { continue }
let game = CanonicalGame(
canonicalId: jsonGame.canonical_id,
schemaVersion: SchemaVersion.current,
@@ -534,10 +545,14 @@ final class BootstrapService {
return formatter.date(from: string)
}
nonisolated private func parseDateTime(date: String, time: String) -> Date? {
nonisolated private func parseDateTime(date: String, time: String, timeZone: TimeZone? = nil) -> Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
// Use the venue's timezone so "1:05p" is interpreted as 1:05 PM at the stadium
let tz = timeZone ?? .current
formatter.timeZone = tz
// Parse date
formatter.dateFormat = "yyyy-MM-dd"
guard let dateOnly = formatter.date(from: date) else { return nil }
@@ -563,7 +578,9 @@ final class BootstrapService {
minute = m
}
return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: dateOnly)
var calendar = Calendar.current
calendar.timeZone = tz
return calendar.date(bySettingHour: hour, minute: minute, second: 0, of: dateOnly)
}
nonisolated private func stateFromCity(_ city: String) -> String {