wip
This commit is contained in:
@@ -82,6 +82,7 @@ class Stadium:
|
|||||||
"primary_team_abbrevs": primary_team_abbrevs or [],
|
"primary_team_abbrevs": primary_team_abbrevs or [],
|
||||||
"year_opened": self.opened_year,
|
"year_opened": self.opened_year,
|
||||||
"timezone_identifier": self.timezone,
|
"timezone_identifier": self.timezone,
|
||||||
|
"image_url": self.image_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -118,6 +119,8 @@ class Stadium:
|
|||||||
longitude=data["longitude"],
|
longitude=data["longitude"],
|
||||||
capacity=data.get("capacity"),
|
capacity=data.get("capacity"),
|
||||||
opened_year=data.get("year_opened"),
|
opened_year=data.get("year_opened"),
|
||||||
|
image_url=data.get("image_url"),
|
||||||
|
timezone=data.get("timezone_identifier"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ class Team:
|
|||||||
"stadium_id": self.stadium_id,
|
"stadium_id": self.stadium_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _make_qualified_id(self, name: Optional[str]) -> Optional[str]:
|
||||||
|
"""Convert a conference/division name to a qualified ID.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"Eastern" → "nba_eastern"
|
||||||
|
"AL West" → "mlb_al_west"
|
||||||
|
"Southeast" → "nba_southeast"
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
# Lowercase, replace spaces with underscores
|
||||||
|
normalized = name.lower().replace(" ", "_")
|
||||||
|
return f"{self.sport.lower()}_{normalized}"
|
||||||
|
|
||||||
def to_canonical_dict(self) -> dict:
|
def to_canonical_dict(self) -> dict:
|
||||||
"""Convert to canonical dictionary format matching iOS app schema.
|
"""Convert to canonical dictionary format matching iOS app schema.
|
||||||
|
|
||||||
@@ -67,8 +81,8 @@ class Team:
|
|||||||
"sport": self.sport.upper(), # iOS Sport enum expects uppercase (e.g., "NFL")
|
"sport": self.sport.upper(), # iOS Sport enum expects uppercase (e.g., "NFL")
|
||||||
"city": self.city,
|
"city": self.city,
|
||||||
"stadium_canonical_id": self.stadium_id or "",
|
"stadium_canonical_id": self.stadium_id or "",
|
||||||
"conference_id": self.conference,
|
"conference_id": self._make_qualified_id(self.conference),
|
||||||
"division_id": self.division,
|
"division_id": self._make_qualified_id(self.division),
|
||||||
"primary_color": self.primary_color,
|
"primary_color": self.primary_color,
|
||||||
"secondary_color": self.secondary_color,
|
"secondary_color": self.secondary_color,
|
||||||
}
|
}
|
||||||
@@ -91,9 +105,38 @@ class Team:
|
|||||||
stadium_id=data.get("stadium_id"),
|
stadium_id=data.get("stadium_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_name_from_qualified_id(qualified_id: Optional[str], sport: str) -> Optional[str]:
|
||||||
|
"""Extract the name portion from a qualified ID.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"nba_eastern" → "Eastern"
|
||||||
|
"mlb_al_west" → "AL West"
|
||||||
|
"nba_southeast" → "Southeast"
|
||||||
|
"""
|
||||||
|
if not qualified_id:
|
||||||
|
return None
|
||||||
|
# Remove sport prefix (e.g., "nba_" or "mlb_")
|
||||||
|
prefix = f"{sport.lower()}_"
|
||||||
|
if qualified_id.startswith(prefix):
|
||||||
|
name = qualified_id[len(prefix):]
|
||||||
|
else:
|
||||||
|
name = qualified_id
|
||||||
|
# Convert underscores to spaces and title case
|
||||||
|
# Special handling for league abbreviations (AL, NL, etc.)
|
||||||
|
parts = name.split("_")
|
||||||
|
result_parts = []
|
||||||
|
for part in parts:
|
||||||
|
if part.upper() in ("AL", "NL", "AFC", "NFC"):
|
||||||
|
result_parts.append(part.upper())
|
||||||
|
else:
|
||||||
|
result_parts.append(part.capitalize())
|
||||||
|
return " ".join(result_parts)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_canonical_dict(cls, data: dict) -> "Team":
|
def from_canonical_dict(cls, data: dict) -> "Team":
|
||||||
"""Create a Team from a canonical dictionary (iOS app format)."""
|
"""Create a Team from a canonical dictionary (iOS app format)."""
|
||||||
|
sport = data["sport"].lower()
|
||||||
return cls(
|
return cls(
|
||||||
id=data["canonical_id"],
|
id=data["canonical_id"],
|
||||||
sport=data["sport"],
|
sport=data["sport"],
|
||||||
@@ -101,8 +144,8 @@ class Team:
|
|||||||
name=data["name"],
|
name=data["name"],
|
||||||
full_name=f"{data['city']} {data['name']}", # Reconstruct full_name
|
full_name=f"{data['city']} {data['name']}", # Reconstruct full_name
|
||||||
abbreviation=data["abbreviation"],
|
abbreviation=data["abbreviation"],
|
||||||
conference=data.get("conference_id"),
|
conference=cls._extract_name_from_qualified_id(data.get("conference_id"), sport),
|
||||||
division=data.get("division_id"),
|
division=cls._extract_name_from_qualified_id(data.get("division_id"), sport),
|
||||||
primary_color=data.get("primary_color"),
|
primary_color=data.get("primary_color"),
|
||||||
secondary_color=data.get("secondary_color"),
|
secondary_color=data.get("secondary_color"),
|
||||||
stadium_id=data.get("stadium_canonical_id"),
|
stadium_id=data.get("stadium_canonical_id"),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def generate_game_id(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Generate a canonical game ID.
|
"""Generate a canonical game ID.
|
||||||
|
|
||||||
Format: {sport}_{season}_{away}_{home}_{MMDD}[_{game_number}]
|
Format: game_{sport}_{season}_{YYYYMMDD}_{away}_{home}[_{game_number}]
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sport: Sport code (e.g., 'nba', 'mlb')
|
sport: Sport code (e.g., 'nba', 'mlb')
|
||||||
@@ -64,27 +64,27 @@ def generate_game_id(
|
|||||||
game_number: Game number for doubleheaders (1 or 2), None for single games
|
game_number: Game number for doubleheaders (1 or 2), None for single games
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Canonical game ID (e.g., 'nba_2025_hou_okc_1021')
|
Canonical game ID (e.g., 'game_nba_2025_20251021_hou_okc')
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> generate_game_id('nba', 2025, 'HOU', 'OKC', date(2025, 10, 21))
|
>>> generate_game_id('nba', 2025, 'HOU', 'OKC', date(2025, 10, 21))
|
||||||
'nba_2025_hou_okc_1021'
|
'game_nba_2025_20251021_hou_okc'
|
||||||
|
|
||||||
>>> generate_game_id('mlb', 2026, 'NYY', 'BOS', date(2026, 4, 1), game_number=1)
|
>>> generate_game_id('mlb', 2026, 'NYY', 'BOS', date(2026, 4, 1), game_number=1)
|
||||||
'mlb_2026_nyy_bos_0401_1'
|
'game_mlb_2026_20260401_nyy_bos_1'
|
||||||
"""
|
"""
|
||||||
# Normalize sport and abbreviations
|
# Normalize sport and abbreviations
|
||||||
sport_norm = sport.lower()
|
sport_norm = sport.lower()
|
||||||
away_norm = away_abbrev.lower()
|
away_norm = away_abbrev.lower()
|
||||||
home_norm = home_abbrev.lower()
|
home_norm = home_abbrev.lower()
|
||||||
|
|
||||||
# Format date as MMDD
|
# Format date as YYYYMMDD
|
||||||
if isinstance(game_date, datetime):
|
if isinstance(game_date, datetime):
|
||||||
game_date = game_date.date()
|
game_date = game_date.date()
|
||||||
date_str = game_date.strftime("%m%d")
|
date_str = game_date.strftime("%Y%m%d")
|
||||||
|
|
||||||
# Build ID
|
# Build ID with game_ prefix
|
||||||
parts = [sport_norm, str(season), away_norm, home_norm, date_str]
|
parts = ["game", sport_norm, str(season), date_str, away_norm, home_norm]
|
||||||
|
|
||||||
# Add game number for doubleheaders
|
# Add game number for doubleheaders
|
||||||
if game_number is not None:
|
if game_number is not None:
|
||||||
@@ -177,50 +177,55 @@ def parse_game_id(game_id: str) -> dict:
|
|||||||
"""Parse a canonical game ID into its components.
|
"""Parse a canonical game ID into its components.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
game_id: Canonical game ID (e.g., 'nba_2025_hou_okc_1021')
|
game_id: Canonical game ID (e.g., 'game_nba_2025_20251021_hou_okc')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with keys: sport, season, away_abbrev, home_abbrev,
|
Dictionary with keys: sport, season, away_abbrev, home_abbrev,
|
||||||
month, day, game_number (optional)
|
year, month, day, game_number (optional)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If game_id format is invalid
|
ValueError: If game_id format is invalid
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> parse_game_id('nba_2025_hou_okc_1021')
|
>>> parse_game_id('game_nba_2025_20251021_hou_okc')
|
||||||
{'sport': 'nba', 'season': 2025, 'away_abbrev': 'hou',
|
{'sport': 'nba', 'season': 2025, 'away_abbrev': 'hou',
|
||||||
'home_abbrev': 'okc', 'month': 10, 'day': 21, 'game_number': None}
|
'home_abbrev': 'okc', 'year': 2025, 'month': 10, 'day': 21, 'game_number': None}
|
||||||
|
|
||||||
>>> parse_game_id('mlb_2026_nyy_bos_0401_1')
|
>>> parse_game_id('game_mlb_2026_20260401_nyy_bos_1')
|
||||||
{'sport': 'mlb', 'season': 2026, 'away_abbrev': 'nyy',
|
{'sport': 'mlb', 'season': 2026, 'away_abbrev': 'nyy',
|
||||||
'home_abbrev': 'bos', 'month': 4, 'day': 1, 'game_number': 1}
|
'home_abbrev': 'bos', 'year': 2026, 'month': 4, 'day': 1, 'game_number': 1}
|
||||||
"""
|
"""
|
||||||
parts = game_id.split("_")
|
parts = game_id.split("_")
|
||||||
|
|
||||||
if len(parts) < 5 or len(parts) > 6:
|
if len(parts) < 6 or len(parts) > 7:
|
||||||
raise ValueError(f"Invalid game ID format: {game_id}")
|
raise ValueError(f"Invalid game ID format: {game_id}")
|
||||||
|
|
||||||
sport = parts[0]
|
if parts[0] != "game":
|
||||||
season = int(parts[1])
|
raise ValueError(f"Game ID must start with 'game_': {game_id}")
|
||||||
away_abbrev = parts[2]
|
|
||||||
home_abbrev = parts[3]
|
|
||||||
date_str = parts[4]
|
|
||||||
|
|
||||||
if len(date_str) != 4:
|
sport = parts[1]
|
||||||
|
season = int(parts[2])
|
||||||
|
date_str = parts[3]
|
||||||
|
away_abbrev = parts[4]
|
||||||
|
home_abbrev = parts[5]
|
||||||
|
|
||||||
|
if len(date_str) != 8:
|
||||||
raise ValueError(f"Invalid date format in game ID: {game_id}")
|
raise ValueError(f"Invalid date format in game ID: {game_id}")
|
||||||
|
|
||||||
month = int(date_str[:2])
|
year = int(date_str[:4])
|
||||||
day = int(date_str[2:])
|
month = int(date_str[4:6])
|
||||||
|
day = int(date_str[6:])
|
||||||
|
|
||||||
game_number = None
|
game_number = None
|
||||||
if len(parts) == 6:
|
if len(parts) == 7:
|
||||||
game_number = int(parts[5])
|
game_number = int(parts[6])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"sport": sport,
|
"sport": sport,
|
||||||
"season": season,
|
"season": season,
|
||||||
"away_abbrev": away_abbrev,
|
"away_abbrev": away_abbrev,
|
||||||
"home_abbrev": home_abbrev,
|
"home_abbrev": home_abbrev,
|
||||||
|
"year": year,
|
||||||
"month": month,
|
"month": month,
|
||||||
"day": day,
|
"day": day,
|
||||||
"game_number": game_number,
|
"game_number": game_number,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class TestGenerateGameId:
|
|||||||
home_abbrev="lal",
|
home_abbrev="lal",
|
||||||
game_date=date(2025, 12, 25),
|
game_date=date(2025, 12, 25),
|
||||||
)
|
)
|
||||||
assert game_id == "nba_2025_bos_lal_1225"
|
assert game_id == "game_nba_2025_20251225_bos_lal"
|
||||||
|
|
||||||
def test_game_id_with_datetime(self):
|
def test_game_id_with_datetime(self):
|
||||||
"""Test game ID generation with datetime object."""
|
"""Test game ID generation with datetime object."""
|
||||||
@@ -66,7 +66,7 @@ class TestGenerateGameId:
|
|||||||
home_abbrev="bos",
|
home_abbrev="bos",
|
||||||
game_date=datetime(2026, 4, 1, 19, 0),
|
game_date=datetime(2026, 4, 1, 19, 0),
|
||||||
)
|
)
|
||||||
assert game_id == "mlb_2026_nyy_bos_0401"
|
assert game_id == "game_mlb_2026_20260401_nyy_bos"
|
||||||
|
|
||||||
def test_game_id_with_game_number(self):
|
def test_game_id_with_game_number(self):
|
||||||
"""Test game ID for doubleheader."""
|
"""Test game ID for doubleheader."""
|
||||||
@@ -86,8 +86,8 @@ class TestGenerateGameId:
|
|||||||
game_date=date(2026, 7, 4),
|
game_date=date(2026, 7, 4),
|
||||||
game_number=2,
|
game_number=2,
|
||||||
)
|
)
|
||||||
assert game_id_1 == "mlb_2026_nyy_bos_0704_1"
|
assert game_id_1 == "game_mlb_2026_20260704_nyy_bos_1"
|
||||||
assert game_id_2 == "mlb_2026_nyy_bos_0704_2"
|
assert game_id_2 == "game_mlb_2026_20260704_nyy_bos_2"
|
||||||
|
|
||||||
def test_sport_lowercased(self):
|
def test_sport_lowercased(self):
|
||||||
"""Test that sport is lowercased."""
|
"""Test that sport is lowercased."""
|
||||||
@@ -98,7 +98,7 @@ class TestGenerateGameId:
|
|||||||
home_abbrev="LAL",
|
home_abbrev="LAL",
|
||||||
game_date=date(2025, 12, 25),
|
game_date=date(2025, 12, 25),
|
||||||
)
|
)
|
||||||
assert game_id == "nba_2025_bos_lal_1225"
|
assert game_id == "game_nba_2025_20251225_bos_lal"
|
||||||
|
|
||||||
|
|
||||||
class TestParseGameId:
|
class TestParseGameId:
|
||||||
@@ -106,22 +106,24 @@ class TestParseGameId:
|
|||||||
|
|
||||||
def test_parse_basic_game_id(self):
|
def test_parse_basic_game_id(self):
|
||||||
"""Test parsing a basic game ID."""
|
"""Test parsing a basic game ID."""
|
||||||
parsed = parse_game_id("nba_2025_bos_lal_1225")
|
parsed = parse_game_id("game_nba_2025_20251225_bos_lal")
|
||||||
assert parsed["sport"] == "nba"
|
assert parsed["sport"] == "nba"
|
||||||
assert parsed["season"] == 2025
|
assert parsed["season"] == 2025
|
||||||
assert parsed["away_abbrev"] == "bos"
|
assert parsed["away_abbrev"] == "bos"
|
||||||
assert parsed["home_abbrev"] == "lal"
|
assert parsed["home_abbrev"] == "lal"
|
||||||
|
assert parsed["year"] == 2025
|
||||||
assert parsed["month"] == 12
|
assert parsed["month"] == 12
|
||||||
assert parsed["day"] == 25
|
assert parsed["day"] == 25
|
||||||
assert parsed["game_number"] is None
|
assert parsed["game_number"] is None
|
||||||
|
|
||||||
def test_parse_game_id_with_game_number(self):
|
def test_parse_game_id_with_game_number(self):
|
||||||
"""Test parsing game ID with game number."""
|
"""Test parsing game ID with game number."""
|
||||||
parsed = parse_game_id("mlb_2026_nyy_bos_0704_2")
|
parsed = parse_game_id("game_mlb_2026_20260704_nyy_bos_2")
|
||||||
assert parsed["sport"] == "mlb"
|
assert parsed["sport"] == "mlb"
|
||||||
assert parsed["season"] == 2026
|
assert parsed["season"] == 2026
|
||||||
assert parsed["away_abbrev"] == "nyy"
|
assert parsed["away_abbrev"] == "nyy"
|
||||||
assert parsed["home_abbrev"] == "bos"
|
assert parsed["home_abbrev"] == "bos"
|
||||||
|
assert parsed["year"] == 2026
|
||||||
assert parsed["month"] == 7
|
assert parsed["month"] == 7
|
||||||
assert parsed["day"] == 4
|
assert parsed["day"] == 4
|
||||||
assert parsed["game_number"] == 2
|
assert parsed["game_number"] == 2
|
||||||
@@ -131,9 +133,11 @@ class TestParseGameId:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
parse_game_id("invalid")
|
parse_game_id("invalid")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
parse_game_id("nba_2025_bos")
|
parse_game_id("nba_2025_bos") # Missing game_ prefix
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
parse_game_id("")
|
parse_game_id("")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_game_id("game_nba_2025_bos_lal") # Missing date
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateTeamId:
|
class TestGenerateTeamId:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ struct Stadium: Identifiable, Codable, Hashable {
|
|||||||
let yearOpened: Int?
|
let yearOpened: Int?
|
||||||
let imageURL: URL?
|
let imageURL: URL?
|
||||||
let timeZoneIdentifier: String?
|
let timeZoneIdentifier: String?
|
||||||
|
let primaryTeamAbbreviations: [String] // Teams that play here (e.g., ["LAC", "LAR"] for SoFi Stadium)
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String,
|
id: String,
|
||||||
@@ -43,7 +44,8 @@ struct Stadium: Identifiable, Codable, Hashable {
|
|||||||
sport: Sport,
|
sport: Sport,
|
||||||
yearOpened: Int? = nil,
|
yearOpened: Int? = nil,
|
||||||
imageURL: URL? = nil,
|
imageURL: URL? = nil,
|
||||||
timeZoneIdentifier: String? = nil
|
timeZoneIdentifier: String? = nil,
|
||||||
|
primaryTeamAbbreviations: [String] = []
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -56,6 +58,7 @@ struct Stadium: Identifiable, Codable, Hashable {
|
|||||||
self.yearOpened = yearOpened
|
self.yearOpened = yearOpened
|
||||||
self.imageURL = imageURL
|
self.imageURL = imageURL
|
||||||
self.timeZoneIdentifier = timeZoneIdentifier
|
self.timeZoneIdentifier = timeZoneIdentifier
|
||||||
|
self.primaryTeamAbbreviations = primaryTeamAbbreviations
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeZone: TimeZone? {
|
var timeZone: TimeZone? {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ struct Team: Identifiable, Codable, Hashable {
|
|||||||
let sport: Sport
|
let sport: Sport
|
||||||
let city: String
|
let city: String
|
||||||
let stadiumId: String // FK: "stadium_mlb_fenway_park"
|
let stadiumId: String // FK: "stadium_mlb_fenway_park"
|
||||||
|
let conferenceId: String? // FK: "nba_eastern", "mlb_al"
|
||||||
|
let divisionId: String? // FK: "nba_southeast", "mlb_al_east"
|
||||||
let logoURL: URL?
|
let logoURL: URL?
|
||||||
let primaryColor: String?
|
let primaryColor: String?
|
||||||
let secondaryColor: String?
|
let secondaryColor: String?
|
||||||
@@ -34,6 +36,8 @@ struct Team: Identifiable, Codable, Hashable {
|
|||||||
sport: Sport,
|
sport: Sport,
|
||||||
city: String,
|
city: String,
|
||||||
stadiumId: String,
|
stadiumId: String,
|
||||||
|
conferenceId: String? = nil,
|
||||||
|
divisionId: String? = nil,
|
||||||
logoURL: URL? = nil,
|
logoURL: URL? = nil,
|
||||||
primaryColor: String? = nil,
|
primaryColor: String? = nil,
|
||||||
secondaryColor: String? = nil
|
secondaryColor: String? = nil
|
||||||
@@ -44,6 +48,8 @@ struct Team: Identifiable, Codable, Hashable {
|
|||||||
self.sport = sport
|
self.sport = sport
|
||||||
self.city = city
|
self.city = city
|
||||||
self.stadiumId = stadiumId
|
self.stadiumId = stadiumId
|
||||||
|
self.conferenceId = conferenceId
|
||||||
|
self.divisionId = divisionId
|
||||||
self.logoURL = logoURL
|
self.logoURL = logoURL
|
||||||
self.primaryColor = primaryColor
|
self.primaryColor = primaryColor
|
||||||
self.secondaryColor = secondaryColor
|
self.secondaryColor = secondaryColor
|
||||||
|
|||||||
@@ -320,6 +320,8 @@ final class CanonicalTeam {
|
|||||||
sport: sportEnum ?? .mlb,
|
sport: sportEnum ?? .mlb,
|
||||||
city: city,
|
city: city,
|
||||||
stadiumId: stadiumCanonicalId,
|
stadiumId: stadiumCanonicalId,
|
||||||
|
conferenceId: conferenceId,
|
||||||
|
divisionId: divisionId,
|
||||||
logoURL: logoURL.flatMap { URL(string: $0) },
|
logoURL: logoURL.flatMap { URL(string: $0) },
|
||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
secondaryColor: secondaryColor
|
secondaryColor: secondaryColor
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ actor BootstrapService {
|
|||||||
let primary_team_abbrevs: [String]
|
let primary_team_abbrevs: [String]
|
||||||
let year_opened: Int?
|
let year_opened: Int?
|
||||||
let timezone_identifier: String?
|
let timezone_identifier: String?
|
||||||
|
let image_url: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct JSONCanonicalTeam: Codable {
|
private struct JSONCanonicalTeam: Codable {
|
||||||
@@ -66,8 +67,9 @@ actor BootstrapService {
|
|||||||
let canonical_id: String
|
let canonical_id: String
|
||||||
let sport: String
|
let sport: String
|
||||||
let season: String
|
let season: String
|
||||||
let date: String
|
let game_datetime_utc: String? // ISO 8601 format (new canonical format)
|
||||||
let time: String?
|
let date: String? // Legacy format (deprecated)
|
||||||
|
let time: String? // Legacy format (deprecated)
|
||||||
let home_team_canonical_id: String
|
let home_team_canonical_id: String
|
||||||
let away_team_canonical_id: String
|
let away_team_canonical_id: String
|
||||||
let stadium_canonical_id: String
|
let stadium_canonical_id: String
|
||||||
@@ -215,6 +217,7 @@ actor BootstrapService {
|
|||||||
longitude: jsonStadium.longitude,
|
longitude: jsonStadium.longitude,
|
||||||
capacity: jsonStadium.capacity,
|
capacity: jsonStadium.capacity,
|
||||||
yearOpened: jsonStadium.year_opened,
|
yearOpened: jsonStadium.year_opened,
|
||||||
|
imageURL: jsonStadium.image_url,
|
||||||
sport: jsonStadium.sport,
|
sport: jsonStadium.sport,
|
||||||
timezoneIdentifier: jsonStadium.timezone_identifier
|
timezoneIdentifier: jsonStadium.timezone_identifier
|
||||||
)
|
)
|
||||||
@@ -423,10 +426,18 @@ actor BootstrapService {
|
|||||||
guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
|
guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
|
||||||
seenGameIds.insert(jsonGame.canonical_id)
|
seenGameIds.insert(jsonGame.canonical_id)
|
||||||
|
|
||||||
guard let dateTime = parseDateTime(date: jsonGame.date, time: jsonGame.time ?? "7:00p") else {
|
// Parse datetime: prefer ISO 8601 format, fall back to legacy date+time
|
||||||
continue
|
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 }
|
||||||
|
|
||||||
let game = CanonicalGame(
|
let game = CanonicalGame(
|
||||||
canonicalId: jsonGame.canonical_id,
|
canonicalId: jsonGame.canonical_id,
|
||||||
schemaVersion: SchemaVersion.current,
|
schemaVersion: SchemaVersion.current,
|
||||||
@@ -677,6 +688,13 @@ actor BootstrapService {
|
|||||||
return "venue_unknown_\(venue.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
return "venue_unknown_\(venue.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated private func parseISO8601(_ string: String) -> Date? {
|
||||||
|
// Handle ISO 8601 format: "2026-03-01T18:05:00Z"
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime]
|
||||||
|
return formatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
nonisolated private func parseDateTime(date: String, time: String) -> Date? {
|
nonisolated private func parseDateTime(date: String, time: String) -> Date? {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
|||||||
Reference in New Issue
Block a user