// // BracketModel.swift // IceGlass // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import Foundation struct PlayoffBracket: Codable { let series: [Series] struct Series: Codable { let seriesUrl: String? let seriesLetter: String let playoffRound: Int let seriesTitle: String let topSeedTeam: Team? let bottomSeedTeam: Team? let topSeedWins: Int let bottomSeedWins: Int struct Team: Codable { let abbrev: String } /// True once both teams are known (i.e. the series is actually matched up). var isMatched: Bool { topSeedTeam != nil && bottomSeedTeam != nil } var isOver: Bool { topSeedWins == 4 || bottomSeedWins == 4 } /// 1-based game number of the next unplayed game in the series (nil if series is over). var nextGameNumber: Int? { isOver ? nil : topSeedWins + bottomSeedWins + 1 } /// Abbrev of whichever side has reached 4 wins, nil if series ongoing. var winner: String? { guard isOver else { return nil } return topSeedWins == 4 ? topSeedTeam?.abbrev : bottomSeedTeam?.abbrev } /// Abbrev of the eliminated side, nil if the series is ongoing. var loser: String? { guard isOver else { return nil } return topSeedWins == 4 ? bottomSeedTeam?.abbrev : topSeedTeam?.abbrev } /// Absolute URL for the NHL.com series page, or nil if the bracket doesn't provide one. var fullSeriesUrl: String? { seriesUrl.map { "https://www.nhl.com\($0)" } } func involves(away: String, home: String) -> Bool { guard let top = topSeedTeam, let bottom = bottomSeedTeam else { return false } let pair = Set([away, home]) return pair == Set([top.abbrev, bottom.abbrev]) } } /// Lowest round number that still has at least one matched, live series. var currentRound: Int? { series .filter { $0.isMatched && !$0.isOver } .map(\.playoffRound) .min() } /// All matched series in the current round. var currentRoundSeries: [Series] { guard let round = currentRound else { return [] } return matchedSeries(inRound: round) } /// Rounds that have at least one matched series, ascending (1…current). var activeRounds: [Int] { Set(series.filter(\.isMatched).map(\.playoffRound)).sorted() } /// All matched series in the given round, sorted by series letter. func matchedSeries(inRound round: Int) -> [Series] { series .filter { $0.playoffRound == round && $0.isMatched } .sorted { $0.seriesLetter < $1.seriesLetter } } func series(for game: Scoreboard.Game) -> Series? { series.first { $0.involves(away: game.awayTeam.abbrev, home: game.homeTeam.abbrev) } } }