// // ScoreboardModel.swift // IceGlass // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import Foundation struct Scoreboard: Codable { let focusedDate: String let focusedDateCount: Int let gamesByDate: [GameDay] struct GameDay: Codable { let date: String // "YYYY-MM-DD" let games: [Game] } struct Game: Codable, Equatable { static func == (lhs: Game, rhs: Game) -> Bool { lhs.id == rhs.id } let id: Int let season: Int let gameType: Int let gameDate: String let gameCenterLink: String let startTimeUTC: String let gameState: String let gameScheduleState: String let awayTeam: Team let homeTeam: Team let period: Int? let periodDescriptor: PeriodDescriptor? struct LocalizedString: Codable { let `default`: String } struct Team: Codable { let id: Int let name: LocalizedString let commonName: LocalizedString let abbrev: String let score: Int? let record: String? let logo: String } struct PeriodDescriptor: Codable { let number: Int let periodType: String let maxRegulationPeriods: Int } // MARK: - Computed Properties var parsedGameState: GameState { GameState(rawValue: gameState) ?? .future } var date: Date { ISO8601DateFormatter().date(from: startTimeUTC) ?? .now } var gameCenterUrl: String { "https://www.nhl.com\(gameCenterLink)" } var videocastUrl: String { "https://videocast.nhl.com/game/\(id)/usnded?autoplay=true" } /// Time string in ET, right-aligned to 8 chars (e.g. " 9:30 PM", "10:00 PM") var startTimeET: String { let raw = date.formatDateET(format: "h:mm a") return raw.count < 8 ? String(repeating: " ", count: 8 - raw.count) + raw : raw } /// Formatted menu title: /// "NYR @ WAS 0: 2 9:30 PM" (finished/live — padded score + time) /// "DAL @ TOR 7:30 PM" (future — no score gap) var menuTitle: String { let state = parsedGameState let matchup = "\(awayTeam.abbrev) @ \(homeTeam.abbrev)" if state.isFuture { return "\(matchup) \(startTimeET)" } let aScore = String(format: "%2d", awayTeam.score ?? 0) let hScore = String(format: "%-2d", homeTeam.score ?? 0) return "\(matchup) \(aScore):\(hScore) \(startTimeET)" } /// Sequential game number encoded in the last 4 digits of `id`. /// Regular season: 1…~1312. Playoffs: 111–417 (`RSG` round/series/game). var seasonGameNumber: Int { id % 10_000 } /// Parsed playoff context; nil for non-playoff games. var playoffContext: PlayoffContext? { guard gameType == 3 else { return nil } let n = id % 1000 return PlayoffContext( round: n / 100, seriesInRound: (n / 10) % 10, gameInSeries: n % 10 ) } struct PlayoffContext: Equatable { let round: Int let seriesInRound: Int let gameInSeries: Int /// A…O by cumulative position (R1 S1 = A, R1 S8 = H, R2 S1 = I, …, R4 S1 = O). var seriesLetter: String { let roundStartIndex = [0, 0, 8, 12, 14] guard round >= 1, round <= 4 else { return "" } let index = roundStartIndex[round] + (seriesInRound - 1) guard index >= 0, index < 15 else { return "" } return String(UnicodeScalar(65 + index)!) } } } }