refactor: change domain model IDs from UUID to String canonical IDs
This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ struct CKTeam {
|
|||||||
|
|
||||||
init(team: Team, stadiumRecordID: CKRecord.ID) {
|
init(team: Team, stadiumRecordID: CKRecord.ID) {
|
||||||
let record = CKRecord(recordType: CKRecordType.team)
|
let record = CKRecord(recordType: CKRecordType.team)
|
||||||
record[CKTeam.idKey] = team.id.uuidString
|
record[CKTeam.idKey] = team.id
|
||||||
record[CKTeam.nameKey] = team.name
|
record[CKTeam.nameKey] = team.name
|
||||||
record[CKTeam.abbreviationKey] = team.abbreviation
|
record[CKTeam.abbreviationKey] = team.abbreviation
|
||||||
record[CKTeam.sportKey] = team.sport.rawValue
|
record[CKTeam.sportKey] = team.sport.rawValue
|
||||||
@@ -67,8 +67,8 @@ struct CKTeam {
|
|||||||
|
|
||||||
var team: Team? {
|
var team: Team? {
|
||||||
// Use teamId field, or fall back to record name
|
// Use teamId field, or fall back to record name
|
||||||
let idString = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName
|
let id = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName
|
||||||
guard let id = UUID(uuidString: idString),
|
guard !id.isEmpty,
|
||||||
let abbreviation = record[CKTeam.abbreviationKey] as? String,
|
let abbreviation = record[CKTeam.abbreviationKey] as? String,
|
||||||
let sportRaw = record[CKTeam.sportKey] as? String,
|
let sportRaw = record[CKTeam.sportKey] as? String,
|
||||||
let sport = Sport(rawValue: sportRaw),
|
let sport = Sport(rawValue: sportRaw),
|
||||||
@@ -78,14 +78,13 @@ struct CKTeam {
|
|||||||
// Name defaults to abbreviation if not provided
|
// Name defaults to abbreviation if not provided
|
||||||
let name = record[CKTeam.nameKey] as? String ?? abbreviation
|
let name = record[CKTeam.nameKey] as? String ?? abbreviation
|
||||||
|
|
||||||
// Stadium reference is optional - use placeholder UUID if not present
|
// Stadium reference is optional - use placeholder string if not present
|
||||||
let stadiumId: UUID
|
let stadiumId: String
|
||||||
if let stadiumRef = record[CKTeam.stadiumRefKey] as? CKRecord.Reference,
|
if let stadiumRef = record[CKTeam.stadiumRefKey] as? CKRecord.Reference {
|
||||||
let refId = UUID(uuidString: stadiumRef.recordID.recordName) {
|
stadiumId = stadiumRef.recordID.recordName
|
||||||
stadiumId = refId
|
|
||||||
} else {
|
} else {
|
||||||
// Generate deterministic placeholder from team ID
|
// Generate placeholder from team ID
|
||||||
stadiumId = UUID()
|
stadiumId = "stadium_placeholder_\(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
let logoURL = (record[CKTeam.logoURLKey] as? String).flatMap { URL(string: $0) }
|
let logoURL = (record[CKTeam.logoURLKey] as? String).flatMap { URL(string: $0) }
|
||||||
@@ -126,7 +125,7 @@ struct CKStadium {
|
|||||||
|
|
||||||
init(stadium: Stadium) {
|
init(stadium: Stadium) {
|
||||||
let record = CKRecord(recordType: CKRecordType.stadium)
|
let record = CKRecord(recordType: CKRecordType.stadium)
|
||||||
record[CKStadium.idKey] = stadium.id.uuidString
|
record[CKStadium.idKey] = stadium.id
|
||||||
record[CKStadium.nameKey] = stadium.name
|
record[CKStadium.nameKey] = stadium.name
|
||||||
record[CKStadium.cityKey] = stadium.city
|
record[CKStadium.cityKey] = stadium.city
|
||||||
record[CKStadium.stateKey] = stadium.state
|
record[CKStadium.stateKey] = stadium.state
|
||||||
@@ -145,8 +144,8 @@ struct CKStadium {
|
|||||||
|
|
||||||
var stadium: Stadium? {
|
var stadium: Stadium? {
|
||||||
// Use stadiumId field, or fall back to record name
|
// Use stadiumId field, or fall back to record name
|
||||||
let idString = (record[CKStadium.idKey] as? String) ?? record.recordID.recordName
|
let id = (record[CKStadium.idKey] as? String) ?? record.recordID.recordName
|
||||||
guard let id = UUID(uuidString: idString),
|
guard !id.isEmpty,
|
||||||
let name = record[CKStadium.nameKey] as? String,
|
let name = record[CKStadium.nameKey] as? String,
|
||||||
let city = record[CKStadium.cityKey] as? String
|
let city = record[CKStadium.cityKey] as? String
|
||||||
else { return nil }
|
else { return nil }
|
||||||
@@ -199,7 +198,7 @@ struct CKGame {
|
|||||||
|
|
||||||
init(game: Game, homeTeamRecordID: CKRecord.ID, awayTeamRecordID: CKRecord.ID, stadiumRecordID: CKRecord.ID) {
|
init(game: Game, homeTeamRecordID: CKRecord.ID, awayTeamRecordID: CKRecord.ID, stadiumRecordID: CKRecord.ID) {
|
||||||
let record = CKRecord(recordType: CKRecordType.game)
|
let record = CKRecord(recordType: CKRecordType.game)
|
||||||
record[CKGame.idKey] = game.id.uuidString
|
record[CKGame.idKey] = game.id
|
||||||
record[CKGame.homeTeamRefKey] = CKRecord.Reference(recordID: homeTeamRecordID, action: .none)
|
record[CKGame.homeTeamRefKey] = CKRecord.Reference(recordID: homeTeamRecordID, action: .none)
|
||||||
record[CKGame.awayTeamRefKey] = CKRecord.Reference(recordID: awayTeamRecordID, action: .none)
|
record[CKGame.awayTeamRefKey] = CKRecord.Reference(recordID: awayTeamRecordID, action: .none)
|
||||||
record[CKGame.stadiumRefKey] = CKRecord.Reference(recordID: stadiumRecordID, action: .none)
|
record[CKGame.stadiumRefKey] = CKRecord.Reference(recordID: stadiumRecordID, action: .none)
|
||||||
@@ -231,9 +230,9 @@ struct CKGame {
|
|||||||
record[CKGame.stadiumCanonicalIdKey] as? String
|
record[CKGame.stadiumCanonicalIdKey] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
func game(homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID) -> Game? {
|
func game(homeTeamId: String, awayTeamId: String, stadiumId: String) -> Game? {
|
||||||
guard let idString = record[CKGame.idKey] as? String,
|
let id = (record[CKGame.idKey] as? String) ?? record.recordID.recordName
|
||||||
let id = UUID(uuidString: idString),
|
guard !id.isEmpty,
|
||||||
let dateTime = record[CKGame.dateTimeKey] as? Date,
|
let dateTime = record[CKGame.dateTimeKey] as? Date,
|
||||||
let sportRaw = record[CKGame.sportKey] as? String,
|
let sportRaw = record[CKGame.sportKey] as? String,
|
||||||
let sport = Sport(rawValue: sportRaw),
|
let sport = Sport(rawValue: sportRaw),
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ enum AchievementRegistry {
|
|||||||
sport: .mlb,
|
sport: .mlb,
|
||||||
iconName: "building.columns.fill",
|
iconName: "building.columns.fill",
|
||||||
iconColor: .green,
|
iconColor: .green,
|
||||||
requirement: .specificStadium("stadium_mlb_bos"),
|
requirement: .specificStadium("stadium_mlb_fenway_park"),
|
||||||
sortOrder: 600
|
sortOrder: 600
|
||||||
),
|
),
|
||||||
AchievementDefinition(
|
AchievementDefinition(
|
||||||
@@ -602,7 +602,7 @@ enum AchievementRegistry {
|
|||||||
sport: .mlb,
|
sport: .mlb,
|
||||||
iconName: "leaf.fill",
|
iconName: "leaf.fill",
|
||||||
iconColor: .green,
|
iconColor: .green,
|
||||||
requirement: .specificStadium("stadium_mlb_chc"),
|
requirement: .specificStadium("stadium_mlb_wrigley_field"),
|
||||||
sortOrder: 601
|
sortOrder: 601
|
||||||
),
|
),
|
||||||
AchievementDefinition(
|
AchievementDefinition(
|
||||||
@@ -613,7 +613,7 @@ enum AchievementRegistry {
|
|||||||
sport: .nba,
|
sport: .nba,
|
||||||
iconName: "sparkles",
|
iconName: "sparkles",
|
||||||
iconColor: .orange,
|
iconColor: .orange,
|
||||||
requirement: .specificStadium("stadium_nba_nyk"),
|
requirement: .specificStadium("stadium_nba_madison_square_garden"),
|
||||||
sortOrder: 602
|
sortOrder: 602
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Game: Identifiable, Codable, Hashable {
|
struct Game: Identifiable, Codable, Hashable {
|
||||||
let id: UUID
|
let id: String // Canonical ID: "game_mlb_2026_bos_nyy_0401"
|
||||||
let homeTeamId: UUID
|
let homeTeamId: String // FK: "team_mlb_bos"
|
||||||
let awayTeamId: UUID
|
let awayTeamId: String // FK: "team_mlb_nyy"
|
||||||
let stadiumId: UUID
|
let stadiumId: String // FK: "stadium_mlb_fenway_park"
|
||||||
let dateTime: Date
|
let dateTime: Date
|
||||||
let sport: Sport
|
let sport: Sport
|
||||||
let season: String
|
let season: String
|
||||||
@@ -17,10 +17,10 @@ struct Game: Identifiable, Codable, Hashable {
|
|||||||
let broadcastInfo: String?
|
let broadcastInfo: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: UUID ,
|
id: String,
|
||||||
homeTeamId: UUID,
|
homeTeamId: String,
|
||||||
awayTeamId: UUID,
|
awayTeamId: String,
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport,
|
sport: Sport,
|
||||||
season: String,
|
season: String,
|
||||||
@@ -78,7 +78,7 @@ struct RichGame: Identifiable, Hashable, Codable {
|
|||||||
let awayTeam: Team
|
let awayTeam: Team
|
||||||
let stadium: Stadium
|
let stadium: Stadium
|
||||||
|
|
||||||
var id: UUID { game.id }
|
var id: String { game.id }
|
||||||
|
|
||||||
var matchupDescription: String {
|
var matchupDescription: String {
|
||||||
"\(awayTeam.abbreviation) @ \(homeTeam.abbreviation)"
|
"\(awayTeam.abbreviation) @ \(homeTeam.abbreviation)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Foundation
|
|||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
|
||||||
struct Stadium: Identifiable, Codable, Hashable {
|
struct Stadium: Identifiable, Codable, Hashable {
|
||||||
let id: UUID
|
let id: String // Canonical ID: "stadium_mlb_fenway_park"
|
||||||
let name: String
|
let name: String
|
||||||
let city: String
|
let city: String
|
||||||
let state: String
|
let state: String
|
||||||
@@ -19,7 +19,7 @@ struct Stadium: Identifiable, Codable, Hashable {
|
|||||||
let imageURL: URL?
|
let imageURL: URL?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: UUID,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
city: String,
|
city: String,
|
||||||
state: String,
|
state: String,
|
||||||
|
|||||||
@@ -6,23 +6,23 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Team: Identifiable, Codable, Hashable {
|
struct Team: Identifiable, Codable, Hashable {
|
||||||
let id: UUID
|
let id: String // Canonical ID: "team_mlb_bos"
|
||||||
let name: String
|
let name: String
|
||||||
let abbreviation: String
|
let abbreviation: String
|
||||||
let sport: Sport
|
let sport: Sport
|
||||||
let city: String
|
let city: String
|
||||||
let stadiumId: UUID
|
let stadiumId: String // FK: "stadium_mlb_fenway_park"
|
||||||
let logoURL: URL?
|
let logoURL: URL?
|
||||||
let primaryColor: String?
|
let primaryColor: String?
|
||||||
let secondaryColor: String?
|
let secondaryColor: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: UUID,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
abbreviation: String,
|
abbreviation: String,
|
||||||
sport: Sport,
|
sport: Sport,
|
||||||
city: String,
|
city: String,
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
logoURL: URL? = nil,
|
logoURL: URL? = nil,
|
||||||
primaryColor: String? = nil,
|
primaryColor: String? = nil,
|
||||||
secondaryColor: String? = nil
|
secondaryColor: String? = nil
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ struct ItineraryDay: Identifiable, Hashable {
|
|||||||
|
|
||||||
var isRestDay: Bool { stops.first?.isRestDay ?? false }
|
var isRestDay: Bool { stops.first?.isRestDay ?? false }
|
||||||
var hasTravelSegment: Bool { !travelSegments.isEmpty }
|
var hasTravelSegment: Bool { !travelSegments.isEmpty }
|
||||||
var gameIds: [UUID] { stops.flatMap { $0.games } }
|
var gameIds: [String] { stops.flatMap { $0.games } }
|
||||||
var hasGames: Bool { !gameIds.isEmpty }
|
var hasGames: Bool { !gameIds.isEmpty }
|
||||||
var primaryCity: String? { stops.first?.city }
|
var primaryCity: String? { stops.first?.city }
|
||||||
var totalDrivingHours: Double { travelSegments.reduce(0) { $0 + $1.durationHours } }
|
var totalDrivingHours: Double { travelSegments.reduce(0) { $0 + $1.durationHours } }
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ struct TripPreferences: Codable, Hashable {
|
|||||||
var startLocation: LocationInput?
|
var startLocation: LocationInput?
|
||||||
var endLocation: LocationInput?
|
var endLocation: LocationInput?
|
||||||
var sports: Set<Sport>
|
var sports: Set<Sport>
|
||||||
var mustSeeGameIds: Set<UUID>
|
var mustSeeGameIds: Set<String>
|
||||||
var travelMode: TravelMode
|
var travelMode: TravelMode
|
||||||
var startDate: Date
|
var startDate: Date
|
||||||
var endDate: Date
|
var endDate: Date
|
||||||
@@ -235,7 +235,7 @@ struct TripPreferences: Codable, Hashable {
|
|||||||
var selectedRegions: Set<Region>
|
var selectedRegions: Set<Region>
|
||||||
|
|
||||||
/// Team to follow (for Follow Team mode)
|
/// Team to follow (for Follow Team mode)
|
||||||
var followTeamId: UUID?
|
var followTeamId: String?
|
||||||
|
|
||||||
/// Whether to start/end from a home location (vs fly-in/fly-out)
|
/// Whether to start/end from a home location (vs fly-in/fly-out)
|
||||||
var useHomeLocation: Bool
|
var useHomeLocation: Bool
|
||||||
@@ -248,7 +248,7 @@ struct TripPreferences: Codable, Hashable {
|
|||||||
startLocation: LocationInput? = nil,
|
startLocation: LocationInput? = nil,
|
||||||
endLocation: LocationInput? = nil,
|
endLocation: LocationInput? = nil,
|
||||||
sports: Set<Sport> = [],
|
sports: Set<Sport> = [],
|
||||||
mustSeeGameIds: Set<UUID> = [],
|
mustSeeGameIds: Set<String> = [],
|
||||||
travelMode: TravelMode = .drive,
|
travelMode: TravelMode = .drive,
|
||||||
startDate: Date = Date(),
|
startDate: Date = Date(),
|
||||||
endDate: Date = Date().addingTimeInterval(86400 * 7),
|
endDate: Date = Date().addingTimeInterval(86400 * 7),
|
||||||
@@ -264,7 +264,7 @@ struct TripPreferences: Codable, Hashable {
|
|||||||
maxDrivingHoursPerDriver: Double? = nil,
|
maxDrivingHoursPerDriver: Double? = nil,
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
selectedRegions: Set<Region> = [.east, .central, .west],
|
selectedRegions: Set<Region> = [.east, .central, .west],
|
||||||
followTeamId: UUID? = nil,
|
followTeamId: String? = nil,
|
||||||
useHomeLocation: Bool = true,
|
useHomeLocation: Bool = true,
|
||||||
gameFirstTripDuration: Int = 7
|
gameFirstTripDuration: Int = 7
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ struct TripStop: Identifiable, Codable, Hashable {
|
|||||||
let coordinate: CLLocationCoordinate2D?
|
let coordinate: CLLocationCoordinate2D?
|
||||||
let arrivalDate: Date
|
let arrivalDate: Date
|
||||||
let departureDate: Date
|
let departureDate: Date
|
||||||
let games: [UUID]
|
let games: [String]
|
||||||
let stadium: UUID?
|
let stadium: String?
|
||||||
let lodging: LodgingSuggestion?
|
let lodging: LodgingSuggestion?
|
||||||
let activities: [ActivitySuggestion]
|
let activities: [ActivitySuggestion]
|
||||||
let isRestDay: Bool
|
let isRestDay: Bool
|
||||||
@@ -29,8 +29,8 @@ struct TripStop: Identifiable, Codable, Hashable {
|
|||||||
coordinate: CLLocationCoordinate2D? = nil,
|
coordinate: CLLocationCoordinate2D? = nil,
|
||||||
arrivalDate: Date,
|
arrivalDate: Date,
|
||||||
departureDate: Date,
|
departureDate: Date,
|
||||||
games: [UUID] = [],
|
games: [String] = [],
|
||||||
stadium: UUID? = nil,
|
stadium: String? = nil,
|
||||||
lodging: LodgingSuggestion? = nil,
|
lodging: LodgingSuggestion? = nil,
|
||||||
activities: [ActivitySuggestion] = [],
|
activities: [ActivitySuggestion] = [],
|
||||||
isRestDay: Bool = false,
|
isRestDay: Bool = false,
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ final class CanonicalStadium {
|
|||||||
|
|
||||||
func toDomain() -> Stadium {
|
func toDomain() -> Stadium {
|
||||||
Stadium(
|
Stadium(
|
||||||
id: uuid,
|
id: canonicalId,
|
||||||
name: name,
|
name: name,
|
||||||
city: city,
|
city: city,
|
||||||
state: state,
|
state: state,
|
||||||
@@ -299,14 +299,14 @@ final class CanonicalTeam {
|
|||||||
|
|
||||||
var sportEnum: Sport? { Sport(rawValue: sport) }
|
var sportEnum: Sport? { Sport(rawValue: sport) }
|
||||||
|
|
||||||
func toDomain(stadiumUUID: UUID) -> Team {
|
func toDomain() -> Team {
|
||||||
Team(
|
Team(
|
||||||
id: uuid,
|
id: canonicalId,
|
||||||
name: name,
|
name: name,
|
||||||
abbreviation: abbreviation,
|
abbreviation: abbreviation,
|
||||||
sport: sportEnum ?? .mlb,
|
sport: sportEnum ?? .mlb,
|
||||||
city: city,
|
city: city,
|
||||||
stadiumId: stadiumUUID,
|
stadiumId: stadiumCanonicalId,
|
||||||
logoURL: logoURL.flatMap { URL(string: $0) },
|
logoURL: logoURL.flatMap { URL(string: $0) },
|
||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
secondaryColor: secondaryColor
|
secondaryColor: secondaryColor
|
||||||
@@ -466,12 +466,12 @@ final class CanonicalGame {
|
|||||||
|
|
||||||
var sportEnum: Sport? { Sport(rawValue: sport) }
|
var sportEnum: Sport? { Sport(rawValue: sport) }
|
||||||
|
|
||||||
func toDomain(homeTeamUUID: UUID, awayTeamUUID: UUID, stadiumUUID: UUID) -> Game {
|
func toDomain() -> Game {
|
||||||
Game(
|
Game(
|
||||||
id: uuid,
|
id: canonicalId,
|
||||||
homeTeamId: homeTeamUUID,
|
homeTeamId: homeTeamCanonicalId,
|
||||||
awayTeamId: awayTeamUUID,
|
awayTeamId: awayTeamCanonicalId,
|
||||||
stadiumId: stadiumUUID,
|
stadiumId: stadiumCanonicalId,
|
||||||
dateTime: dateTime,
|
dateTime: dateTime,
|
||||||
sport: sportEnum ?? .mlb,
|
sport: sportEnum ?? .mlb,
|
||||||
season: season,
|
season: season,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ final class SavedTrip {
|
|||||||
var updatedAt: Date
|
var updatedAt: Date
|
||||||
var status: String
|
var status: String
|
||||||
var tripData: Data // Encoded Trip struct
|
var tripData: Data // Encoded Trip struct
|
||||||
var gamesData: Data? // Encoded [UUID: RichGame] dictionary
|
var gamesData: Data? // Encoded [String: RichGame] dictionary
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade)
|
@Relationship(deleteRule: .cascade)
|
||||||
var votes: [TripVote]?
|
var votes: [TripVote]?
|
||||||
@@ -43,16 +43,16 @@ final class SavedTrip {
|
|||||||
try? JSONDecoder().decode(Trip.self, from: tripData)
|
try? JSONDecoder().decode(Trip.self, from: tripData)
|
||||||
}
|
}
|
||||||
|
|
||||||
var games: [UUID: RichGame] {
|
var games: [String: RichGame] {
|
||||||
guard let data = gamesData else { return [:] }
|
guard let data = gamesData else { return [:] }
|
||||||
return (try? JSONDecoder().decode([UUID: RichGame].self, from: data)) ?? [:]
|
return (try? JSONDecoder().decode([String: RichGame].self, from: data)) ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
var tripStatus: TripStatus {
|
var tripStatus: TripStatus {
|
||||||
TripStatus(rawValue: status) ?? .draft
|
TripStatus(rawValue: status) ?? .draft
|
||||||
}
|
}
|
||||||
|
|
||||||
static func from(_ trip: Trip, games: [UUID: RichGame] = [:], status: TripStatus = .planned) -> SavedTrip? {
|
static func from(_ trip: Trip, games: [String: RichGame] = [:], status: TripStatus = .planned) -> SavedTrip? {
|
||||||
guard let tripData = try? JSONEncoder().encode(trip) else { return nil }
|
guard let tripData = try? JSONEncoder().encode(trip) else { return nil }
|
||||||
let gamesData = try? JSONEncoder().encode(games)
|
let gamesData = try? JSONEncoder().encode(games)
|
||||||
return SavedTrip(
|
return SavedTrip(
|
||||||
@@ -75,7 +75,7 @@ final class TripVote {
|
|||||||
var tripId: UUID
|
var tripId: UUID
|
||||||
var voterId: String
|
var voterId: String
|
||||||
var voterName: String
|
var voterName: String
|
||||||
var gameVotes: Data // [UUID: Bool] encoded
|
var gameVotes: Data // [String: Bool] encoded (game IDs to vote)
|
||||||
var routeVotes: Data // [String: Int] encoded
|
var routeVotes: Data // [String: Int] encoded
|
||||||
var leisurePreference: String
|
var leisurePreference: String
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ final class StadiumVisit {
|
|||||||
@Attribute(.unique) var id: UUID
|
@Attribute(.unique) var id: UUID
|
||||||
|
|
||||||
// Stadium identity (stable across renames)
|
// Stadium identity (stable across renames)
|
||||||
var canonicalStadiumId: String // Links to CanonicalStadium.canonicalId
|
var stadiumId: String // Canonical ID: "stadium_mlb_fenway_park"
|
||||||
var stadiumUUID: UUID // Runtime UUID for display lookups
|
|
||||||
var stadiumNameAtVisit: String // Frozen at visit time
|
var stadiumNameAtVisit: String // Frozen at visit time
|
||||||
|
|
||||||
// Visit details
|
// Visit details
|
||||||
@@ -75,9 +74,9 @@ final class StadiumVisit {
|
|||||||
var visitTypeRaw: String // VisitType.rawValue
|
var visitTypeRaw: String // VisitType.rawValue
|
||||||
|
|
||||||
// Game info (optional - nil for tours/other visits)
|
// Game info (optional - nil for tours/other visits)
|
||||||
var gameId: UUID?
|
var gameId: String? // Canonical ID: "game_mlb_2026_bos_nyy_0401"
|
||||||
var homeTeamId: UUID?
|
var homeTeamId: String? // Canonical ID: "team_mlb_bos"
|
||||||
var awayTeamId: UUID?
|
var awayTeamId: String? // Canonical ID: "team_mlb_nyy"
|
||||||
var homeTeamName: String? // For display when team lookup fails
|
var homeTeamName: String? // For display when team lookup fails
|
||||||
var awayTeamName: String?
|
var awayTeamName: String?
|
||||||
var finalScore: String? // "5-3" format
|
var finalScore: String? // "5-3" format
|
||||||
@@ -109,15 +108,14 @@ final class StadiumVisit {
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
id: UUID = UUID(),
|
id: UUID = UUID(),
|
||||||
canonicalStadiumId: String,
|
stadiumId: String,
|
||||||
stadiumUUID: UUID,
|
|
||||||
stadiumNameAtVisit: String,
|
stadiumNameAtVisit: String,
|
||||||
visitDate: Date,
|
visitDate: Date,
|
||||||
sport: Sport,
|
sport: Sport,
|
||||||
visitType: VisitType = .game,
|
visitType: VisitType = .game,
|
||||||
gameId: UUID? = nil,
|
gameId: String? = nil,
|
||||||
homeTeamId: UUID? = nil,
|
homeTeamId: String? = nil,
|
||||||
awayTeamId: UUID? = nil,
|
awayTeamId: String? = nil,
|
||||||
homeTeamName: String? = nil,
|
homeTeamName: String? = nil,
|
||||||
awayTeamName: String? = nil,
|
awayTeamName: String? = nil,
|
||||||
finalScore: String? = nil,
|
finalScore: String? = nil,
|
||||||
@@ -133,8 +131,7 @@ final class StadiumVisit {
|
|||||||
source: VisitSource = .manual
|
source: VisitSource = .manual
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.canonicalStadiumId = canonicalStadiumId
|
self.stadiumId = stadiumId
|
||||||
self.stadiumUUID = stadiumUUID
|
|
||||||
self.stadiumNameAtVisit = stadiumNameAtVisit
|
self.stadiumNameAtVisit = stadiumNameAtVisit
|
||||||
self.visitDate = visitDate
|
self.visitDate = visitDate
|
||||||
self.sport = sport.rawValue
|
self.sport = sport.rawValue
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ final class AchievementEngine {
|
|||||||
func recalculateAllAchievements() async throws -> AchievementDelta {
|
func recalculateAllAchievements() async throws -> AchievementDelta {
|
||||||
// Get all visits
|
// Get all visits
|
||||||
let visits = try fetchAllVisits()
|
let visits = try fetchAllVisits()
|
||||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||||
|
|
||||||
// Get currently earned achievements
|
// Get currently earned achievements
|
||||||
let currentAchievements = try fetchEarnedAchievements()
|
let currentAchievements = try fetchEarnedAchievements()
|
||||||
@@ -112,7 +112,7 @@ final class AchievementEngine {
|
|||||||
/// Quick check after new visit (incremental)
|
/// Quick check after new visit (incremental)
|
||||||
func checkAchievementsForNewVisit(_ visit: StadiumVisit) async throws -> [AchievementDefinition] {
|
func checkAchievementsForNewVisit(_ visit: StadiumVisit) async throws -> [AchievementDefinition] {
|
||||||
let visits = try fetchAllVisits()
|
let visits = try fetchAllVisits()
|
||||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||||
|
|
||||||
let currentAchievements = try fetchEarnedAchievements()
|
let currentAchievements = try fetchEarnedAchievements()
|
||||||
let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId })
|
let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId })
|
||||||
@@ -152,7 +152,7 @@ final class AchievementEngine {
|
|||||||
/// Get progress toward all achievements
|
/// Get progress toward all achievements
|
||||||
func getProgress() async throws -> [AchievementProgress] {
|
func getProgress() async throws -> [AchievementProgress] {
|
||||||
let visits = try fetchAllVisits()
|
let visits = try fetchAllVisits()
|
||||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||||
let earnedAchievements = try fetchEarnedAchievements()
|
let earnedAchievements = try fetchEarnedAchievements()
|
||||||
let earnedIds = Set(earnedAchievements.map { $0.achievementTypeId })
|
let earnedIds = Set(earnedAchievements.map { $0.achievementTypeId })
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ final class AchievementEngine {
|
|||||||
|
|
||||||
case .visitCountForSport(let count, let sport):
|
case .visitCountForSport(let count, let sport):
|
||||||
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
||||||
let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId })
|
let sportStadiums = Set(sportVisits.map { $0.stadiumId })
|
||||||
return sportStadiums.count >= count
|
return sportStadiums.count >= count
|
||||||
|
|
||||||
case .completeDivision(let divisionId):
|
case .completeDivision(let divisionId):
|
||||||
@@ -214,41 +214,12 @@ final class AchievementEngine {
|
|||||||
case .multipleLeagues(let leagueCount):
|
case .multipleLeagues(let leagueCount):
|
||||||
return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount)
|
return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount)
|
||||||
|
|
||||||
case .specificStadium(let symbolicId):
|
case .specificStadium(let stadiumId):
|
||||||
// Resolve symbolic ID (e.g., "stadium_mlb_bos") to actual UUID string
|
// Direct comparison - canonical IDs match everywhere
|
||||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return false }
|
return visitedStadiumIds.contains(stadiumId)
|
||||||
return visitedStadiumIds.contains(resolvedId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves symbolic stadium IDs (e.g., "stadium_mlb_bos") to actual stadium UUID strings
|
|
||||||
private func resolveSymbolicStadiumId(_ symbolicId: String) -> String? {
|
|
||||||
// Parse symbolic ID format: "stadium_{sport}_{teamAbbrev}"
|
|
||||||
let parts = symbolicId.split(separator: "_")
|
|
||||||
guard parts.count == 3,
|
|
||||||
parts[0] == "stadium" else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sport raw values are uppercase (e.g., "MLB"), but symbolic IDs use lowercase
|
|
||||||
let sportString = String(parts[1]).uppercased()
|
|
||||||
guard let sport = Sport(rawValue: sportString) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let teamAbbrev = String(parts[2]).uppercased()
|
|
||||||
|
|
||||||
// Find team by abbreviation and sport
|
|
||||||
guard let team = dataProvider.teams.first(where: {
|
|
||||||
$0.abbreviation.uppercased() == teamAbbrev && $0.sport == sport
|
|
||||||
}) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the stadium UUID as string (matches visit's canonicalStadiumId format)
|
|
||||||
return team.stadiumId.uuidString
|
|
||||||
}
|
|
||||||
|
|
||||||
private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set<String>) -> Bool {
|
private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set<String>) -> Bool {
|
||||||
guard let division = LeagueStructure.division(byId: divisionId) else { return false }
|
guard let division = LeagueStructure.division(byId: divisionId) else { return false }
|
||||||
|
|
||||||
@@ -291,7 +262,7 @@ final class AchievementEngine {
|
|||||||
if daysDiff < withinDays {
|
if daysDiff < withinDays {
|
||||||
// Check unique stadiums in window
|
// Check unique stadiums in window
|
||||||
let windowVisits = Array(sortedVisits[i..<(i + requiredVisits)])
|
let windowVisits = Array(sortedVisits[i..<(i + requiredVisits)])
|
||||||
let uniqueStadiums = Set(windowVisits.map { $0.canonicalStadiumId })
|
let uniqueStadiums = Set(windowVisits.map { $0.stadiumId })
|
||||||
if uniqueStadiums.count >= requiredVisits {
|
if uniqueStadiums.count >= requiredVisits {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -322,7 +293,7 @@ final class AchievementEngine {
|
|||||||
|
|
||||||
case .visitCountForSport(let count, let sport):
|
case .visitCountForSport(let count, let sport):
|
||||||
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
||||||
let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId })
|
let sportStadiums = Set(sportVisits.map { $0.stadiumId })
|
||||||
return (sportStadiums.count, count)
|
return (sportStadiums.count, count)
|
||||||
|
|
||||||
case .completeDivision(let divisionId):
|
case .completeDivision(let divisionId):
|
||||||
@@ -348,10 +319,9 @@ final class AchievementEngine {
|
|||||||
let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
|
let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
|
||||||
return (leagues.count, leagueCount)
|
return (leagues.count, leagueCount)
|
||||||
|
|
||||||
case .specificStadium(let symbolicId):
|
case .specificStadium(let stadiumId):
|
||||||
// Resolve symbolic ID to actual UUID string
|
// Direct comparison - canonical IDs match everywhere
|
||||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return (0, 1) }
|
return (visitedStadiumIds.contains(stadiumId) ? 1 : 0, 1)
|
||||||
return (visitedStadiumIds.contains(resolvedId) ? 1 : 0, 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,15 +338,15 @@ final class AchievementEngine {
|
|||||||
|
|
||||||
case .completeDivision(let divisionId):
|
case .completeDivision(let divisionId):
|
||||||
let stadiumIds = Set(getStadiumIdsForDivision(divisionId))
|
let stadiumIds = Set(getStadiumIdsForDivision(divisionId))
|
||||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||||
|
|
||||||
case .completeConference(let conferenceId):
|
case .completeConference(let conferenceId):
|
||||||
let stadiumIds = Set(getStadiumIdsForConference(conferenceId))
|
let stadiumIds = Set(getStadiumIdsForConference(conferenceId))
|
||||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||||
|
|
||||||
case .completeLeague(let sport):
|
case .completeLeague(let sport):
|
||||||
let stadiumIds = Set(getStadiumIdsForLeague(sport))
|
let stadiumIds = Set(getStadiumIdsForLeague(sport))
|
||||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||||
|
|
||||||
case .visitsInDays(let requiredVisits, let days):
|
case .visitsInDays(let requiredVisits, let days):
|
||||||
// Find the qualifying window of visits
|
// Find the qualifying window of visits
|
||||||
@@ -391,10 +361,9 @@ final class AchievementEngine {
|
|||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
|
|
||||||
case .specificStadium(let symbolicId):
|
case .specificStadium(let stadiumId):
|
||||||
// Resolve symbolic ID to actual UUID string
|
// Direct comparison - canonical IDs match everywhere
|
||||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return [] }
|
return visits.filter { $0.stadiumId == stadiumId }.map { $0.id }
|
||||||
return visits.filter { $0.canonicalStadiumId == resolvedId }.map { $0.id }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,17 +379,8 @@ final class AchievementEngine {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stadium UUIDs for these teams
|
// Get canonical stadium IDs for these teams
|
||||||
// CanonicalTeam has stadiumCanonicalId, we need to find the corresponding Stadium UUID
|
return canonicalTeams.map { $0.stadiumCanonicalId }
|
||||||
var stadiumIds: [String] = []
|
|
||||||
for canonicalTeam in canonicalTeams {
|
|
||||||
// Find the domain team by matching name/abbreviation to get stadium UUID
|
|
||||||
if let team = dataProvider.teams.first(where: { $0.abbreviation == canonicalTeam.abbreviation && $0.sport.rawValue == canonicalTeam.sport }) {
|
|
||||||
stadiumIds.append(team.stadiumId.uuidString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stadiumIds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getStadiumIdsForConference(_ conferenceId: String) -> [String] {
|
private func getStadiumIdsForConference(_ conferenceId: String) -> [String] {
|
||||||
@@ -434,7 +394,7 @@ final class AchievementEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func getStadiumIdsForLeague(_ sport: Sport) -> [String] {
|
private func getStadiumIdsForLeague(_ sport: Sport) -> [String] {
|
||||||
// Get all stadiums for this sport - return UUID strings to match visit format
|
// Get all stadium canonical IDs for this sport
|
||||||
return dataProvider.stadiums
|
return dataProvider.stadiums
|
||||||
.filter { stadium in
|
.filter { stadium in
|
||||||
// Check if stadium hosts teams of this sport
|
// Check if stadium hosts teams of this sport
|
||||||
@@ -442,7 +402,7 @@ final class AchievementEngine {
|
|||||||
team.stadiumId == stadium.id && team.sport == sport
|
team.stadiumId == stadium.id && team.sport == sport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { $0.id.uuidString }
|
.map { $0.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Data Fetching
|
// MARK: - Data Fetching
|
||||||
|
|||||||
@@ -165,18 +165,18 @@ actor CloudKitService {
|
|||||||
let ckGame = CKGame(record: record)
|
let ckGame = CKGame(record: record)
|
||||||
|
|
||||||
guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
|
guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
|
||||||
let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference,
|
let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference
|
||||||
let homeId = UUID(uuidString: homeRef.recordID.recordName),
|
|
||||||
let awayId = UUID(uuidString: awayRef.recordID.recordName)
|
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
let homeId = homeRef.recordID.recordName
|
||||||
|
let awayId = awayRef.recordID.recordName
|
||||||
|
|
||||||
// Stadium ref is optional - use placeholder if not present
|
// Stadium ref is optional - use placeholder if not present
|
||||||
let stadiumId: UUID
|
let stadiumId: String
|
||||||
if let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference,
|
if let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference {
|
||||||
let refId = UUID(uuidString: stadiumRef.recordID.recordName) {
|
stadiumId = stadiumRef.recordID.recordName
|
||||||
stadiumId = refId
|
|
||||||
} else {
|
} else {
|
||||||
stadiumId = UUID() // Placeholder - will be resolved via team lookup
|
stadiumId = "stadium_placeholder_\(UUID().uuidString)" // Placeholder - will be resolved via team lookup
|
||||||
}
|
}
|
||||||
|
|
||||||
return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
|
return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
|
||||||
@@ -188,8 +188,8 @@ actor CloudKitService {
|
|||||||
return allGames.sorted { $0.dateTime < $1.dateTime }
|
return allGames.sorted { $0.dateTime < $1.dateTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchGame(by id: UUID) async throws -> Game? {
|
func fetchGame(by id: String) async throws -> Game? {
|
||||||
let predicate = NSPredicate(format: "gameId == %@", id.uuidString)
|
let predicate = NSPredicate(format: "gameId == %@", id)
|
||||||
let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)
|
let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)
|
||||||
|
|
||||||
let (results, _) = try await publicDatabase.records(matching: query)
|
let (results, _) = try await publicDatabase.records(matching: query)
|
||||||
@@ -201,12 +201,13 @@ actor CloudKitService {
|
|||||||
|
|
||||||
guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
|
guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference,
|
||||||
let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference,
|
let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference,
|
||||||
let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference,
|
let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference
|
||||||
let homeId = UUID(uuidString: homeRef.recordID.recordName),
|
|
||||||
let awayId = UUID(uuidString: awayRef.recordID.recordName),
|
|
||||||
let stadiumId = UUID(uuidString: stadiumRef.recordID.recordName)
|
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
let homeId = homeRef.recordID.recordName
|
||||||
|
let awayId = awayRef.recordID.recordName
|
||||||
|
let stadiumId = stadiumRef.recordID.recordName
|
||||||
|
|
||||||
return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
|
return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,13 +278,11 @@ actor CloudKitService {
|
|||||||
let stadiumCanonicalId = ckGame.stadiumCanonicalId
|
let stadiumCanonicalId = ckGame.stadiumCanonicalId
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
// For the Game domain object, we still need UUIDs - use placeholder
|
// For the Game domain object, use canonical IDs directly
|
||||||
// The sync service will use canonical IDs for relationships
|
|
||||||
let placeholderUUID = UUID()
|
|
||||||
guard let game = ckGame.game(
|
guard let game = ckGame.game(
|
||||||
homeTeamId: placeholderUUID,
|
homeTeamId: homeTeamCanonicalId,
|
||||||
awayTeamId: placeholderUUID,
|
awayTeamId: awayTeamCanonicalId,
|
||||||
stadiumId: placeholderUUID
|
stadiumId: stadiumCanonicalId
|
||||||
) else { return nil }
|
) else { return nil }
|
||||||
|
|
||||||
return SyncGame(
|
return SyncGame(
|
||||||
|
|||||||
@@ -21,14 +21,9 @@ final class AppDataProvider: ObservableObject {
|
|||||||
@Published private(set) var error: Error?
|
@Published private(set) var error: Error?
|
||||||
@Published private(set) var errorMessage: String?
|
@Published private(set) var errorMessage: String?
|
||||||
|
|
||||||
private var teamsById: [UUID: Team] = [:]
|
// Lookup dictionaries - keyed by canonical ID (String)
|
||||||
private var stadiumsById: [UUID: Stadium] = [:]
|
private var teamsById: [String: Team] = [:]
|
||||||
private var stadiumsByCanonicalId: [String: Stadium] = [:]
|
private var stadiumsById: [String: Stadium] = [:]
|
||||||
private var teamsByCanonicalId: [String: Team] = [:]
|
|
||||||
|
|
||||||
// Canonical ID lookups for game conversion
|
|
||||||
private var canonicalTeamUUIDs: [String: UUID] = [:]
|
|
||||||
private var canonicalStadiumUUIDs: [String: UUID] = [:]
|
|
||||||
|
|
||||||
private var modelContext: ModelContext?
|
private var modelContext: ModelContext?
|
||||||
|
|
||||||
@@ -63,11 +58,11 @@ final class AppDataProvider: ObservableObject {
|
|||||||
|
|
||||||
// Convert to domain models and build lookups
|
// Convert to domain models and build lookups
|
||||||
var loadedStadiums: [Stadium] = []
|
var loadedStadiums: [Stadium] = []
|
||||||
|
var stadiumLookup: [String: Stadium] = [:]
|
||||||
for canonical in canonicalStadiums {
|
for canonical in canonicalStadiums {
|
||||||
let stadium = canonical.toDomain()
|
let stadium = canonical.toDomain()
|
||||||
loadedStadiums.append(stadium)
|
loadedStadiums.append(stadium)
|
||||||
stadiumsByCanonicalId[canonical.canonicalId] = stadium
|
stadiumLookup[stadium.id] = stadium
|
||||||
canonicalStadiumUUIDs[canonical.canonicalId] = stadium.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch canonical teams from SwiftData
|
// Fetch canonical teams from SwiftData
|
||||||
@@ -78,31 +73,17 @@ final class AppDataProvider: ObservableObject {
|
|||||||
|
|
||||||
// Convert to domain models
|
// Convert to domain models
|
||||||
var loadedTeams: [Team] = []
|
var loadedTeams: [Team] = []
|
||||||
|
var teamLookup: [String: Team] = [:]
|
||||||
for canonical in canonicalTeams {
|
for canonical in canonicalTeams {
|
||||||
// Get stadium UUID for this team
|
let team = canonical.toDomain()
|
||||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
|
||||||
let team = canonical.toDomain(stadiumUUID: stadiumUUID)
|
|
||||||
loadedTeams.append(team)
|
loadedTeams.append(team)
|
||||||
teamsByCanonicalId[canonical.canonicalId] = team
|
teamLookup[team.id] = team
|
||||||
canonicalTeamUUIDs[canonical.canonicalId] = team.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.teams = loadedTeams
|
self.teams = loadedTeams
|
||||||
self.stadiums = loadedStadiums
|
self.stadiums = loadedStadiums
|
||||||
|
self.teamsById = teamLookup
|
||||||
// Build lookup dictionaries (use reduce to handle potential duplicates gracefully)
|
self.stadiumsById = stadiumLookup
|
||||||
self.teamsById = loadedTeams.reduce(into: [:]) { dict, team in
|
|
||||||
if dict[team.id] != nil {
|
|
||||||
print("⚠️ Duplicate team UUID: \(team.id) - \(team.name)")
|
|
||||||
}
|
|
||||||
dict[team.id] = team
|
|
||||||
}
|
|
||||||
self.stadiumsById = loadedStadiums.reduce(into: [:]) { dict, stadium in
|
|
||||||
if dict[stadium.id] != nil {
|
|
||||||
print("⚠️ Duplicate stadium UUID: \(stadium.id) - \(stadium.name)")
|
|
||||||
}
|
|
||||||
dict[stadium.id] = stadium
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error
|
self.error = error
|
||||||
@@ -123,11 +104,11 @@ final class AppDataProvider: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Data Access
|
// MARK: - Data Access
|
||||||
|
|
||||||
func team(for id: UUID) -> Team? {
|
func team(for id: String) -> Team? {
|
||||||
teamsById[id]
|
teamsById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
func stadium(for id: UUID) -> Stadium? {
|
func stadium(for id: String) -> Stadium? {
|
||||||
stadiumsById[id]
|
stadiumsById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,47 +137,27 @@ final class AppDataProvider: ObservableObject {
|
|||||||
let canonicalGames = try context.fetch(descriptor)
|
let canonicalGames = try context.fetch(descriptor)
|
||||||
|
|
||||||
// Filter by sport and convert to domain models
|
// Filter by sport and convert to domain models
|
||||||
let result = canonicalGames.compactMap { canonical -> Game? in
|
return canonicalGames.compactMap { canonical -> Game? in
|
||||||
guard sportStrings.contains(canonical.sport) else { return nil }
|
guard sportStrings.contains(canonical.sport) else { return nil }
|
||||||
|
return canonical.toDomain()
|
||||||
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
|
}
|
||||||
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
|
|
||||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
|
||||||
|
|
||||||
return canonical.toDomain(
|
|
||||||
homeTeamUUID: homeTeamUUID,
|
|
||||||
awayTeamUUID: awayTeamUUID,
|
|
||||||
stadiumUUID: stadiumUUID
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
/// Fetch a single game by canonical ID
|
||||||
}
|
func fetchGame(by id: String) async throws -> Game? {
|
||||||
|
|
||||||
/// Fetch a single game by ID
|
|
||||||
func fetchGame(by id: UUID) async throws -> Game? {
|
|
||||||
guard let context = modelContext else {
|
guard let context = modelContext else {
|
||||||
throw DataProviderError.contextNotConfigured
|
throw DataProviderError.contextNotConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
let idString = id.uuidString
|
|
||||||
let descriptor = FetchDescriptor<CanonicalGame>(
|
let descriptor = FetchDescriptor<CanonicalGame>(
|
||||||
predicate: #Predicate<CanonicalGame> { $0.canonicalId == idString }
|
predicate: #Predicate<CanonicalGame> { $0.canonicalId == id }
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let canonical = try context.fetch(descriptor).first else {
|
guard let canonical = try context.fetch(descriptor).first else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
|
return canonical.toDomain()
|
||||||
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
|
|
||||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
|
||||||
|
|
||||||
return canonical.toDomain(
|
|
||||||
homeTeamUUID: homeTeamUUID,
|
|
||||||
awayTeamUUID: awayTeamUUID,
|
|
||||||
stadiumUUID: stadiumUUID
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch games with full team and stadium data
|
/// Fetch games with full team and stadium data
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ enum NoMatchReason: Sendable {
|
|||||||
// MARK: - Game Match Result
|
// MARK: - Game Match Result
|
||||||
|
|
||||||
struct GameMatchCandidate: Identifiable, Sendable {
|
struct GameMatchCandidate: Identifiable, Sendable {
|
||||||
let id: UUID
|
let id: String
|
||||||
let game: Game
|
let game: Game
|
||||||
let stadium: Stadium
|
let stadium: Stadium
|
||||||
let homeTeam: Team
|
let homeTeam: Team
|
||||||
@@ -67,14 +67,20 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
|||||||
|
|
||||||
/// Initialize from a scraped historical game
|
/// Initialize from a scraped historical game
|
||||||
init(scrapedGame: ScrapedGame, stadium: Stadium) {
|
init(scrapedGame: ScrapedGame, stadium: Stadium) {
|
||||||
self.id = UUID()
|
let matchId = UUID()
|
||||||
|
self.id = "scraped_match_\(matchId.uuidString)"
|
||||||
self.stadium = stadium
|
self.stadium = stadium
|
||||||
|
|
||||||
|
// Generate synthetic IDs for scraped games
|
||||||
|
let syntheticHomeTeamId = "scraped_team_\(UUID().uuidString)"
|
||||||
|
let syntheticAwayTeamId = "scraped_team_\(UUID().uuidString)"
|
||||||
|
let syntheticGameId = "scraped_game_\(matchId.uuidString)"
|
||||||
|
|
||||||
// Create synthetic Team objects from scraped names
|
// Create synthetic Team objects from scraped names
|
||||||
// Scraped names already include city (e.g., "Chicago Cubs"), so we use empty city
|
// Scraped names already include city (e.g., "Chicago Cubs"), so we use empty city
|
||||||
// to avoid duplication in fullName computed property
|
// to avoid duplication in fullName computed property
|
||||||
self.homeTeam = Team(
|
self.homeTeam = Team(
|
||||||
id: UUID(),
|
id: syntheticHomeTeamId,
|
||||||
name: scrapedGame.homeTeam,
|
name: scrapedGame.homeTeam,
|
||||||
abbreviation: String(scrapedGame.homeTeam.suffix(3)).uppercased(),
|
abbreviation: String(scrapedGame.homeTeam.suffix(3)).uppercased(),
|
||||||
sport: scrapedGame.sport,
|
sport: scrapedGame.sport,
|
||||||
@@ -83,7 +89,7 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.awayTeam = Team(
|
self.awayTeam = Team(
|
||||||
id: UUID(),
|
id: syntheticAwayTeamId,
|
||||||
name: scrapedGame.awayTeam,
|
name: scrapedGame.awayTeam,
|
||||||
abbreviation: String(scrapedGame.awayTeam.suffix(3)).uppercased(),
|
abbreviation: String(scrapedGame.awayTeam.suffix(3)).uppercased(),
|
||||||
sport: scrapedGame.sport,
|
sport: scrapedGame.sport,
|
||||||
@@ -94,7 +100,7 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
|||||||
// Create synthetic Game object
|
// Create synthetic Game object
|
||||||
let year = Calendar.current.component(.year, from: scrapedGame.date)
|
let year = Calendar.current.component(.year, from: scrapedGame.date)
|
||||||
self.game = Game(
|
self.game = Game(
|
||||||
id: self.id,
|
id: syntheticGameId,
|
||||||
homeTeamId: self.homeTeam.id,
|
homeTeamId: self.homeTeam.id,
|
||||||
awayTeamId: self.awayTeam.id,
|
awayTeamId: self.awayTeam.id,
|
||||||
stadiumId: stadium.id,
|
stadiumId: stadium.id,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ struct RouteDescriptionInput: Identifiable {
|
|||||||
let totalMiles: Double
|
let totalMiles: Double
|
||||||
let totalDrivingHours: Double
|
let totalDrivingHours: Double
|
||||||
|
|
||||||
init(from option: ItineraryOption, games: [UUID: RichGame]) {
|
init(from option: ItineraryOption, games: [String: RichGame]) {
|
||||||
self.id = option.id
|
self.id = option.id
|
||||||
self.cities = Array(NSOrderedSet(array: option.stops.map { $0.city })) as? [String] ?? []
|
self.cities = Array(NSOrderedSet(array: option.stops.map { $0.city })) as? [String] ?? []
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ extension MatchConfidence: Comparable {
|
|||||||
// MARK: - Stadium Match
|
// MARK: - Stadium Match
|
||||||
|
|
||||||
struct StadiumMatch: Identifiable, Sendable {
|
struct StadiumMatch: Identifiable, Sendable {
|
||||||
let id: UUID
|
let id: String
|
||||||
let stadium: Stadium
|
let stadium: Stadium
|
||||||
let distance: CLLocationDistance
|
let distance: CLLocationDistance
|
||||||
let confidence: MatchConfidence
|
let confidence: MatchConfidence
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct SuggestedTrip: Identifiable {
|
|||||||
let region: Region
|
let region: Region
|
||||||
let isSingleSport: Bool
|
let isSingleSport: Bool
|
||||||
let trip: Trip
|
let trip: Trip
|
||||||
let richGames: [UUID: RichGame]
|
let richGames: [String: RichGame]
|
||||||
let sports: Set<Sport>
|
let sports: Set<Sport>
|
||||||
|
|
||||||
var displaySports: [Sport] {
|
var displaySports: [Sport] {
|
||||||
@@ -119,8 +119,8 @@ final class SuggestedTripsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build lookups (use reduce to handle potential duplicate UUIDs gracefully)
|
// Build lookups (use reduce to handle potential duplicate UUIDs gracefully)
|
||||||
let stadiumsById = dataProvider.stadiums.reduce(into: [UUID: Stadium]()) { $0[$1.id] = $1 }
|
let stadiumsById = dataProvider.stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 }
|
||||||
let teamsById = dataProvider.teams.reduce(into: [UUID: Team]()) { $0[$1.id] = $1 }
|
let teamsById = dataProvider.teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 }
|
||||||
|
|
||||||
var generatedTrips: [SuggestedTrip] = []
|
var generatedTrips: [SuggestedTrip] = []
|
||||||
|
|
||||||
@@ -208,8 +208,8 @@ final class SuggestedTripsGenerator {
|
|||||||
games: [Game],
|
games: [Game],
|
||||||
region: Region,
|
region: Region,
|
||||||
singleSport: Bool,
|
singleSport: Bool,
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team],
|
teams: [String: Team],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
excludingSport: Sport? = nil
|
excludingSport: Sport? = nil
|
||||||
@@ -292,8 +292,8 @@ final class SuggestedTripsGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildRichGames(from games: [Game], teams: [UUID: Team], stadiums: [UUID: Stadium]) -> [UUID: RichGame] {
|
private func buildRichGames(from games: [Game], teams: [String: Team], stadiums: [String: Stadium]) -> [String: RichGame] {
|
||||||
var result: [UUID: RichGame] = [:]
|
var result: [String: RichGame] = [:]
|
||||||
for game in games {
|
for game in games {
|
||||||
guard let homeTeam = teams[game.homeTeamId],
|
guard let homeTeam = teams[game.homeTeamId],
|
||||||
let awayTeam = teams[game.awayTeamId],
|
let awayTeam = teams[game.awayTeamId],
|
||||||
@@ -305,8 +305,8 @@ final class SuggestedTripsGenerator {
|
|||||||
|
|
||||||
private func generateCrossCountryTrip(
|
private func generateCrossCountryTrip(
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team],
|
teams: [String: Team],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
excludeGames: [Game]
|
excludeGames: [Game]
|
||||||
@@ -392,7 +392,7 @@ final class SuggestedTripsGenerator {
|
|||||||
|
|
||||||
// Build stops by grouping consecutive games at the same stadium
|
// Build stops by grouping consecutive games at the same stadium
|
||||||
var tripStops: [TripStop] = []
|
var tripStops: [TripStop] = []
|
||||||
var currentStadiumId: UUID? = nil
|
var currentStadiumId: String? = nil
|
||||||
var currentGames: [Game] = []
|
var currentGames: [Game] = []
|
||||||
|
|
||||||
for game in selectedGames {
|
for game in selectedGames {
|
||||||
@@ -463,7 +463,7 @@ final class SuggestedTripsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a TripStop from a group of games at the same stadium
|
/// Builds a TripStop from a group of games at the same stadium
|
||||||
private func buildTripStop(from games: [Game], stadiumId: UUID, stadiums: [UUID: Stadium], stopNumber: Int) -> TripStop? {
|
private func buildTripStop(from games: [Game], stadiumId: String, stadiums: [String: Stadium], stopNumber: Int) -> TripStop? {
|
||||||
guard !games.isEmpty else { return nil }
|
guard !games.isEmpty else { return nil }
|
||||||
|
|
||||||
let sortedGames = games.sorted { $0.dateTime < $1.dateTime }
|
let sortedGames = games.sorted { $0.dateTime < $1.dateTime }
|
||||||
@@ -488,7 +488,7 @@ final class SuggestedTripsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds travel segments between consecutive stops using TravelEstimator
|
/// Builds travel segments between consecutive stops using TravelEstimator
|
||||||
private func buildTravelSegments(from stops: [TripStop], stadiums: [UUID: Stadium]) -> [TravelSegment] {
|
private func buildTravelSegments(from stops: [TripStop], stadiums: [String: Stadium]) -> [TravelSegment] {
|
||||||
guard stops.count >= 2 else { return [] }
|
guard stops.count >= 2 else { return [] }
|
||||||
|
|
||||||
var segments: [TravelSegment] = []
|
var segments: [TravelSegment] = []
|
||||||
@@ -560,7 +560,7 @@ final class SuggestedTripsGenerator {
|
|||||||
/// Builds a trip following a geographic corridor (moving consistently east or west)
|
/// Builds a trip following a geographic corridor (moving consistently east or west)
|
||||||
private func buildCorridorTrip(
|
private func buildCorridorTrip(
|
||||||
games: [(game: Game, lon: Double)],
|
games: [(game: Game, lon: Double)],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
calendar: Calendar
|
calendar: Calendar
|
||||||
) -> [Game] {
|
) -> [Game] {
|
||||||
@@ -631,7 +631,7 @@ final class SuggestedTripsGenerator {
|
|||||||
startGames: [Game],
|
startGames: [Game],
|
||||||
middleGames: [Game],
|
middleGames: [Game],
|
||||||
endGames: [Game],
|
endGames: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
calendar: Calendar
|
calendar: Calendar
|
||||||
) -> [Game] {
|
) -> [Game] {
|
||||||
// Sort all games by date
|
// Sort all games by date
|
||||||
@@ -668,7 +668,7 @@ final class SuggestedTripsGenerator {
|
|||||||
var totalDistance: Double = 0
|
var totalDistance: Double = 0
|
||||||
|
|
||||||
// Track all days with games and their stadiums for conflict detection
|
// Track all days with games and their stadiums for conflict detection
|
||||||
var gamesByDay: [Date: UUID] = [:]
|
var gamesByDay: [Date: String] = [:]
|
||||||
gamesByDay[calendar.startOfDay(for: startGame.dateTime)] = startGame.stadiumId
|
gamesByDay[calendar.startOfDay(for: startGame.dateTime)] = startGame.stadiumId
|
||||||
|
|
||||||
// Find middle games that fit between start and end (limit to 2-3 middle stops)
|
// Find middle games that fit between start and end (limit to 2-3 middle stops)
|
||||||
@@ -774,7 +774,7 @@ final class SuggestedTripsGenerator {
|
|||||||
|
|
||||||
/// Validates that no two games in the route are on the same calendar day at different stadiums
|
/// Validates that no two games in the route are on the same calendar day at different stadiums
|
||||||
private func validateNoSameDayConflicts(_ games: [Game], calendar: Calendar) -> Bool {
|
private func validateNoSameDayConflicts(_ games: [Game], calendar: Calendar) -> Bool {
|
||||||
var gamesByDay: [Date: UUID] = [:]
|
var gamesByDay: [Date: String] = [:]
|
||||||
for game in games {
|
for game in games {
|
||||||
let day = calendar.startOfDay(for: game.dateTime)
|
let day = calendar.startOfDay(for: game.dateTime)
|
||||||
if let existingStadiumId = gamesByDay[day] {
|
if let existingStadiumId = gamesByDay[day] {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ final class PDFGenerator {
|
|||||||
|
|
||||||
func generatePDF(
|
func generatePDF(
|
||||||
for trip: Trip,
|
for trip: Trip,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
assets: PDFAssetPrefetcher.PrefetchedAssets? = nil
|
assets: PDFAssetPrefetcher.PrefetchedAssets? = nil
|
||||||
) async throws -> Data {
|
) async throws -> Data {
|
||||||
let pdfRenderer = UIGraphicsPDFRenderer(
|
let pdfRenderer = UIGraphicsPDFRenderer(
|
||||||
@@ -244,7 +244,7 @@ final class PDFGenerator {
|
|||||||
private func drawItineraryPages(
|
private func drawItineraryPages(
|
||||||
context: UIGraphicsPDFRendererContext,
|
context: UIGraphicsPDFRendererContext,
|
||||||
trip: Trip,
|
trip: Trip,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
assets: PDFAssetPrefetcher.PrefetchedAssets?
|
assets: PDFAssetPrefetcher.PrefetchedAssets?
|
||||||
) {
|
) {
|
||||||
var pageNumber = 3
|
var pageNumber = 3
|
||||||
@@ -283,7 +283,7 @@ final class PDFGenerator {
|
|||||||
drawFooter(context: context, pageNumber: pageNumber)
|
drawFooter(context: context, pageNumber: pageNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func estimateDayHeight(day: ItineraryDay, games: [UUID: RichGame]) -> CGFloat {
|
private func estimateDayHeight(day: ItineraryDay, games: [String: RichGame]) -> CGFloat {
|
||||||
var height: CGFloat = 60 // Day header + city
|
var height: CGFloat = 60 // Day header + city
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
@@ -305,7 +305,7 @@ final class PDFGenerator {
|
|||||||
private func drawDay(
|
private func drawDay(
|
||||||
context: UIGraphicsPDFRendererContext,
|
context: UIGraphicsPDFRendererContext,
|
||||||
day: ItineraryDay,
|
day: ItineraryDay,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
assets: PDFAssetPrefetcher.PrefetchedAssets?,
|
assets: PDFAssetPrefetcher.PrefetchedAssets?,
|
||||||
y: CGFloat
|
y: CGFloat
|
||||||
) -> CGFloat {
|
) -> CGFloat {
|
||||||
@@ -520,7 +520,7 @@ final class PDFGenerator {
|
|||||||
private func drawCitySpotlightPages(
|
private func drawCitySpotlightPages(
|
||||||
context: UIGraphicsPDFRendererContext,
|
context: UIGraphicsPDFRendererContext,
|
||||||
trip: Trip,
|
trip: Trip,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
assets: PDFAssetPrefetcher.PrefetchedAssets?
|
assets: PDFAssetPrefetcher.PrefetchedAssets?
|
||||||
) {
|
) {
|
||||||
guard let cityPOIs = assets?.cityPOIs, !cityPOIs.isEmpty else { return }
|
guard let cityPOIs = assets?.cityPOIs, !cityPOIs.isEmpty else { return }
|
||||||
@@ -661,7 +661,7 @@ final class PDFGenerator {
|
|||||||
private func drawSummaryPage(
|
private func drawSummaryPage(
|
||||||
context: UIGraphicsPDFRendererContext,
|
context: UIGraphicsPDFRendererContext,
|
||||||
trip: Trip,
|
trip: Trip,
|
||||||
games: [UUID: RichGame]
|
games: [String: RichGame]
|
||||||
) {
|
) {
|
||||||
var y: CGFloat = margin
|
var y: CGFloat = margin
|
||||||
|
|
||||||
@@ -896,7 +896,7 @@ final class ExportService {
|
|||||||
/// Export trip to PDF with full prefetched assets
|
/// Export trip to PDF with full prefetched assets
|
||||||
func exportToPDF(
|
func exportToPDF(
|
||||||
trip: Trip,
|
trip: Trip,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
progressCallback: ((PDFAssetPrefetcher.PrefetchProgress) async -> Void)? = nil
|
progressCallback: ((PDFAssetPrefetcher.PrefetchProgress) async -> Void)? = nil
|
||||||
) async throws -> URL {
|
) async throws -> URL {
|
||||||
// Prefetch all assets
|
// Prefetch all assets
|
||||||
@@ -918,7 +918,7 @@ final class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Quick export without prefetching (basic PDF)
|
/// Quick export without prefetching (basic PDF)
|
||||||
func exportToPDFBasic(trip: Trip, games: [UUID: RichGame]) async throws -> URL {
|
func exportToPDFBasic(trip: Trip, games: [String: RichGame]) async throws -> URL {
|
||||||
let data = try await pdfGenerator.generatePDF(for: trip, games: games, assets: nil)
|
let data = try await pdfGenerator.generatePDF(for: trip, games: games, assets: nil)
|
||||||
|
|
||||||
let fileName = "\(trip.name.replacingOccurrences(of: " ", with: "_"))_\(Date().timeIntervalSince1970).pdf"
|
let fileName = "\(trip.name.replacingOccurrences(of: " ", with: "_"))_\(Date().timeIntervalSince1970).pdf"
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ actor PDFAssetPrefetcher {
|
|||||||
struct PrefetchedAssets {
|
struct PrefetchedAssets {
|
||||||
let routeMap: UIImage?
|
let routeMap: UIImage?
|
||||||
let cityMaps: [String: UIImage]
|
let cityMaps: [String: UIImage]
|
||||||
let teamLogos: [UUID: UIImage]
|
let teamLogos: [String: UIImage]
|
||||||
let stadiumPhotos: [UUID: UIImage]
|
let stadiumPhotos: [String: UIImage]
|
||||||
let cityPOIs: [String: [POISearchService.POI]]
|
let cityPOIs: [String: [POISearchService.POI]]
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
@@ -63,7 +63,7 @@ actor PDFAssetPrefetcher {
|
|||||||
/// - Returns: All prefetched assets
|
/// - Returns: All prefetched assets
|
||||||
func prefetchAssets(
|
func prefetchAssets(
|
||||||
for trip: Trip,
|
for trip: Trip,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
progressCallback: ((PrefetchProgress) async -> Void)? = nil
|
progressCallback: ((PrefetchProgress) async -> Void)? = nil
|
||||||
) async -> PrefetchedAssets {
|
) async -> PrefetchedAssets {
|
||||||
var progress = PrefetchProgress()
|
var progress = PrefetchProgress()
|
||||||
@@ -71,8 +71,8 @@ actor PDFAssetPrefetcher {
|
|||||||
// Collect unique teams and stadiums from games
|
// Collect unique teams and stadiums from games
|
||||||
var teams: [Team] = []
|
var teams: [Team] = []
|
||||||
var stadiums: [Stadium] = []
|
var stadiums: [Stadium] = []
|
||||||
var seenTeamIds: Set<UUID> = []
|
var seenTeamIds: Set<String> = []
|
||||||
var seenStadiumIds: Set<UUID> = []
|
var seenStadiumIds: Set<String> = []
|
||||||
|
|
||||||
for (_, richGame) in games {
|
for (_, richGame) in games {
|
||||||
if !seenTeamIds.contains(richGame.homeTeam.id) {
|
if !seenTeamIds.contains(richGame.homeTeam.id) {
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ actor RemoteImageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch team logos by team ID
|
/// Fetch team logos by team ID
|
||||||
func fetchTeamLogos(teams: [Team]) async -> [UUID: UIImage] {
|
func fetchTeamLogos(teams: [Team]) async -> [String: UIImage] {
|
||||||
let urlToTeam: [URL: UUID] = Dictionary(
|
let urlToTeam: [URL: String] = Dictionary(
|
||||||
uniqueKeysWithValues: teams.compactMap { team in
|
uniqueKeysWithValues: teams.compactMap { team in
|
||||||
guard let logoURL = team.logoURL else { return nil }
|
guard let logoURL = team.logoURL else { return nil }
|
||||||
return (logoURL, team.id)
|
return (logoURL, team.id)
|
||||||
@@ -121,7 +121,7 @@ actor RemoteImageService {
|
|||||||
|
|
||||||
let images = await fetchImages(from: Array(urlToTeam.keys))
|
let images = await fetchImages(from: Array(urlToTeam.keys))
|
||||||
|
|
||||||
var result: [UUID: UIImage] = [:]
|
var result: [String: UIImage] = [:]
|
||||||
for (url, image) in images {
|
for (url, image) in images {
|
||||||
if let teamId = urlToTeam[url] {
|
if let teamId = urlToTeam[url] {
|
||||||
result[teamId] = image
|
result[teamId] = image
|
||||||
@@ -132,8 +132,8 @@ actor RemoteImageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch stadium photos by stadium ID
|
/// Fetch stadium photos by stadium ID
|
||||||
func fetchStadiumPhotos(stadiums: [Stadium]) async -> [UUID: UIImage] {
|
func fetchStadiumPhotos(stadiums: [Stadium]) async -> [String: UIImage] {
|
||||||
let urlToStadium: [URL: UUID] = Dictionary(
|
let urlToStadium: [URL: String] = Dictionary(
|
||||||
uniqueKeysWithValues: stadiums.compactMap { stadium in
|
uniqueKeysWithValues: stadiums.compactMap { stadium in
|
||||||
guard let imageURL = stadium.imageURL else { return nil }
|
guard let imageURL = stadium.imageURL else { return nil }
|
||||||
return (imageURL, stadium.id)
|
return (imageURL, stadium.id)
|
||||||
@@ -142,7 +142,7 @@ actor RemoteImageService {
|
|||||||
|
|
||||||
let images = await fetchImages(from: Array(urlToStadium.keys))
|
let images = await fetchImages(from: Array(urlToStadium.keys))
|
||||||
|
|
||||||
var result: [UUID: UIImage] = [:]
|
var result: [String: UIImage] = [:]
|
||||||
for (url, image) in images {
|
for (url, image) in images {
|
||||||
if let stadiumId = urlToStadium[url] {
|
if let stadiumId = urlToStadium[url] {
|
||||||
result[stadiumId] = image
|
result[stadiumId] = image
|
||||||
|
|||||||
@@ -174,12 +174,14 @@ final class PhotoImportViewModel {
|
|||||||
|
|
||||||
// Create the visit
|
// Create the visit
|
||||||
let visit = StadiumVisit(
|
let visit = StadiumVisit(
|
||||||
canonicalStadiumId: match.stadium.id.uuidString,
|
stadiumId: match.stadium.id,
|
||||||
stadiumUUID: match.stadium.id,
|
|
||||||
stadiumNameAtVisit: match.stadium.name,
|
stadiumNameAtVisit: match.stadium.name,
|
||||||
visitDate: match.game.dateTime,
|
visitDate: match.game.dateTime,
|
||||||
sport: match.game.sport,
|
sport: match.game.sport,
|
||||||
visitType: .game,
|
visitType: .game,
|
||||||
|
gameId: match.game.id,
|
||||||
|
homeTeamId: match.homeTeam.id,
|
||||||
|
awayTeamId: match.awayTeam.id,
|
||||||
homeTeamName: match.homeTeam.fullName,
|
homeTeamName: match.homeTeam.fullName,
|
||||||
awayTeamName: match.awayTeam.fullName,
|
awayTeamName: match.awayTeam.fullName,
|
||||||
finalScore: match.formattedFinalScore,
|
finalScore: match.formattedFinalScore,
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ final class ProgressViewModel {
|
|||||||
let visitedStadiumIds = Set(
|
let visitedStadiumIds = Set(
|
||||||
visits
|
visits
|
||||||
.filter { $0.sportEnum == selectedSport }
|
.filter { $0.sportEnum == selectedSport }
|
||||||
.compactMap { visit -> UUID? in
|
.compactMap { visit -> String? in
|
||||||
// Match visit's canonical stadium ID to a stadium
|
// Match visit's canonical stadium ID to a stadium
|
||||||
stadiums.first { stadium in
|
stadiums.first { stadium in
|
||||||
stadium.id == visit.stadiumUUID
|
stadium.id == visit.stadiumId
|
||||||
}?.id
|
}?.id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -62,11 +62,11 @@ final class ProgressViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stadium visit status indexed by stadium ID
|
/// Stadium visit status indexed by stadium ID
|
||||||
var stadiumVisitStatus: [UUID: StadiumVisitStatus] {
|
var stadiumVisitStatus: [String: StadiumVisitStatus] {
|
||||||
var statusMap: [UUID: StadiumVisitStatus] = [:]
|
var statusMap: [String: StadiumVisitStatus] = [:]
|
||||||
|
|
||||||
// Group visits by stadium
|
// Group visits by stadium
|
||||||
let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumUUID }
|
let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumId }
|
||||||
|
|
||||||
for stadium in stadiums {
|
for stadium in stadiums {
|
||||||
if let stadiumVisits = visitsByStadium[stadium.id], !stadiumVisits.isEmpty {
|
if let stadiumVisits = visitsByStadium[stadium.id], !stadiumVisits.isEmpty {
|
||||||
@@ -114,7 +114,7 @@ final class ProgressViewModel {
|
|||||||
.sorted { $0.visitDate > $1.visitDate }
|
.sorted { $0.visitDate > $1.visitDate }
|
||||||
.prefix(10)
|
.prefix(10)
|
||||||
.compactMap { visit -> VisitSummary? in
|
.compactMap { visit -> VisitSummary? in
|
||||||
guard let stadium = stadiums.first(where: { $0.id == visit.stadiumUUID }),
|
guard let stadium = stadiums.first(where: { $0.id == visit.stadiumId }),
|
||||||
let sport = visit.sportEnum else {
|
let sport = visit.sportEnum else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import MapKit
|
|||||||
|
|
||||||
struct ProgressMapView: View {
|
struct ProgressMapView: View {
|
||||||
let stadiums: [Stadium]
|
let stadiums: [Stadium]
|
||||||
let visitStatus: [UUID: StadiumVisitStatus]
|
let visitStatus: [String: StadiumVisitStatus]
|
||||||
@Binding var selectedStadium: Stadium?
|
@Binding var selectedStadium: Stadium?
|
||||||
|
|
||||||
// Fixed region for continental US - map is locked to this view
|
// Fixed region for continental US - map is locked to this view
|
||||||
|
|||||||
@@ -389,8 +389,7 @@ struct StadiumVisitSheet: View {
|
|||||||
|
|
||||||
// Create the visit
|
// Create the visit
|
||||||
let visit = StadiumVisit(
|
let visit = StadiumVisit(
|
||||||
canonicalStadiumId: stadium.id.uuidString, // Simplified - in production use StadiumIdentityService
|
stadiumId: stadium.id,
|
||||||
stadiumUUID: stadium.id,
|
|
||||||
stadiumNameAtVisit: stadium.name,
|
stadiumNameAtVisit: stadium.name,
|
||||||
visitDate: visitDate,
|
visitDate: visitDate,
|
||||||
sport: selectedSport,
|
sport: selectedSport,
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ extension VisitSource {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
let stadium = Stadium(
|
let stadium = Stadium(
|
||||||
id: UUID(),
|
id: "stadium_preview_oracle_park",
|
||||||
name: "Oracle Park",
|
name: "Oracle Park",
|
||||||
city: "San Francisco",
|
city: "San Francisco",
|
||||||
state: "CA",
|
state: "CA",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ final class TripCreationViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
var mustSeeGameIds: Set<UUID> = []
|
var mustSeeGameIds: Set<String> = []
|
||||||
var availableGames: [RichGame] = []
|
var availableGames: [RichGame] = []
|
||||||
var isLoadingGames: Bool = false
|
var isLoadingGames: Bool = false
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ final class TripCreationViewModel {
|
|||||||
var selectedRegions: Set<Region> = [.east, .central, .west]
|
var selectedRegions: Set<Region> = [.east, .central, .west]
|
||||||
|
|
||||||
// Follow Team Mode
|
// Follow Team Mode
|
||||||
var followTeamId: UUID?
|
var followTeamId: String?
|
||||||
var useHomeLocation: Bool = true
|
var useHomeLocation: Bool = true
|
||||||
|
|
||||||
// Game First Mode - Trip duration for sliding windows
|
// Game First Mode - Trip duration for sliding windows
|
||||||
@@ -148,8 +148,8 @@ final class TripCreationViewModel {
|
|||||||
|
|
||||||
// MARK: - Cached Data
|
// MARK: - Cached Data
|
||||||
|
|
||||||
private var teams: [UUID: Team] = [:]
|
private var teams: [String: Team] = [:]
|
||||||
private var stadiums: [UUID: Stadium] = [:]
|
private var stadiums: [String: Stadium] = [:]
|
||||||
private var games: [Game] = []
|
private var games: [Game] = []
|
||||||
private(set) var currentPreferences: TripPreferences?
|
private(set) var currentPreferences: TripPreferences?
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ final class TripCreationViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleMustSeeGame(_ gameId: UUID) {
|
func toggleMustSeeGame(_ gameId: String) {
|
||||||
if mustSeeGameIds.contains(gameId) {
|
if mustSeeGameIds.contains(gameId) {
|
||||||
mustSeeGameIds.remove(gameId)
|
mustSeeGameIds.remove(gameId)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import SwiftUI
|
|||||||
/// Renders a single timeline item (stop, travel, or rest).
|
/// Renders a single timeline item (stop, travel, or rest).
|
||||||
struct TimelineItemView: View {
|
struct TimelineItemView: View {
|
||||||
let item: TimelineItem
|
let item: TimelineItem
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
let isFirst: Bool
|
let isFirst: Bool
|
||||||
let isLast: Bool
|
let isLast: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
item: TimelineItem,
|
item: TimelineItem,
|
||||||
games: [UUID: RichGame],
|
games: [String: RichGame],
|
||||||
isFirst: Bool = false,
|
isFirst: Bool = false,
|
||||||
isLast: Bool = false
|
isLast: Bool = false
|
||||||
) {
|
) {
|
||||||
@@ -122,7 +122,7 @@ struct TimelineItemView: View {
|
|||||||
|
|
||||||
struct StopItemContent: View {
|
struct StopItemContent: View {
|
||||||
let stop: ItineraryStop
|
let stop: ItineraryStop
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
|
|
||||||
private var gamesAtStop: [RichGame] {
|
private var gamesAtStop: [RichGame] {
|
||||||
stop.games.compactMap { games[$0] }
|
stop.games.compactMap { games[$0] }
|
||||||
@@ -291,7 +291,7 @@ struct TimelineGameRow: View {
|
|||||||
/// Full timeline view for an itinerary option.
|
/// Full timeline view for an itinerary option.
|
||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
let option: ItineraryOption
|
let option: ItineraryOption
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
|
|
||||||
private var timeline: [TimelineItem] {
|
private var timeline: [TimelineItem] {
|
||||||
option.generateTimeline()
|
option.generateTimeline()
|
||||||
@@ -316,7 +316,7 @@ struct TimelineView: View {
|
|||||||
/// Horizontal scrolling timeline for compact display.
|
/// Horizontal scrolling timeline for compact display.
|
||||||
struct HorizontalTimelineView: View {
|
struct HorizontalTimelineView: View {
|
||||||
let option: ItineraryOption
|
let option: ItineraryOption
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
|
|
||||||
private var timeline: [TimelineItem] {
|
private var timeline: [TimelineItem] {
|
||||||
option.generateTimeline()
|
option.generateTimeline()
|
||||||
@@ -368,7 +368,7 @@ struct HorizontalTimelineView: View {
|
|||||||
|
|
||||||
struct HorizontalTimelineItemView: View {
|
struct HorizontalTimelineItemView: View {
|
||||||
let item: TimelineItem
|
let item: TimelineItem
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ struct TripCreationView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildGamesDictionary() -> [UUID: RichGame] {
|
private func buildGamesDictionary() -> [String: RichGame] {
|
||||||
viewModel.availableGames.reduce(into: [:]) { $0[$1.id] = $1 }
|
viewModel.availableGames.reduce(into: [:]) { $0[$1.id] = $1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,16 +949,16 @@ extension TripCreationViewModel.ViewState {
|
|||||||
|
|
||||||
struct GamePickerSheet: View {
|
struct GamePickerSheet: View {
|
||||||
let games: [RichGame]
|
let games: [RichGame]
|
||||||
@Binding var selectedIds: Set<UUID>
|
@Binding var selectedIds: Set<String>
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@State private var expandedSports: Set<Sport> = []
|
@State private var expandedSports: Set<Sport> = []
|
||||||
@State private var expandedTeams: Set<UUID> = []
|
@State private var expandedTeams: Set<String> = []
|
||||||
|
|
||||||
// Group games by Sport → Team (home team only to avoid duplicates)
|
// Group games by Sport → Team (home team only to avoid duplicates)
|
||||||
private var gamesBySport: [Sport: [TeamWithGames]] {
|
private var gamesBySport: [Sport: [TeamWithGames]] {
|
||||||
var result: [Sport: [UUID: TeamWithGames]] = [:]
|
var result: [Sport: [String: TeamWithGames]] = [:]
|
||||||
|
|
||||||
for game in games {
|
for game in games {
|
||||||
let sport = game.game.sport
|
let sport = game.game.sport
|
||||||
@@ -1063,9 +1063,9 @@ struct GamePickerSheet: View {
|
|||||||
struct SportSection: View {
|
struct SportSection: View {
|
||||||
let sport: Sport
|
let sport: Sport
|
||||||
let teams: [TeamWithGames]
|
let teams: [TeamWithGames]
|
||||||
@Binding var selectedIds: Set<UUID>
|
@Binding var selectedIds: Set<String>
|
||||||
@Binding var expandedSports: Set<Sport>
|
@Binding var expandedSports: Set<Sport>
|
||||||
@Binding var expandedTeams: Set<UUID>
|
@Binding var expandedTeams: Set<String>
|
||||||
let selectedCount: Int
|
let selectedCount: Int
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@@ -1146,8 +1146,8 @@ struct SportSection: View {
|
|||||||
|
|
||||||
struct TeamSection: View {
|
struct TeamSection: View {
|
||||||
let teamData: TeamWithGames
|
let teamData: TeamWithGames
|
||||||
@Binding var selectedIds: Set<UUID>
|
@Binding var selectedIds: Set<String>
|
||||||
@Binding var expandedTeams: Set<UUID>
|
@Binding var expandedTeams: Set<String>
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@@ -1310,7 +1310,7 @@ struct TeamWithGames: Identifiable {
|
|||||||
let sport: Sport
|
let sport: Sport
|
||||||
var games: [RichGame]
|
var games: [RichGame]
|
||||||
|
|
||||||
var id: UUID { team.id }
|
var id: String { team.id }
|
||||||
|
|
||||||
var sortedGames: [RichGame] {
|
var sortedGames: [RichGame] {
|
||||||
games.sorted { $0.game.dateTime < $1.game.dateTime }
|
games.sorted { $0.game.dateTime < $1.game.dateTime }
|
||||||
@@ -1504,7 +1504,7 @@ enum CitiesFilter: Int, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
struct TripOptionsView: View {
|
struct TripOptionsView: View {
|
||||||
let options: [ItineraryOption]
|
let options: [ItineraryOption]
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
let preferences: TripPreferences?
|
let preferences: TripPreferences?
|
||||||
let convertToTrip: (ItineraryOption) -> Trip
|
let convertToTrip: (ItineraryOption) -> Trip
|
||||||
|
|
||||||
@@ -1774,7 +1774,7 @@ struct TripOptionsView: View {
|
|||||||
|
|
||||||
struct TripOptionCard: View {
|
struct TripOptionCard: View {
|
||||||
let option: ItineraryOption
|
let option: ItineraryOption
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
let onSelect: () -> Void
|
let onSelect: () -> Void
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@@ -2393,7 +2393,7 @@ struct DayCell: View {
|
|||||||
// MARK: - Team Picker Sheet
|
// MARK: - Team Picker Sheet
|
||||||
|
|
||||||
struct TeamPickerSheet: View {
|
struct TeamPickerSheet: View {
|
||||||
@Binding var selectedTeamId: UUID?
|
@Binding var selectedTeamId: String?
|
||||||
let teamsBySport: [Sport: [Team]]
|
let teamsBySport: [Sport: [Team]]
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct TripDetailView: View {
|
|||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
let trip: Trip
|
let trip: Trip
|
||||||
let games: [UUID: RichGame]
|
let games: [String: RichGame]
|
||||||
|
|
||||||
@State private var selectedDay: ItineraryDay?
|
@State private var selectedDay: ItineraryDay?
|
||||||
@State private var showExportSheet = false
|
@State private var showExportSheet = false
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ enum GameDAGRouter {
|
|||||||
|
|
||||||
/// Composite key for exact deduplication
|
/// Composite key for exact deduplication
|
||||||
var uniqueKey: String {
|
var uniqueKey: String {
|
||||||
route.map { $0.id.uuidString }.joined(separator: "-")
|
route.map { $0.id }.joined(separator: "-")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +106,9 @@ enum GameDAGRouter {
|
|||||||
///
|
///
|
||||||
static func findRoutes(
|
static func findRoutes(
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
constraints: DrivingConstraints,
|
constraints: DrivingConstraints,
|
||||||
anchorGameIds: Set<UUID> = [],
|
anchorGameIds: Set<String> = [],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
beamWidth: Int = defaultBeamWidth
|
beamWidth: Int = defaultBeamWidth
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
@@ -219,10 +219,10 @@ enum GameDAGRouter {
|
|||||||
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
|
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
|
||||||
static func findAllSensibleRoutes(
|
static func findAllSensibleRoutes(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
anchorGameIds: Set<UUID> = [],
|
anchorGameIds: Set<String> = [],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop]
|
stopBuilder: ([Game], [String: Stadium]) -> [ItineraryStop]
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
let constraints = DrivingConstraints.default
|
let constraints = DrivingConstraints.default
|
||||||
return findRoutes(
|
return findRoutes(
|
||||||
@@ -244,7 +244,7 @@ enum GameDAGRouter {
|
|||||||
/// - Short duration AND long duration
|
/// - Short duration AND long duration
|
||||||
private static func selectDiverseRoutes(
|
private static func selectDiverseRoutes(
|
||||||
_ routes: [[Game]],
|
_ routes: [[Game]],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
maxCount: Int
|
maxCount: Int
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
guard !routes.isEmpty else { return [] }
|
guard !routes.isEmpty else { return [] }
|
||||||
@@ -362,14 +362,14 @@ enum GameDAGRouter {
|
|||||||
/// Keeps routes that span the diversity space rather than just high-scoring ones.
|
/// Keeps routes that span the diversity space rather than just high-scoring ones.
|
||||||
private static func diversityPrune(
|
private static func diversityPrune(
|
||||||
_ paths: [[Game]],
|
_ paths: [[Game]],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
targetCount: Int
|
targetCount: Int
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
// Remove exact duplicates first
|
// Remove exact duplicates first
|
||||||
var uniquePaths: [[Game]] = []
|
var uniquePaths: [[Game]] = []
|
||||||
var seen = Set<String>()
|
var seen = Set<String>()
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let key = path.map { $0.id.uuidString }.joined(separator: "-")
|
let key = path.map { $0.id }.joined(separator: "-")
|
||||||
if !seen.contains(key) {
|
if !seen.contains(key) {
|
||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
uniquePaths.append(path)
|
uniquePaths.append(path)
|
||||||
@@ -425,7 +425,7 @@ enum GameDAGRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a profile for a route.
|
/// Builds a profile for a route.
|
||||||
private static func buildProfile(for route: [Game], stadiums: [UUID: Stadium]) -> RouteProfile {
|
private static func buildProfile(for route: [Game], stadiums: [String: Stadium]) -> RouteProfile {
|
||||||
let gameCount = route.count
|
let gameCount = route.count
|
||||||
let cities = Set(route.compactMap { stadiums[$0.stadiumId]?.city })
|
let cities = Set(route.compactMap { stadiums[$0.stadiumId]?.city })
|
||||||
let cityCount = cities.count
|
let cityCount = cities.count
|
||||||
@@ -488,7 +488,7 @@ enum GameDAGRouter {
|
|||||||
private static func canTransition(
|
private static func canTransition(
|
||||||
from: Game,
|
from: Game,
|
||||||
to: Game,
|
to: Game,
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
constraints: DrivingConstraints
|
constraints: DrivingConstraints
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
// Time must move forward
|
// Time must move forward
|
||||||
@@ -562,7 +562,7 @@ enum GameDAGRouter {
|
|||||||
private static func estimateDistanceMiles(
|
private static func estimateDistanceMiles(
|
||||||
from: Game,
|
from: Game,
|
||||||
to: Game,
|
to: Game,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> Double {
|
) -> Double {
|
||||||
if from.stadiumId == to.stadiumId { return 0 }
|
if from.stadiumId == to.stadiumId { return 0 }
|
||||||
|
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
|||||||
///
|
///
|
||||||
private func buildStops(
|
private func buildStops(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [ItineraryStop] {
|
) -> [ItineraryStop] {
|
||||||
guard !games.isEmpty else { return [] }
|
guard !games.isEmpty else { return [] }
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
|||||||
// Group consecutive games at the same stadium into stops
|
// Group consecutive games at the same stadium into stops
|
||||||
// If you visit A, then B, then A again, that's 3 stops (A, B, A)
|
// If you visit A, then B, then A again, that's 3 stops (A, B, A)
|
||||||
var stops: [ItineraryStop] = []
|
var stops: [ItineraryStop] = []
|
||||||
var currentStadiumId: UUID? = nil
|
var currentStadiumId: String? = nil
|
||||||
var currentGames: [Game] = []
|
var currentGames: [Game] = []
|
||||||
|
|
||||||
for game in sortedGames {
|
for game in sortedGames {
|
||||||
@@ -340,8 +340,8 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
|||||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||||
private func createStop(
|
private func createStop(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> ItineraryStop? {
|
) -> ItineraryStop? {
|
||||||
guard !games.isEmpty else { return nil }
|
guard !games.isEmpty else { return nil }
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
|||||||
var unique: [[Game]] = []
|
var unique: [[Game]] = []
|
||||||
|
|
||||||
for route in routes {
|
for route in routes {
|
||||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||||
if !seen.contains(key) {
|
if !seen.contains(key) {
|
||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
unique.append(route)
|
unique.append(route)
|
||||||
@@ -396,7 +396,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
|||||||
/// This ensures we get diverse options from East, Central, and West coasts.
|
/// This ensures we get diverse options from East, Central, and West coasts.
|
||||||
private func findRoutesPerRegion(
|
private func findRoutesPerRegion(
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
allowRepeatCities: Bool
|
allowRepeatCities: Bool
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
// Partition games by region
|
// Partition games by region
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
|||||||
/// Creates separate stops when visiting the same city with other cities in between.
|
/// Creates separate stops when visiting the same city with other cities in between.
|
||||||
private func buildStops(
|
private func buildStops(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [ItineraryStop] {
|
) -> [ItineraryStop] {
|
||||||
guard !games.isEmpty else { return [] }
|
guard !games.isEmpty else { return [] }
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
|||||||
|
|
||||||
// Group consecutive games at the same stadium
|
// Group consecutive games at the same stadium
|
||||||
var stops: [ItineraryStop] = []
|
var stops: [ItineraryStop] = []
|
||||||
var currentStadiumId: UUID? = nil
|
var currentStadiumId: String? = nil
|
||||||
var currentGames: [Game] = []
|
var currentGames: [Game] = []
|
||||||
|
|
||||||
for game in sortedGames {
|
for game in sortedGames {
|
||||||
@@ -357,8 +357,8 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
|||||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||||
private func createStop(
|
private func createStop(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> ItineraryStop? {
|
) -> ItineraryStop? {
|
||||||
guard !games.isEmpty else { return nil }
|
guard !games.isEmpty else { return nil }
|
||||||
|
|
||||||
@@ -396,8 +396,8 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
|||||||
/// For Scenario B, routes must still contain all anchor games.
|
/// For Scenario B, routes must still contain all anchor games.
|
||||||
private func findRoutesPerRegion(
|
private func findRoutesPerRegion(
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
anchorGameIds: Set<UUID>,
|
anchorGameIds: Set<String>,
|
||||||
allowRepeatCities: Bool
|
allowRepeatCities: Bool
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
// First, determine which region(s) the anchor games are in
|
// First, determine which region(s) the anchor games are in
|
||||||
@@ -459,7 +459,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
|||||||
var unique: [[Game]] = []
|
var unique: [[Game]] = []
|
||||||
|
|
||||||
for route in routes {
|
for route in routes {
|
||||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||||
if !seen.contains(key) {
|
if !seen.contains(key) {
|
||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
unique.append(route)
|
unique.append(route)
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
/// Finds all stadiums in a given city (case-insensitive match).
|
/// Finds all stadiums in a given city (case-insensitive match).
|
||||||
private func findStadiumsInCity(
|
private func findStadiumsInCity(
|
||||||
cityName: String,
|
cityName: String,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [Stadium] {
|
) -> [Stadium] {
|
||||||
let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces)
|
let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces)
|
||||||
return stadiums.values.filter { stadium in
|
return stadiums.values.filter { stadium in
|
||||||
@@ -296,14 +296,14 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
private func findDirectionalStadiums(
|
private func findDirectionalStadiums(
|
||||||
from start: CLLocationCoordinate2D,
|
from start: CLLocationCoordinate2D,
|
||||||
to end: CLLocationCoordinate2D,
|
to end: CLLocationCoordinate2D,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> Set<UUID> {
|
) -> Set<String> {
|
||||||
let directDistance = distanceBetween(start, end)
|
let directDistance = distanceBetween(start, end)
|
||||||
|
|
||||||
// Allow detours up to 50% longer than direct distance
|
// Allow detours up to 50% longer than direct distance
|
||||||
let maxDetourDistance = directDistance * 1.5
|
let maxDetourDistance = directDistance * 1.5
|
||||||
|
|
||||||
var directionalIds: Set<UUID> = []
|
var directionalIds: Set<String> = []
|
||||||
|
|
||||||
for (id, stadium) in stadiums {
|
for (id, stadium) in stadiums {
|
||||||
let stadiumCoord = stadium.coordinate
|
let stadiumCoord = stadium.coordinate
|
||||||
@@ -349,8 +349,8 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
/// Create a date range from start_game.date to end_game.date
|
/// Create a date range from start_game.date to end_game.date
|
||||||
///
|
///
|
||||||
private func generateDateRanges(
|
private func generateDateRanges(
|
||||||
startStadiumIds: Set<UUID>,
|
startStadiumIds: Set<String>,
|
||||||
endStadiumIds: Set<UUID>,
|
endStadiumIds: Set<String>,
|
||||||
allGames: [Game],
|
allGames: [Game],
|
||||||
request: PlanningRequest
|
request: PlanningRequest
|
||||||
) -> [DateInterval] {
|
) -> [DateInterval] {
|
||||||
@@ -417,7 +417,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
/// Creates separate stops when visiting the same city with other cities in between.
|
/// Creates separate stops when visiting the same city with other cities in between.
|
||||||
private func buildStops(
|
private func buildStops(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [ItineraryStop] {
|
) -> [ItineraryStop] {
|
||||||
guard !games.isEmpty else { return [] }
|
guard !games.isEmpty else { return [] }
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
|
|
||||||
// Group consecutive games at the same stadium
|
// Group consecutive games at the same stadium
|
||||||
var stops: [ItineraryStop] = []
|
var stops: [ItineraryStop] = []
|
||||||
var currentStadiumId: UUID? = nil
|
var currentStadiumId: String? = nil
|
||||||
var currentGames: [Game] = []
|
var currentGames: [Game] = []
|
||||||
|
|
||||||
for game in sortedGames {
|
for game in sortedGames {
|
||||||
@@ -458,8 +458,8 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||||
private func createStop(
|
private func createStop(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> ItineraryStop? {
|
) -> ItineraryStop? {
|
||||||
guard !games.isEmpty else { return nil }
|
guard !games.isEmpty else { return nil }
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
|||||||
start: LocationInput,
|
start: LocationInput,
|
||||||
end: LocationInput,
|
end: LocationInput,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [ItineraryStop] {
|
) -> [ItineraryStop] {
|
||||||
|
|
||||||
var stops: [ItineraryStop] = []
|
var stops: [ItineraryStop] = []
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
|||||||
// MARK: - Team Filtering
|
// MARK: - Team Filtering
|
||||||
|
|
||||||
/// Filters games to those involving the followed team (home or away).
|
/// Filters games to those involving the followed team (home or away).
|
||||||
private func filterToTeam(_ games: [Game], teamId: UUID) -> [Game] {
|
private func filterToTeam(_ games: [Game], teamId: String) -> [Game] {
|
||||||
games.filter { game in
|
games.filter { game in
|
||||||
game.homeTeamId == teamId || game.awayTeamId == teamId
|
game.homeTeamId == teamId || game.awayTeamId == teamId
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
|||||||
private func applyRepeatCityFilter(
|
private func applyRepeatCityFilter(
|
||||||
_ games: [Game],
|
_ games: [Game],
|
||||||
allowRepeat: Bool,
|
allowRepeat: Bool,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [Game] {
|
) -> [Game] {
|
||||||
guard !allowRepeat else {
|
guard !allowRepeat else {
|
||||||
print("🔍 applyRepeatCityFilter: allowRepeat=true, returning all \(games.count) games")
|
print("🔍 applyRepeatCityFilter: allowRepeat=true, returning all \(games.count) games")
|
||||||
@@ -317,14 +317,14 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
|||||||
/// Same logic as ScenarioAPlanner.
|
/// Same logic as ScenarioAPlanner.
|
||||||
private func buildStops(
|
private func buildStops(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> [ItineraryStop] {
|
) -> [ItineraryStop] {
|
||||||
guard !games.isEmpty else { return [] }
|
guard !games.isEmpty else { return [] }
|
||||||
|
|
||||||
let sortedGames = games.sorted { $0.startTime < $1.startTime }
|
let sortedGames = games.sorted { $0.startTime < $1.startTime }
|
||||||
|
|
||||||
var stops: [ItineraryStop] = []
|
var stops: [ItineraryStop] = []
|
||||||
var currentStadiumId: UUID? = nil
|
var currentStadiumId: String? = nil
|
||||||
var currentGames: [Game] = []
|
var currentGames: [Game] = []
|
||||||
|
|
||||||
for game in sortedGames {
|
for game in sortedGames {
|
||||||
@@ -354,8 +354,8 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
|||||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||||
private func createStop(
|
private func createStop(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> ItineraryStop? {
|
) -> ItineraryStop? {
|
||||||
guard !games.isEmpty else { return nil }
|
guard !games.isEmpty else { return nil }
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
|||||||
var unique: [[Game]] = []
|
var unique: [[Game]] = []
|
||||||
|
|
||||||
for route in routes {
|
for route in routes {
|
||||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||||
if !seen.contains(key) {
|
if !seen.contains(key) {
|
||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
unique.append(route)
|
unique.append(route)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ enum ScenarioPlannerFactory {
|
|||||||
/// Creates the appropriate planner based on the request inputs
|
/// Creates the appropriate planner based on the request inputs
|
||||||
static func planner(for request: PlanningRequest) -> ScenarioPlanner {
|
static func planner(for request: PlanningRequest) -> ScenarioPlanner {
|
||||||
print("🔍 ScenarioPlannerFactory: Selecting planner...")
|
print("🔍 ScenarioPlannerFactory: Selecting planner...")
|
||||||
print(" - followTeamId: \(request.preferences.followTeamId?.uuidString ?? "nil")")
|
print(" - followTeamId: \(request.preferences.followTeamId ?? "nil")")
|
||||||
print(" - selectedGames.count: \(request.selectedGames.count)")
|
print(" - selectedGames.count: \(request.selectedGames.count)")
|
||||||
print(" - startLocation: \(request.startLocation?.name ?? "nil")")
|
print(" - startLocation: \(request.startLocation?.name ?? "nil")")
|
||||||
print(" - endLocation: \(request.endLocation?.name ?? "nil")")
|
print(" - endLocation: \(request.endLocation?.name ?? "nil")")
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ struct ItineraryStop: Identifiable, Hashable {
|
|||||||
let city: String
|
let city: String
|
||||||
let state: String
|
let state: String
|
||||||
let coordinate: CLLocationCoordinate2D?
|
let coordinate: CLLocationCoordinate2D?
|
||||||
let games: [UUID]
|
let games: [String] // Canonical game IDs
|
||||||
let arrivalDate: Date
|
let arrivalDate: Date
|
||||||
let departureDate: Date
|
let departureDate: Date
|
||||||
let location: LocationInput
|
let location: LocationInput
|
||||||
@@ -490,8 +490,8 @@ extension ItineraryOption {
|
|||||||
struct PlanningRequest {
|
struct PlanningRequest {
|
||||||
let preferences: TripPreferences
|
let preferences: TripPreferences
|
||||||
let availableGames: [Game]
|
let availableGames: [Game]
|
||||||
let teams: [UUID: Team]
|
let teams: [String: Team] // Keyed by canonical ID
|
||||||
let stadiums: [UUID: Stadium]
|
let stadiums: [String: Stadium] // Keyed by canonical ID
|
||||||
|
|
||||||
// MARK: - Computed Properties for Engine
|
// MARK: - Computed Properties for Engine
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ struct FixtureGenerator {
|
|||||||
let stadiums: [Stadium]
|
let stadiums: [Stadium]
|
||||||
let teams: [Team]
|
let teams: [Team]
|
||||||
let games: [Game]
|
let games: [Game]
|
||||||
let stadiumsById: [UUID: Stadium]
|
let stadiumsById: [String: Stadium]
|
||||||
let teamsById: [UUID: Team]
|
let teamsById: [String: Team]
|
||||||
|
|
||||||
func richGame(from game: Game) -> RichGame? {
|
func richGame(from game: Game) -> RichGame? {
|
||||||
guard let homeTeam = teamsById[game.homeTeamId],
|
guard let homeTeam = teamsById[game.homeTeamId],
|
||||||
@@ -168,7 +168,7 @@ struct FixtureGenerator {
|
|||||||
return cities.enumerated().map { index, city in
|
return cities.enumerated().map { index, city in
|
||||||
let sport = config.sports.randomElement(using: &rng) ?? .mlb
|
let sport = config.sports.randomElement(using: &rng) ?? .mlb
|
||||||
return Stadium(
|
return Stadium(
|
||||||
id: UUID(),
|
id: "stadium_test_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))_\(index)",
|
||||||
name: "\(city.name) \(sport.rawValue) Stadium",
|
name: "\(city.name) \(sport.rawValue) Stadium",
|
||||||
city: city.name,
|
city: city.name,
|
||||||
state: city.state,
|
state: city.state,
|
||||||
@@ -203,8 +203,9 @@ struct FixtureGenerator {
|
|||||||
|
|
||||||
usedNames.insert("\(stadium.city) \(teamName)")
|
usedNames.insert("\(stadium.city) \(teamName)")
|
||||||
|
|
||||||
|
let teamId = "team_test_\(stadium.city.lowercased().replacingOccurrences(of: " ", with: "_"))_\(teamName.lowercased())_\(teams.count)"
|
||||||
teams.append(Team(
|
teams.append(Team(
|
||||||
id: UUID(),
|
id: teamId,
|
||||||
name: teamName,
|
name: teamName,
|
||||||
abbreviation: String(teamName.prefix(3)).uppercased(),
|
abbreviation: String(teamName.prefix(3)).uppercased(),
|
||||||
sport: stadium.sport,
|
sport: stadium.sport,
|
||||||
@@ -251,8 +252,9 @@ struct FixtureGenerator {
|
|||||||
let hour = Int.random(in: 13...21, using: &rng)
|
let hour = Int.random(in: 13...21, using: &rng)
|
||||||
let gameDateTime = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: gameDate)!
|
let gameDateTime = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: gameDate)!
|
||||||
|
|
||||||
|
let gameId = "game_test_\(games.count)_\(homeTeam.abbreviation)_\(awayTeam.abbreviation)"
|
||||||
games.append(Game(
|
games.append(Game(
|
||||||
id: UUID(),
|
id: gameId,
|
||||||
homeTeamId: homeTeam.id,
|
homeTeamId: homeTeam.id,
|
||||||
awayTeamId: awayTeam.id,
|
awayTeamId: awayTeam.id,
|
||||||
stadiumId: stadium.id,
|
stadiumId: stadium.id,
|
||||||
@@ -331,8 +333,8 @@ struct FixtureGenerator {
|
|||||||
coordinate: CLLocationCoordinate2D(latitude: city.lat, longitude: city.lon),
|
coordinate: CLLocationCoordinate2D(latitude: city.lat, longitude: city.lon),
|
||||||
arrivalDate: currentDate,
|
arrivalDate: currentDate,
|
||||||
departureDate: departureDate,
|
departureDate: departureDate,
|
||||||
games: [UUID()],
|
games: ["game_test_\(i)"],
|
||||||
stadium: UUID()
|
stadium: "stadium_test_\(i)"
|
||||||
))
|
))
|
||||||
|
|
||||||
currentDate = departureDate
|
currentDate = departureDate
|
||||||
@@ -351,12 +353,12 @@ struct FixtureGenerator {
|
|||||||
cities: [(name: String, lat: Double, lon: Double)],
|
cities: [(name: String, lat: Double, lon: Double)],
|
||||||
rng: inout SeededRandomNumberGenerator
|
rng: inout SeededRandomNumberGenerator
|
||||||
) -> [Game] {
|
) -> [Game] {
|
||||||
cities.map { city in
|
cities.enumerated().map { index, city in
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_conflict_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))_\(index)"
|
||||||
return Game(
|
return Game(
|
||||||
id: UUID(),
|
id: "game_conflict_\(index)_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))",
|
||||||
homeTeamId: UUID(),
|
homeTeamId: "team_conflict_home_\(index)",
|
||||||
awayTeamId: UUID(),
|
awayTeamId: "team_conflict_away_\(index)",
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
dateTime: date,
|
dateTime: date,
|
||||||
sport: .mlb,
|
sport: .mlb,
|
||||||
@@ -367,7 +369,7 @@ struct FixtureGenerator {
|
|||||||
|
|
||||||
/// Generate a stadium at a specific location
|
/// Generate a stadium at a specific location
|
||||||
static func makeStadium(
|
static func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
name: String = "Test Stadium",
|
name: String = "Test Stadium",
|
||||||
city: String = "Test City",
|
city: String = "Test City",
|
||||||
state: String = "TS",
|
state: String = "TS",
|
||||||
@@ -390,12 +392,12 @@ struct FixtureGenerator {
|
|||||||
|
|
||||||
/// Generate a team
|
/// Generate a team
|
||||||
static func makeTeam(
|
static func makeTeam(
|
||||||
id: UUID = UUID(),
|
id: String = "team_test_\(UUID().uuidString)",
|
||||||
name: String = "Test Team",
|
name: String = "Test Team",
|
||||||
abbreviation: String = "TST",
|
abbreviation: String = "TST",
|
||||||
sport: Sport = .mlb,
|
sport: Sport = .mlb,
|
||||||
city: String = "Test City",
|
city: String = "Test City",
|
||||||
stadiumId: UUID
|
stadiumId: String
|
||||||
) -> Team {
|
) -> Team {
|
||||||
Team(
|
Team(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -409,10 +411,10 @@ struct FixtureGenerator {
|
|||||||
|
|
||||||
/// Generate a game
|
/// Generate a game
|
||||||
static func makeGame(
|
static func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
homeTeamId: UUID,
|
homeTeamId: String,
|
||||||
awayTeamId: UUID,
|
awayTeamId: String,
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
dateTime: Date = Date(),
|
dateTime: Date = Date(),
|
||||||
sport: Sport = .mlb,
|
sport: Sport = .mlb,
|
||||||
season: String = "2026",
|
season: String = "2026",
|
||||||
@@ -454,8 +456,8 @@ struct FixtureGenerator {
|
|||||||
coordinate: CLLocationCoordinate2D? = nil,
|
coordinate: CLLocationCoordinate2D? = nil,
|
||||||
arrivalDate: Date = Date(),
|
arrivalDate: Date = Date(),
|
||||||
departureDate: Date? = nil,
|
departureDate: Date? = nil,
|
||||||
games: [UUID] = [],
|
games: [String] = [],
|
||||||
stadium: UUID? = nil,
|
stadium: String? = nil,
|
||||||
isRestDay: Bool = false
|
isRestDay: Bool = false
|
||||||
) -> TripStop {
|
) -> TripStop {
|
||||||
TripStop(
|
TripStop(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct BruteForceRouteVerifier {
|
|||||||
let isOptimal: Bool
|
let isOptimal: Bool
|
||||||
let proposedRouteDistance: Double
|
let proposedRouteDistance: Double
|
||||||
let optimalRouteDistance: Double
|
let optimalRouteDistance: Double
|
||||||
let optimalRoute: [UUID]?
|
let optimalRoute: [String]?
|
||||||
let improvement: Double? // Percentage improvement if not optimal
|
let improvement: Double? // Percentage improvement if not optimal
|
||||||
let permutationsChecked: Int
|
let permutationsChecked: Int
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ struct BruteForceRouteVerifier {
|
|||||||
/// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal)
|
/// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal)
|
||||||
/// - Returns: Verification result
|
/// - Returns: Verification result
|
||||||
static func verify(
|
static func verify(
|
||||||
proposedRoute: [UUID],
|
proposedRoute: [String],
|
||||||
stops: [UUID: CLLocationCoordinate2D],
|
stops: [String: CLLocationCoordinate2D],
|
||||||
tolerance: Double = 0
|
tolerance: Double = 0
|
||||||
) -> VerificationResult {
|
) -> VerificationResult {
|
||||||
guard proposedRoute.count <= TestConstants.bruteForceMaxStops else {
|
guard proposedRoute.count <= TestConstants.bruteForceMaxStops else {
|
||||||
@@ -64,7 +64,7 @@ struct BruteForceRouteVerifier {
|
|||||||
// Find optimal route by checking all permutations
|
// Find optimal route by checking all permutations
|
||||||
let allPermutations = permutations(of: proposedRoute)
|
let allPermutations = permutations(of: proposedRoute)
|
||||||
var optimalDistance = Double.infinity
|
var optimalDistance = Double.infinity
|
||||||
var optimalRoute: [UUID] = []
|
var optimalRoute: [String] = []
|
||||||
|
|
||||||
for permutation in allPermutations {
|
for permutation in allPermutations {
|
||||||
let distance = calculateRouteDistance(permutation, stops: stops)
|
let distance = calculateRouteDistance(permutation, stops: stops)
|
||||||
@@ -102,10 +102,10 @@ struct BruteForceRouteVerifier {
|
|||||||
|
|
||||||
/// Verify a route is optimal with a fixed start and end point
|
/// Verify a route is optimal with a fixed start and end point
|
||||||
static func verifyWithFixedEndpoints(
|
static func verifyWithFixedEndpoints(
|
||||||
proposedRoute: [UUID],
|
proposedRoute: [String],
|
||||||
stops: [UUID: CLLocationCoordinate2D],
|
stops: [String: CLLocationCoordinate2D],
|
||||||
startId: UUID,
|
startId: String,
|
||||||
endId: UUID,
|
endId: String,
|
||||||
tolerance: Double = 0
|
tolerance: Double = 0
|
||||||
) -> VerificationResult {
|
) -> VerificationResult {
|
||||||
guard proposedRoute.first == startId && proposedRoute.last == endId else {
|
guard proposedRoute.first == startId && proposedRoute.last == endId else {
|
||||||
@@ -132,7 +132,7 @@ struct BruteForceRouteVerifier {
|
|||||||
// Generate all permutations of intermediate stops
|
// Generate all permutations of intermediate stops
|
||||||
let allPermutations = permutations(of: Array(intermediateStops))
|
let allPermutations = permutations(of: Array(intermediateStops))
|
||||||
var optimalDistance = Double.infinity
|
var optimalDistance = Double.infinity
|
||||||
var optimalRoute: [UUID] = []
|
var optimalRoute: [String] = []
|
||||||
|
|
||||||
for permutation in allPermutations {
|
for permutation in allPermutations {
|
||||||
var fullRoute = [startId]
|
var fullRoute = [startId]
|
||||||
@@ -172,8 +172,8 @@ struct BruteForceRouteVerifier {
|
|||||||
|
|
||||||
/// Check if there's an obviously better route (significantly shorter)
|
/// Check if there's an obviously better route (significantly shorter)
|
||||||
static func hasObviouslyBetterRoute(
|
static func hasObviouslyBetterRoute(
|
||||||
proposedRoute: [UUID],
|
proposedRoute: [String],
|
||||||
stops: [UUID: CLLocationCoordinate2D],
|
stops: [String: CLLocationCoordinate2D],
|
||||||
threshold: Double = 0.1 // 10% improvement threshold
|
threshold: Double = 0.1 // 10% improvement threshold
|
||||||
) -> (hasBetter: Bool, improvement: Double?) {
|
) -> (hasBetter: Bool, improvement: Double?) {
|
||||||
let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold)
|
let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold)
|
||||||
@@ -184,8 +184,8 @@ struct BruteForceRouteVerifier {
|
|||||||
|
|
||||||
/// Calculate total route distance using haversine formula
|
/// Calculate total route distance using haversine formula
|
||||||
static func calculateRouteDistance(
|
static func calculateRouteDistance(
|
||||||
_ route: [UUID],
|
_ route: [String],
|
||||||
stops: [UUID: CLLocationCoordinate2D]
|
stops: [String: CLLocationCoordinate2D]
|
||||||
) -> Double {
|
) -> Double {
|
||||||
guard route.count >= 2 else { return 0 }
|
guard route.count >= 2 else { return 0 }
|
||||||
|
|
||||||
@@ -263,15 +263,15 @@ struct BruteForceRouteVerifier {
|
|||||||
extension BruteForceRouteVerifier {
|
extension BruteForceRouteVerifier {
|
||||||
/// Verify a trip's route is optimal
|
/// Verify a trip's route is optimal
|
||||||
static func verifyTrip(_ trip: Trip) -> VerificationResult {
|
static func verifyTrip(_ trip: Trip) -> VerificationResult {
|
||||||
var stops: [UUID: CLLocationCoordinate2D] = [:]
|
var stops: [String: CLLocationCoordinate2D] = [:]
|
||||||
|
|
||||||
for stop in trip.stops {
|
for stop in trip.stops {
|
||||||
if let coord = stop.coordinate {
|
if let coord = stop.coordinate {
|
||||||
stops[stop.id] = coord
|
stops[stop.id.uuidString] = coord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let routeIds = trip.stops.map { $0.id }
|
let routeIds = trip.stops.map { $0.id.uuidString }
|
||||||
return verify(proposedRoute: routeIds, stops: stops)
|
return verify(proposedRoute: routeIds, stops: stops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Internal Storage
|
// MARK: - Internal Storage
|
||||||
|
|
||||||
private var teamsById: [UUID: Team] = [:]
|
private var teamsById: [String: Team] = [:]
|
||||||
private var stadiumsById: [UUID: Stadium] = [:]
|
private var stadiumsById: [String: Stadium] = [:]
|
||||||
private var games: [Game] = []
|
private var games: [Game] = []
|
||||||
private var gamesById: [UUID: Game] = [:]
|
private var gamesById: [String: Game] = [:]
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
@@ -140,11 +140,11 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Data Access
|
// MARK: - Data Access
|
||||||
|
|
||||||
func team(for id: UUID) -> Team? {
|
func team(for id: String) -> Team? {
|
||||||
teamsById[id]
|
teamsById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
func stadium(for id: UUID) -> Stadium? {
|
func stadium(for id: String) -> Stadium? {
|
||||||
stadiumsById[id]
|
stadiumsById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
}.sorted { $0.dateTime < $1.dateTime }
|
}.sorted { $0.dateTime < $1.dateTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchGame(by id: UUID) async throws -> Game? {
|
func fetchGame(by id: String) async throws -> Game? {
|
||||||
await simulateLatency()
|
await simulateLatency()
|
||||||
|
|
||||||
if config.shouldFailOnFetch {
|
if config.shouldFailOnFetch {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ actor MockCloudKitService {
|
|||||||
}.sorted { $0.dateTime < $1.dateTime }
|
}.sorted { $0.dateTime < $1.dateTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchGame(by id: UUID) async throws -> Game? {
|
func fetchGame(by id: String) async throws -> Game? {
|
||||||
try await simulateNetwork()
|
try await simulateNetwork()
|
||||||
return games.first { $0.id == id }
|
return games.first { $0.id == id }
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ actor MockCloudKitService {
|
|||||||
return stadiums.map { stadium in
|
return stadiums.map { stadium in
|
||||||
CloudKitService.SyncStadium(
|
CloudKitService.SyncStadium(
|
||||||
stadium: stadium,
|
stadium: stadium,
|
||||||
canonicalId: stadium.id.uuidString
|
canonicalId: stadium.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,8 +177,8 @@ actor MockCloudKitService {
|
|||||||
return teams.filter { $0.sport == sport }.map { team in
|
return teams.filter { $0.sport == sport }.map { team in
|
||||||
CloudKitService.SyncTeam(
|
CloudKitService.SyncTeam(
|
||||||
team: team,
|
team: team,
|
||||||
canonicalId: team.id.uuidString,
|
canonicalId: team.id,
|
||||||
stadiumCanonicalId: team.stadiumId.uuidString
|
stadiumCanonicalId: team.stadiumId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,10 +197,10 @@ actor MockCloudKitService {
|
|||||||
}.map { game in
|
}.map { game in
|
||||||
CloudKitService.SyncGame(
|
CloudKitService.SyncGame(
|
||||||
game: game,
|
game: game,
|
||||||
canonicalId: game.id.uuidString,
|
canonicalId: game.id,
|
||||||
homeTeamCanonicalId: game.homeTeamId.uuidString,
|
homeTeamCanonicalId: game.homeTeamId,
|
||||||
awayTeamCanonicalId: game.awayTeamId.uuidString,
|
awayTeamCanonicalId: game.awayTeamId,
|
||||||
stadiumCanonicalId: game.stadiumId.uuidString
|
stadiumCanonicalId: game.stadiumId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ struct ConcurrencyTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -50,10 +50,10 @@ struct ConcurrencyTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -73,7 +73,7 @@ struct ConcurrencyTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [String: Stadium]
|
||||||
) -> PlanningRequest {
|
) -> PlanningRequest {
|
||||||
let preferences = TripPreferences(
|
let preferences = TripPreferences(
|
||||||
planningMode: .dateRange,
|
planningMode: .dateRange,
|
||||||
@@ -107,8 +107,8 @@ struct ConcurrencyTests {
|
|||||||
|
|
||||||
let pair = cityPairs[requestIndex % cityPairs.count]
|
let pair = cityPairs[requestIndex % cityPairs.count]
|
||||||
|
|
||||||
let stadium1Id = UUID()
|
let stadium1Id = "stadium_1_\(UUID().uuidString)"
|
||||||
let stadium2Id = UUID()
|
let stadium2Id = "stadium_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let stadium1 = makeStadium(id: stadium1Id, city: pair.city1.0, lat: pair.city1.1, lon: pair.city1.2)
|
let stadium1 = makeStadium(id: stadium1Id, city: pair.city1.0, lat: pair.city1.1, lon: pair.city1.2)
|
||||||
let stadium2 = makeStadium(id: stadium2Id, city: pair.city2.0, lat: pair.city2.1, lon: pair.city2.2)
|
let stadium2 = makeStadium(id: stadium2Id, city: pair.city2.0, lat: pair.city2.1, lon: pair.city2.2)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct EdgeCaseTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
state: String = "ST",
|
state: String = "ST",
|
||||||
lat: Double,
|
lat: Double,
|
||||||
@@ -52,10 +52,10 @@ struct EdgeCaseTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -75,7 +75,7 @@ struct EdgeCaseTests {
|
|||||||
city: String,
|
city: String,
|
||||||
state: String = "ST",
|
state: String = "ST",
|
||||||
coordinate: CLLocationCoordinate2D? = nil,
|
coordinate: CLLocationCoordinate2D? = nil,
|
||||||
games: [UUID] = [],
|
games: [String] = [],
|
||||||
arrivalDate: Date = Date()
|
arrivalDate: Date = Date()
|
||||||
) -> ItineraryStop {
|
) -> ItineraryStop {
|
||||||
ItineraryStop(
|
ItineraryStop(
|
||||||
@@ -95,8 +95,8 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.1 - Nil stadium ID handled gracefully")
|
@Test("11.1 - Nil stadium ID handled gracefully")
|
||||||
func test_nilStadium_HandlesGracefully() {
|
func test_nilStadium_HandlesGracefully() {
|
||||||
// Setup: Create games where stadium lookup would return nil
|
// Setup: Create games where stadium lookup would return nil
|
||||||
let validStadiumId = UUID()
|
let validStadiumId = "stadium_valid_\(UUID().uuidString)"
|
||||||
let nonExistentStadiumId = UUID()
|
let nonExistentStadiumId = "stadium_nonexistent_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: validStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: validStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [validStadiumId: chicago]
|
let stadiums = [validStadiumId: chicago]
|
||||||
@@ -133,7 +133,7 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.2 - Malformed date handled gracefully")
|
@Test("11.2 - Malformed date handled gracefully")
|
||||||
func test_malformedDate_HandlesGracefully() {
|
func test_malformedDate_HandlesGracefully() {
|
||||||
// Setup: Create games with dates at extremes
|
// Setup: Create games with dates at extremes
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: chicago]
|
let stadiums = [stadiumId: chicago]
|
||||||
|
|
||||||
@@ -172,9 +172,9 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.3 - Invalid coordinates handled gracefully")
|
@Test("11.3 - Invalid coordinates handled gracefully")
|
||||||
func test_invalidCoordinates_HandlesGracefully() {
|
func test_invalidCoordinates_HandlesGracefully() {
|
||||||
// Setup: Create stadiums with invalid coordinates
|
// Setup: Create stadiums with invalid coordinates
|
||||||
let validId = UUID()
|
let validId = "stadium_valid_\(UUID().uuidString)"
|
||||||
let invalidLatId = UUID()
|
let invalidLatId = "stadium_invalidlat_\(UUID().uuidString)"
|
||||||
let invalidLonId = UUID()
|
let invalidLonId = "stadium_invalidlon_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Valid stadium
|
// Valid stadium
|
||||||
let validStadium = makeStadium(id: validId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let validStadium = makeStadium(id: validId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -239,7 +239,7 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.4 - Missing required fields handled gracefully")
|
@Test("11.4 - Missing required fields handled gracefully")
|
||||||
func test_missingRequiredFields_HandlesGracefully() {
|
func test_missingRequiredFields_HandlesGracefully() {
|
||||||
// Setup: Test with empty games array
|
// Setup: Test with empty games array
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: chicago]
|
let stadiums = [stadiumId: chicago]
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ struct EdgeCaseTests {
|
|||||||
|
|
||||||
// Test with empty stadiums dictionary
|
// Test with empty stadiums dictionary
|
||||||
let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19))
|
let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19))
|
||||||
let emptyStadiums: [UUID: Stadium] = [:]
|
let emptyStadiums: [String: Stadium] = [:]
|
||||||
|
|
||||||
let routes2 = GameDAGRouter.findRoutes(
|
let routes2 = GameDAGRouter.findRoutes(
|
||||||
games: [game],
|
games: [game],
|
||||||
@@ -271,9 +271,9 @@ struct EdgeCaseTests {
|
|||||||
|
|
||||||
// Test with mismatched team IDs (homeTeamId and awayTeamId don't exist)
|
// Test with mismatched team IDs (homeTeamId and awayTeamId don't exist)
|
||||||
let game2 = Game(
|
let game2 = Game(
|
||||||
id: UUID(),
|
id: "game_test_\(UUID().uuidString)",
|
||||||
homeTeamId: UUID(), // Non-existent team
|
homeTeamId: "team_nonexistent_\(UUID().uuidString)", // Non-existent team
|
||||||
awayTeamId: UUID(), // Non-existent team
|
awayTeamId: "team_nonexistent_\(UUID().uuidString)", // Non-existent team
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
dateTime: makeDate(day: 5, hour: 19),
|
dateTime: makeDate(day: 5, hour: 19),
|
||||||
sport: .mlb,
|
sport: .mlb,
|
||||||
@@ -298,8 +298,8 @@ struct EdgeCaseTests {
|
|||||||
// Default: 8 hours/day * 60 mph * 2 days = 960 miles max
|
// Default: 8 hours/day * 60 mph * 2 days = 960 miles max
|
||||||
// With 1.3 road factor, haversine distance should be 960/1.3 ≈ 738 miles
|
// With 1.3 road factor, haversine distance should be 960/1.3 ≈ 738 miles
|
||||||
|
|
||||||
let stadiumId1 = UUID()
|
let stadiumId1 = "stadium_1_\(UUID().uuidString)"
|
||||||
let stadiumId2 = UUID()
|
let stadiumId2 = "stadium_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
// NYC and Chicago are about 790 miles apart (haversine)
|
// NYC and Chicago are about 790 miles apart (haversine)
|
||||||
// With road factor 1.3, that's ~1027 road miles
|
// With road factor 1.3, that's ~1027 road miles
|
||||||
@@ -344,8 +344,8 @@ struct EdgeCaseTests {
|
|||||||
// NYC to LA is ~2,451 miles haversine, ~3,186 with road factor
|
// NYC to LA is ~2,451 miles haversine, ~3,186 with road factor
|
||||||
// At 60 mph, that's ~53 hours - way over 16 hour limit
|
// At 60 mph, that's ~53 hours - way over 16 hour limit
|
||||||
|
|
||||||
let stadiumId1 = UUID()
|
let stadiumId1 = "stadium_1_\(UUID().uuidString)"
|
||||||
let stadiumId2 = UUID()
|
let stadiumId2 = "stadium_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
||||||
@@ -398,7 +398,7 @@ struct EdgeCaseTests {
|
|||||||
// Calculate a point exactly 50 miles south (along a corridor)
|
// Calculate a point exactly 50 miles south (along a corridor)
|
||||||
// 1 degree of latitude ≈ 69 miles
|
// 1 degree of latitude ≈ 69 miles
|
||||||
// 50 miles ≈ 0.725 degrees
|
// 50 miles ≈ 0.725 degrees
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let exactlyAtBoundary = makeStadium(
|
let exactlyAtBoundary = makeStadium(
|
||||||
id: stadiumId,
|
id: stadiumId,
|
||||||
city: "BoundaryCity",
|
city: "BoundaryCity",
|
||||||
@@ -434,7 +434,7 @@ struct EdgeCaseTests {
|
|||||||
// Calculate a point 51 miles south (just outside the radius)
|
// Calculate a point 51 miles south (just outside the radius)
|
||||||
// 1 degree of latitude ≈ 69 miles
|
// 1 degree of latitude ≈ 69 miles
|
||||||
// 51 miles ≈ 0.739 degrees
|
// 51 miles ≈ 0.739 degrees
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let justOutsideBoundary = makeStadium(
|
let justOutsideBoundary = makeStadium(
|
||||||
id: stadiumId,
|
id: stadiumId,
|
||||||
city: "OutsideCity",
|
city: "OutsideCity",
|
||||||
@@ -464,8 +464,8 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.9 - Game in different time zone normalizes correctly")
|
@Test("11.9 - Game in different time zone normalizes correctly")
|
||||||
func test_gameInDifferentTimeZone_NormalizesToUTC() {
|
func test_gameInDifferentTimeZone_NormalizesToUTC() {
|
||||||
// Setup: Create games in different time zones
|
// Setup: Create games in different time zones
|
||||||
let stadiumId1 = UUID()
|
let stadiumId1 = "stadium_1_\(UUID().uuidString)"
|
||||||
let stadiumId2 = UUID()
|
let stadiumId2 = "stadium_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
||||||
@@ -508,7 +508,7 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.10 - DST spring forward handled correctly")
|
@Test("11.10 - DST spring forward handled correctly")
|
||||||
func test_dstSpringForward_HandlesCorrectly() {
|
func test_dstSpringForward_HandlesCorrectly() {
|
||||||
// Setup: Test around DST transition (Spring forward: March 8, 2026, 2 AM -> 3 AM)
|
// Setup: Test around DST transition (Spring forward: March 8, 2026, 2 AM -> 3 AM)
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: chicago]
|
let stadiums = [stadiumId: chicago]
|
||||||
|
|
||||||
@@ -553,7 +553,7 @@ struct EdgeCaseTests {
|
|||||||
@Test("11.11 - DST fall back handled correctly")
|
@Test("11.11 - DST fall back handled correctly")
|
||||||
func test_dstFallBack_HandlesCorrectly() {
|
func test_dstFallBack_HandlesCorrectly() {
|
||||||
// Setup: Test around DST transition (Fall back: November 1, 2026, 2 AM -> 1 AM)
|
// Setup: Test around DST transition (Fall back: November 1, 2026, 2 AM -> 1 AM)
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: chicago]
|
let stadiums = [stadiumId: chicago]
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
// Create a stadium at a known location
|
// Create a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double
|
lon: Double
|
||||||
@@ -48,14 +48,14 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
// Create a game at a stadium
|
// Create a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
dateTime: Date
|
dateTime: Date
|
||||||
) -> Game {
|
) -> Game {
|
||||||
Game(
|
Game(
|
||||||
id: id,
|
id: id,
|
||||||
homeTeamId: UUID(),
|
homeTeamId: "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID(),
|
awayTeamId: "team_test_\(UUID().uuidString)",
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
dateTime: dateTime,
|
dateTime: dateTime,
|
||||||
sport: .mlb,
|
sport: .mlb,
|
||||||
@@ -78,7 +78,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.2 - Single game returns single route")
|
@Test("2.2 - Single game returns single route")
|
||||||
func test_findRoutes_SingleGame_ReturnsSingleRoute() {
|
func test_findRoutes_SingleGame_ReturnsSingleRoute() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.3 - Single game with matching anchor returns single route")
|
@Test("2.3 - Single game with matching anchor returns single route")
|
||||||
func test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute() {
|
func test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.4 - Single game with non-matching anchor returns empty")
|
@Test("2.4 - Single game with non-matching anchor returns empty")
|
||||||
func test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty() {
|
func test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
let nonExistentAnchor = UUID()
|
let nonExistentAnchor = "stadium_nonexistent_\(UUID().uuidString)"
|
||||||
|
|
||||||
let routes = GameDAGRouter.findRoutes(
|
let routes = GameDAGRouter.findRoutes(
|
||||||
games: [game],
|
games: [game],
|
||||||
@@ -132,8 +132,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.5 - Two games with feasible transition returns both in order")
|
@Test("2.5 - Two games with feasible transition returns both in order")
|
||||||
func test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder() {
|
func test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder() {
|
||||||
// Chicago to Milwaukee is ~90 miles - easily feasible
|
// Chicago to Milwaukee is ~90 miles - easily feasible
|
||||||
let chicagoStadiumId = UUID()
|
let chicagoStadiumId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeStadiumId = UUID()
|
let milwaukeeStadiumId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicagoStadium = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicagoStadium = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukeeStadium = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukeeStadium = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -162,8 +162,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.6 - Two games with infeasible transition returns separate routes")
|
@Test("2.6 - Two games with infeasible transition returns separate routes")
|
||||||
func test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes() {
|
func test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes() {
|
||||||
// NYC to LA on same day is infeasible
|
// NYC to LA on same day is infeasible
|
||||||
let nycStadiumId = UUID()
|
let nycStadiumId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let laStadiumId = UUID()
|
let laStadiumId = "stadium_la_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
||||||
@@ -191,7 +191,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.7 - Two games same stadium same day (doubleheader) succeeds")
|
@Test("2.7 - Two games same stadium same day (doubleheader) succeeds")
|
||||||
func test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds() {
|
func test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
// Doubleheader: 1pm and 7pm same day, same stadium
|
// Doubleheader: 1pm and 7pm same day, same stadium
|
||||||
@@ -219,7 +219,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.8 - With anchors only returns routes containing all anchors")
|
@Test("2.8 - With anchors only returns routes containing all anchors")
|
||||||
func test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors() {
|
func test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
@@ -246,8 +246,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.9 - Impossible anchors returns empty")
|
@Test("2.9 - Impossible anchors returns empty")
|
||||||
func test_findRoutes_ImpossibleAnchors_ReturnsEmpty() {
|
func test_findRoutes_ImpossibleAnchors_ReturnsEmpty() {
|
||||||
// Two anchors at opposite ends of country on same day - impossible to attend both
|
// Two anchors at opposite ends of country on same day - impossible to attend both
|
||||||
let nycStadiumId = UUID()
|
let nycStadiumId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let laStadiumId = UUID()
|
let laStadiumId = "stadium_la_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
||||||
@@ -271,9 +271,9 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.10 - Multiple anchors route must contain all")
|
@Test("2.10 - Multiple anchors route must contain all")
|
||||||
func test_findRoutes_MultipleAnchors_RouteMustContainAll() {
|
func test_findRoutes_MultipleAnchors_RouteMustContainAll() {
|
||||||
// Three games in nearby cities over 3 days - all feasible
|
// Three games in nearby cities over 3 days - all feasible
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -306,7 +306,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.11 - Allow repeat cities same city multiple days allowed")
|
@Test("2.11 - Allow repeat cities same city multiple days allowed")
|
||||||
func test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed() {
|
func test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
// Three games in Chicago over 3 days
|
// Three games in Chicago over 3 days
|
||||||
@@ -330,8 +330,8 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.12 - Disallow repeat cities skips second visit")
|
@Test("2.12 - Disallow repeat cities skips second visit")
|
||||||
func test_findRoutes_DisallowRepeatCities_SkipsSecondVisit() {
|
func test_findRoutes_DisallowRepeatCities_SkipsSecondVisit() {
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -360,7 +360,7 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.13 - Disallow repeat cities only option is repeat overrides with warning")
|
@Test("2.13 - Disallow repeat cities only option is repeat overrides with warning")
|
||||||
func test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning() {
|
func test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning() {
|
||||||
// When only games available are in the same city, we still need to produce routes
|
// When only games available are in the same city, we still need to produce routes
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
// Only Chicago games available
|
// Only Chicago games available
|
||||||
@@ -388,8 +388,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.14 - Exceeds max daily driving transition rejected")
|
@Test("2.14 - Exceeds max daily driving transition rejected")
|
||||||
func test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected() {
|
func test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected() {
|
||||||
// NYC to Denver is ~1,800 miles, way over 8 hours of driving (480 miles at 60mph)
|
// NYC to Denver is ~1,800 miles, way over 8 hours of driving (480 miles at 60mph)
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let denverId = UUID()
|
let denverId = "stadium_denver_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let denver = makeStadium(id: denverId, city: "Denver", lat: 39.7392, lon: -104.9903)
|
let denver = makeStadium(id: denverId, city: "Denver", lat: 39.7392, lon: -104.9903)
|
||||||
@@ -417,8 +417,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.15 - Multi-day drive allowed if within daily limits")
|
@Test("2.15 - Multi-day drive allowed if within daily limits")
|
||||||
func test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits() {
|
func test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits() {
|
||||||
// NYC to Chicago is ~790 miles - doable over multiple days
|
// NYC to Chicago is ~790 miles - doable over multiple days
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -443,8 +443,8 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.16 - Same day different stadiums checks available time")
|
@Test("2.16 - Same day different stadiums checks available time")
|
||||||
func test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime() {
|
func test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime() {
|
||||||
// Chicago to Milwaukee is ~90 miles (~1.5 hours driving)
|
// Chicago to Milwaukee is ~90 miles (~1.5 hours driving)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -484,7 +484,7 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.17 - Max day lookahead respects limit")
|
@Test("2.17 - Max day lookahead respects limit")
|
||||||
func test_findRoutes_MaxDayLookahead_RespectsLimit() {
|
func test_findRoutes_MaxDayLookahead_RespectsLimit() {
|
||||||
// Games more than 5 days apart should not connect directly
|
// Games more than 5 days apart should not connect directly
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
@@ -514,7 +514,7 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.18 - DST transition handles correctly")
|
@Test("2.18 - DST transition handles correctly")
|
||||||
func test_findRoutes_DSTTransition_HandlesCorrectly() {
|
func test_findRoutes_DSTTransition_HandlesCorrectly() {
|
||||||
// Test around DST transition (March 9, 2026 - spring forward)
|
// Test around DST transition (March 9, 2026 - spring forward)
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
// Create dates around DST transition
|
// Create dates around DST transition
|
||||||
@@ -550,7 +550,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.19 - Midnight game assigns to correct day")
|
@Test("2.19 - Midnight game assigns to correct day")
|
||||||
func test_findRoutes_MidnightGame_AssignsToCorrectDay() {
|
func test_findRoutes_MidnightGame_AssignsToCorrectDay() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
// Game at 12:05 AM belongs to the new day
|
// Game at 12:05 AM belongs to the new day
|
||||||
@@ -583,7 +583,7 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.20 - Select diverse routes includes short and long trips")
|
@Test("2.20 - Select diverse routes includes short and long trips")
|
||||||
func test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented() {
|
func test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented() {
|
||||||
// Create a mix of games over a week
|
// Create a mix of games over a week
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
var games: [Game] = []
|
var games: [Game] = []
|
||||||
@@ -611,9 +611,9 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.21 - Select diverse routes includes high and low mileage")
|
@Test("2.21 - Select diverse routes includes high and low mileage")
|
||||||
func test_selectDiverseRoutes_HighAndLowMileage_BothRepresented() {
|
func test_selectDiverseRoutes_HighAndLowMileage_BothRepresented() {
|
||||||
// Create games in both nearby and distant cities
|
// Create games in both nearby and distant cities
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let laId = UUID()
|
let laId = "stadium_la_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -647,11 +647,11 @@ struct GameDAGRouterTests {
|
|||||||
("Cleveland", 41.4993, -81.6944),
|
("Cleveland", 41.4993, -81.6944),
|
||||||
]
|
]
|
||||||
|
|
||||||
var stadiums: [UUID: Stadium] = [:]
|
var stadiums: [String: Stadium] = [:]
|
||||||
var games: [Game] = []
|
var games: [Game] = []
|
||||||
|
|
||||||
for (index, city) in cities.enumerated() {
|
for (index, city) in cities.enumerated() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
stadiums[stadiumId] = makeStadium(id: stadiumId, city: city.0, lat: city.1, lon: city.2)
|
stadiums[stadiumId] = makeStadium(id: stadiumId, city: city.0, lat: city.1, lon: city.2)
|
||||||
games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: index + 1)))
|
games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: index + 1)))
|
||||||
}
|
}
|
||||||
@@ -675,7 +675,7 @@ struct GameDAGRouterTests {
|
|||||||
|
|
||||||
@Test("2.23 - Select diverse routes deduplicates")
|
@Test("2.23 - Select diverse routes deduplicates")
|
||||||
func test_selectDiverseRoutes_DuplicateRoutes_Deduplicated() {
|
func test_selectDiverseRoutes_DuplicateRoutes_Deduplicated() {
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
|
||||||
@@ -693,7 +693,7 @@ struct GameDAGRouterTests {
|
|||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
var seen = Set<String>()
|
var seen = Set<String>()
|
||||||
for route in routes {
|
for route in routes {
|
||||||
let key = route.map { $0.id.uuidString }.joined(separator: "-")
|
let key = route.map { $0.id }.joined(separator: "-")
|
||||||
#expect(!seen.contains(key), "Routes should be deduplicated")
|
#expect(!seen.contains(key), "Routes should be deduplicated")
|
||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
}
|
}
|
||||||
@@ -704,9 +704,9 @@ struct GameDAGRouterTests {
|
|||||||
@Test("2.24 - Graph with potential cycle handles silently")
|
@Test("2.24 - Graph with potential cycle handles silently")
|
||||||
func test_findRoutes_GraphWithPotentialCycle_HandlesSilently() {
|
func test_findRoutes_GraphWithPotentialCycle_HandlesSilently() {
|
||||||
// Create a scenario where a naive algorithm might get stuck in a loop
|
// Create a scenario where a naive algorithm might get stuck in a loop
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct ItineraryBuilderTests {
|
|||||||
@Test("Single stop creates itinerary with one stop and no travel segments")
|
@Test("Single stop creates itinerary with one stop and no travel segments")
|
||||||
func test_builder_SingleGame_CreatesSingleDay() {
|
func test_builder_SingleGame_CreatesSingleDay() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let gameId = UUID()
|
let gameId = "game_test_\(UUID().uuidString)"
|
||||||
let stop = makeItineraryStop(
|
let stop = makeItineraryStop(
|
||||||
city: "New York",
|
city: "New York",
|
||||||
state: "NY",
|
state: "NY",
|
||||||
@@ -60,9 +60,9 @@ struct ItineraryBuilderTests {
|
|||||||
func test_builder_MultiCity_CreatesTravelSegmentsBetween() {
|
func test_builder_MultiCity_CreatesTravelSegmentsBetween() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let stops = [
|
let stops = [
|
||||||
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
|
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]),
|
||||||
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"]),
|
||||||
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
|
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"])
|
||||||
]
|
]
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -103,8 +103,8 @@ struct ItineraryBuilderTests {
|
|||||||
func test_builder_SameCity_MultipleGames_GroupsOnSameDay() {
|
func test_builder_SameCity_MultipleGames_GroupsOnSameDay() {
|
||||||
// Arrange - Two stops in the same city (different games, same location)
|
// Arrange - Two stops in the same city (different games, same location)
|
||||||
let stops = [
|
let stops = [
|
||||||
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_1_\(UUID().uuidString)"]),
|
||||||
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()])
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_2_\(UUID().uuidString)"])
|
||||||
]
|
]
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -141,8 +141,8 @@ struct ItineraryBuilderTests {
|
|||||||
// Boston to Chicago is ~850 miles haversine, ~1100 with road factor
|
// Boston to Chicago is ~850 miles haversine, ~1100 with road factor
|
||||||
// At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3)
|
// At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3)
|
||||||
let stops = [
|
let stops = [
|
||||||
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
|
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]),
|
||||||
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
|
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"])
|
||||||
]
|
]
|
||||||
|
|
||||||
// Use constraints that allow long trips
|
// Use constraints that allow long trips
|
||||||
@@ -192,7 +192,7 @@ struct ItineraryBuilderTests {
|
|||||||
city: "Boston",
|
city: "Boston",
|
||||||
state: "MA",
|
state: "MA",
|
||||||
coordinate: boston,
|
coordinate: boston,
|
||||||
games: [UUID()],
|
games: ["game_boston_\(UUID().uuidString)"],
|
||||||
departureDate: now
|
departureDate: now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ struct ItineraryBuilderTests {
|
|||||||
city: "New York",
|
city: "New York",
|
||||||
state: "NY",
|
state: "NY",
|
||||||
coordinate: nyc,
|
coordinate: nyc,
|
||||||
games: [UUID()],
|
games: ["game_nyc_\(UUID().uuidString)"],
|
||||||
firstGameStart: gameStartSoon
|
firstGameStart: gameStartSoon
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ struct ItineraryBuilderTests {
|
|||||||
city: "Boston",
|
city: "Boston",
|
||||||
state: "MA",
|
state: "MA",
|
||||||
coordinate: boston,
|
coordinate: boston,
|
||||||
games: [UUID()],
|
games: ["game_boston_\(UUID().uuidString)"],
|
||||||
departureDate: now
|
departureDate: now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ struct ItineraryBuilderTests {
|
|||||||
city: "New York",
|
city: "New York",
|
||||||
state: "NY",
|
state: "NY",
|
||||||
coordinate: nyc,
|
coordinate: nyc,
|
||||||
games: [UUID()],
|
games: ["game_nyc_\(UUID().uuidString)"],
|
||||||
firstGameStart: gameLater
|
firstGameStart: gameLater
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ struct ItineraryBuilderTests {
|
|||||||
city: String,
|
city: String,
|
||||||
state: String,
|
state: String,
|
||||||
coordinate: CLLocationCoordinate2D? = nil,
|
coordinate: CLLocationCoordinate2D? = nil,
|
||||||
games: [UUID] = [],
|
games: [String] = [],
|
||||||
arrivalDate: Date = Date(),
|
arrivalDate: Date = Date(),
|
||||||
departureDate: Date? = nil,
|
departureDate: Date? = nil,
|
||||||
firstGameStart: Date? = nil
|
firstGameStart: Date? = nil
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ struct RouteFiltersTests {
|
|||||||
coordinate: nil,
|
coordinate: nil,
|
||||||
arrivalDate: startDate,
|
arrivalDate: startDate,
|
||||||
departureDate: end,
|
departureDate: end,
|
||||||
games: [UUID()],
|
games: ["game_test_\(UUID().uuidString)"],
|
||||||
stadium: UUID()
|
stadium: "stadium_test_\(UUID().uuidString)"
|
||||||
)
|
)
|
||||||
|
|
||||||
return Trip(
|
return Trip(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ScenarioAPlannerTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -51,10 +51,10 @@ struct ScenarioAPlannerTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -74,8 +74,8 @@ struct ScenarioAPlannerTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team] = [:],
|
teams: [String: Team] = [:],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
maxDrivingHoursPerDriver: Double = 8.0,
|
maxDrivingHoursPerDriver: Double = 8.0,
|
||||||
@@ -106,9 +106,9 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.1 - Valid date range returns games in range")
|
@Test("4.1 - Valid date range returns games in range")
|
||||||
func test_planByDates_ValidDateRange_ReturnsGamesInRange() {
|
func test_planByDates_ValidDateRange_ReturnsGamesInRange() {
|
||||||
// Setup: 3 games across nearby cities over 5 days
|
// Setup: 3 games across nearby cities over 5 days
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -144,7 +144,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.2 - Single day range returns games on that day")
|
@Test("4.2 - Single day range returns games on that day")
|
||||||
func test_planByDates_SingleDayRange_ReturnsGamesOnThatDay() {
|
func test_planByDates_SingleDayRange_ReturnsGamesOnThatDay() {
|
||||||
// Setup: Multiple games on a single day at the same stadium
|
// Setup: Multiple games on a single day at the same stadium
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
@@ -182,10 +182,10 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.3 - Multi-week range returns multiple games")
|
@Test("4.3 - Multi-week range returns multiple games")
|
||||||
func test_planByDates_MultiWeekRange_ReturnsMultipleGames() {
|
func test_planByDates_MultiWeekRange_ReturnsMultipleGames() {
|
||||||
// Setup: Games spread across 3 weeks in nearby cities
|
// Setup: Games spread across 3 weeks in nearby cities
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
let clevelandId = UUID()
|
let clevelandId = "stadium_cleveland_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -230,7 +230,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.4 - No games in range returns failure")
|
@Test("4.4 - No games in range returns failure")
|
||||||
func test_planByDates_NoGamesInRange_ThrowsError() {
|
func test_planByDates_NoGamesInRange_ThrowsError() {
|
||||||
// Setup: Games outside the requested date range
|
// Setup: Games outside the requested date range
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.5 - End date before start date returns failure")
|
@Test("4.5 - End date before start date returns failure")
|
||||||
func test_planByDates_EndDateBeforeStartDate_ThrowsError() {
|
func test_planByDates_EndDateBeforeStartDate_ThrowsError() {
|
||||||
// Setup: Invalid date range where end < start
|
// Setup: Invalid date range where end < start
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.6 - Single game in range returns single game route")
|
@Test("4.6 - Single game in range returns single game route")
|
||||||
func test_planByDates_SingleGameInRange_ReturnsSingleGameRoute() {
|
func test_planByDates_SingleGameInRange_ReturnsSingleGameRoute() {
|
||||||
// Setup: Only one game in the date range
|
// Setup: Only one game in the date range
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
@@ -354,9 +354,9 @@ struct ScenarioAPlannerTests {
|
|||||||
func test_planByDates_UsesDAGRouterForRouting() {
|
func test_planByDates_UsesDAGRouterForRouting() {
|
||||||
// Setup: Games that require DAG routing logic
|
// Setup: Games that require DAG routing logic
|
||||||
// Create games in multiple cities with feasible transitions
|
// Create games in multiple cities with feasible transitions
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -403,8 +403,8 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.9 - Respects driver constraints")
|
@Test("4.9 - Respects driver constraints")
|
||||||
func test_planByDates_RespectsDriverConstraints() {
|
func test_planByDates_RespectsDriverConstraints() {
|
||||||
// Setup: Games that would require excessive daily driving if constraints are loose
|
// Setup: Games that would require excessive daily driving if constraints are loose
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
|
|
||||||
// NYC to Chicago is ~790 miles (~13 hours of driving)
|
// NYC to Chicago is ~790 miles (~13 hours of driving)
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -474,9 +474,9 @@ struct ScenarioAPlannerTests {
|
|||||||
func test_planByDates_MustStop_FiltersToGamesInCity() {
|
func test_planByDates_MustStop_FiltersToGamesInCity() {
|
||||||
// Setup: Games in Chicago, Milwaukee, Detroit
|
// Setup: Games in Chicago, Milwaukee, Detroit
|
||||||
// Must-stop = Chicago → should only return Chicago games
|
// Must-stop = Chicago → should only return Chicago games
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -518,8 +518,8 @@ struct ScenarioAPlannerTests {
|
|||||||
func test_planByDates_MustStop_NoMatchingGames_ReturnsFailure() {
|
func test_planByDates_MustStop_NoMatchingGames_ReturnsFailure() {
|
||||||
// Setup: Games only in Milwaukee and Detroit
|
// Setup: Games only in Milwaukee and Detroit
|
||||||
// Must-stop = Chicago → no games there, should fail
|
// Must-stop = Chicago → no games there, should fail
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
|
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
|
||||||
@@ -553,10 +553,10 @@ struct ScenarioAPlannerTests {
|
|||||||
// Setup: Cubs home game in Chicago + Cubs away game in Milwaukee (playing at Milwaukee)
|
// Setup: Cubs home game in Chicago + Cubs away game in Milwaukee (playing at Milwaukee)
|
||||||
// Must-stop = Chicago → should ONLY return the Chicago home game
|
// Must-stop = Chicago → should ONLY return the Chicago home game
|
||||||
// This tests Issue #8: "Must stop needs to be home team"
|
// This tests Issue #8: "Must stop needs to be home team"
|
||||||
let chicagoStadiumId = UUID()
|
let chicagoStadiumId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeStadiumId = UUID()
|
let milwaukeeStadiumId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let cubsTeamId = UUID()
|
let cubsTeamId = "team_cubs_\(UUID().uuidString)"
|
||||||
let brewersTeamId = UUID()
|
let brewersTeamId = "team_brewers_\(UUID().uuidString)"
|
||||||
|
|
||||||
let wrigleyField = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.9484, lon: -87.6553)
|
let wrigleyField = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.9484, lon: -87.6553)
|
||||||
let millerPark = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0280, lon: -87.9712)
|
let millerPark = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0280, lon: -87.9712)
|
||||||
@@ -608,7 +608,7 @@ struct ScenarioAPlannerTests {
|
|||||||
func test_planByDates_MustStop_PartialCityMatch_Works() {
|
func test_planByDates_MustStop_PartialCityMatch_Works() {
|
||||||
// Setup: User types "Chicago" but stadium city is "Chicago, IL"
|
// Setup: User types "Chicago" but stadium city is "Chicago, IL"
|
||||||
// Should still match via contains
|
// Should still match via contains
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [chicagoId: chicago]
|
let stadiums = [chicagoId: chicago]
|
||||||
|
|
||||||
@@ -636,7 +636,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.14 - Must-stop case insensitive")
|
@Test("4.14 - Must-stop case insensitive")
|
||||||
func test_planByDates_MustStop_CaseInsensitive() {
|
func test_planByDates_MustStop_CaseInsensitive() {
|
||||||
// Setup: Must-stop = "CHICAGO" (uppercase) should match "Chicago"
|
// Setup: Must-stop = "CHICAGO" (uppercase) should match "Chicago"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [chicagoId: chicago]
|
let stadiums = [chicagoId: chicago]
|
||||||
|
|
||||||
@@ -662,7 +662,7 @@ struct ScenarioAPlannerTests {
|
|||||||
@Test("4.15 - Multiple games in must-stop city all included")
|
@Test("4.15 - Multiple games in must-stop city all included")
|
||||||
func test_planByDates_MustStop_MultipleGamesInCity_AllIncluded() {
|
func test_planByDates_MustStop_MultipleGamesInCity_AllIncluded() {
|
||||||
// Setup: Multiple games in Chicago on different days
|
// Setup: Multiple games in Chicago on different days
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [chicagoId: chicago]
|
let stadiums = [chicagoId: chicago]
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ScenarioBPlannerTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -51,10 +51,10 @@ struct ScenarioBPlannerTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -74,9 +74,9 @@ struct ScenarioBPlannerTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
allGames: [Game],
|
allGames: [Game],
|
||||||
mustSeeGameIds: Set<UUID>,
|
mustSeeGameIds: Set<String>,
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team] = [:],
|
teams: [String: Team] = [:],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
maxDrivingHoursPerDriver: Double = 8.0
|
maxDrivingHoursPerDriver: Double = 8.0
|
||||||
@@ -106,11 +106,11 @@ struct ScenarioBPlannerTests {
|
|||||||
@Test("5.1 - Single must-see game returns trip with that game")
|
@Test("5.1 - Single must-see game returns trip with that game")
|
||||||
func test_mustSeeGames_SingleGame_ReturnsTripWithThatGame() {
|
func test_mustSeeGames_SingleGame_ReturnsTripWithThatGame() {
|
||||||
// Setup: Single must-see game
|
// Setup: Single must-see game
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let gameId = UUID()
|
let gameId = "game_test_\(UUID().uuidString)"
|
||||||
let game = makeGame(id: gameId, stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19))
|
let game = makeGame(id: gameId, stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19))
|
||||||
|
|
||||||
let request = makePlanningRequest(
|
let request = makePlanningRequest(
|
||||||
@@ -139,9 +139,9 @@ struct ScenarioBPlannerTests {
|
|||||||
func test_mustSeeGames_MultipleGames_ReturnsOptimalRoute() {
|
func test_mustSeeGames_MultipleGames_ReturnsOptimalRoute() {
|
||||||
// Setup: 3 must-see games in nearby cities (all Central region for single-region search)
|
// Setup: 3 must-see games in nearby cities (all Central region for single-region search)
|
||||||
// Region boundary: Central is -110 to -85 longitude
|
// Region boundary: Central is -110 to -85 longitude
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let stLouisId = UUID()
|
let stLouisId = "stadium_stlouis_\(UUID().uuidString)"
|
||||||
|
|
||||||
// All cities in Central region (longitude between -110 and -85)
|
// All cities in Central region (longitude between -110 and -85)
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -150,9 +150,9 @@ struct ScenarioBPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, stLouisId: stLouis]
|
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, stLouisId: stLouis]
|
||||||
|
|
||||||
let game1Id = UUID()
|
let game1Id = "game_test_1_\(UUID().uuidString)"
|
||||||
let game2Id = UUID()
|
let game2Id = "game_test_2_\(UUID().uuidString)"
|
||||||
let game3Id = UUID()
|
let game3Id = "game_test_3_\(UUID().uuidString)"
|
||||||
|
|
||||||
let game1 = makeGame(id: game1Id, stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
|
let game1 = makeGame(id: game1Id, stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19))
|
||||||
let game2 = makeGame(id: game2Id, stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
|
let game2 = makeGame(id: game2Id, stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19))
|
||||||
@@ -187,8 +187,8 @@ struct ScenarioBPlannerTests {
|
|||||||
@Test("5.3 - Games in different cities are connected")
|
@Test("5.3 - Games in different cities are connected")
|
||||||
func test_mustSeeGames_GamesInDifferentCities_ConnectsThem() {
|
func test_mustSeeGames_GamesInDifferentCities_ConnectsThem() {
|
||||||
// Setup: 2 must-see games in distant but reachable cities
|
// Setup: 2 must-see games in distant but reachable cities
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
|
|
||||||
// NYC to Boston is ~215 miles (~4 hours driving)
|
// NYC to Boston is ~215 miles (~4 hours driving)
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -196,8 +196,8 @@ struct ScenarioBPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [nycId: nyc, bostonId: boston]
|
let stadiums = [nycId: nyc, bostonId: boston]
|
||||||
|
|
||||||
let game1Id = UUID()
|
let game1Id = "game_test_1_\(UUID().uuidString)"
|
||||||
let game2Id = UUID()
|
let game2Id = "game_test_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Games 2 days apart - plenty of time to drive
|
// Games 2 days apart - plenty of time to drive
|
||||||
let game1 = makeGame(id: game1Id, stadiumId: nycId, dateTime: makeDate(day: 5, hour: 19))
|
let game1 = makeGame(id: game1Id, stadiumId: nycId, dateTime: makeDate(day: 5, hour: 19))
|
||||||
@@ -238,7 +238,7 @@ struct ScenarioBPlannerTests {
|
|||||||
@Test("5.4 - Empty selection returns failure")
|
@Test("5.4 - Empty selection returns failure")
|
||||||
func test_mustSeeGames_EmptySelection_ThrowsError() {
|
func test_mustSeeGames_EmptySelection_ThrowsError() {
|
||||||
// Setup: No must-see games selected
|
// Setup: No must-see games selected
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
@@ -266,8 +266,8 @@ struct ScenarioBPlannerTests {
|
|||||||
func test_mustSeeGames_ImpossibleToConnect_ThrowsError() {
|
func test_mustSeeGames_ImpossibleToConnect_ThrowsError() {
|
||||||
// Setup: Games on same day in cities ~850 miles apart (impossible in 8 hours)
|
// Setup: Games on same day in cities ~850 miles apart (impossible in 8 hours)
|
||||||
// Both cities in East region (> -85 longitude) so regional search covers both
|
// Both cities in East region (> -85 longitude) so regional search covers both
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let atlantaId = UUID()
|
let atlantaId = "stadium_atlanta_\(UUID().uuidString)"
|
||||||
|
|
||||||
// NYC to Atlanta is ~850 miles (~13 hours driving) - impossible with 8-hour limit
|
// NYC to Atlanta is ~850 miles (~13 hours driving) - impossible with 8-hour limit
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -275,8 +275,8 @@ struct ScenarioBPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [nycId: nyc, atlantaId: atlanta]
|
let stadiums = [nycId: nyc, atlantaId: atlanta]
|
||||||
|
|
||||||
let game1Id = UUID()
|
let game1Id = "game_test_1_\(UUID().uuidString)"
|
||||||
let game2Id = UUID()
|
let game2Id = "game_test_2_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Same day games 6 hours apart - even if you left right after game 1,
|
// Same day games 6 hours apart - even if you left right after game 1,
|
||||||
// you can't drive 850 miles in 6 hours with 8-hour daily limit
|
// you can't drive 850 miles in 6 hours with 8-hour daily limit
|
||||||
@@ -409,10 +409,10 @@ struct ScenarioBPlannerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build coordinate map for brute force verification
|
// Build coordinate map for brute force verification
|
||||||
var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:]
|
var stopCoordinates: [String: CLLocationCoordinate2D] = [:]
|
||||||
for stop in firstOption.stops {
|
for stop in firstOption.stops {
|
||||||
if let coord = stop.coordinate {
|
if let coord = stop.coordinate {
|
||||||
stopCoordinates[stop.id] = coord
|
stopCoordinates[stop.id.uuidString] = coord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ struct ScenarioBPlannerTests {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopIds = firstOption.stops.map { $0.id }
|
let stopIds = firstOption.stops.map { $0.id.uuidString }
|
||||||
let verificationResult = BruteForceRouteVerifier.verify(
|
let verificationResult = BruteForceRouteVerifier.verify(
|
||||||
proposedRoute: stopIds,
|
proposedRoute: stopIds,
|
||||||
stops: stopCoordinates,
|
stops: stopCoordinates,
|
||||||
@@ -471,17 +471,17 @@ struct ScenarioBPlannerTests {
|
|||||||
#expect(!includedMustSee.isEmpty, "Should include some must-see games")
|
#expect(!includedMustSee.isEmpty, "Should include some must-see games")
|
||||||
|
|
||||||
// Build coordinate map
|
// Build coordinate map
|
||||||
var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:]
|
var stopCoordinates: [String: CLLocationCoordinate2D] = [:]
|
||||||
for stop in firstOption.stops {
|
for stop in firstOption.stops {
|
||||||
if let coord = stop.coordinate {
|
if let coord = stop.coordinate {
|
||||||
stopCoordinates[stop.id] = coord
|
stopCoordinates[stop.id.uuidString] = coord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that there's no obviously better route (10% threshold)
|
// Check that there's no obviously better route (10% threshold)
|
||||||
guard stopCoordinates.count >= 2 else { return }
|
guard stopCoordinates.count >= 2 else { return }
|
||||||
|
|
||||||
let stopIds = firstOption.stops.map { $0.id }
|
let stopIds = firstOption.stops.map { $0.id.uuidString }
|
||||||
let (hasBetter, improvement) = BruteForceRouteVerifier.hasObviouslyBetterRoute(
|
let (hasBetter, improvement) = BruteForceRouteVerifier.hasObviouslyBetterRoute(
|
||||||
proposedRoute: stopIds,
|
proposedRoute: stopIds,
|
||||||
stops: stopCoordinates,
|
stops: stopCoordinates,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct ScenarioCPlannerTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
state: String = "ST",
|
state: String = "ST",
|
||||||
lat: Double,
|
lat: Double,
|
||||||
@@ -53,10 +53,10 @@ struct ScenarioCPlannerTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -91,8 +91,8 @@ struct ScenarioCPlannerTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
allGames: [Game],
|
allGames: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team] = [:],
|
teams: [String: Team] = [:],
|
||||||
mustStopLocations: [LocationInput] = [],
|
mustStopLocations: [LocationInput] = [],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
@@ -127,8 +127,8 @@ struct ScenarioCPlannerTests {
|
|||||||
func test_departReturn_SameCity_ReturnsRoundTrip() {
|
func test_departReturn_SameCity_ReturnsRoundTrip() {
|
||||||
// Setup: Start and end in Chicago
|
// Setup: Start and end in Chicago
|
||||||
// Create stadiums in Chicago and a nearby city (Milwaukee) for games along the route
|
// Create stadiums in Chicago and a nearby city (Milwaukee) for games along the route
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -169,10 +169,10 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.2 - Different cities creates one-way route")
|
@Test("6.2 - Different cities creates one-way route")
|
||||||
func test_departReturn_DifferentCities_ReturnsOneWayRoute() {
|
func test_departReturn_DifferentCities_ReturnsOneWayRoute() {
|
||||||
// Setup: Boston to Washington DC corridor (East Coast)
|
// Setup: Boston to Washington DC corridor (East Coast)
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let phillyId = UUID()
|
let phillyId = "stadium_philly_\(UUID().uuidString)"
|
||||||
let dcId = UUID()
|
let dcId = "stadium_dc_\(UUID().uuidString)"
|
||||||
|
|
||||||
// East Coast corridor from north to south
|
// East Coast corridor from north to south
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", state: "MA", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", state: "MA", lat: 42.3601, lon: -71.0589)
|
||||||
@@ -220,10 +220,10 @@ struct ScenarioCPlannerTests {
|
|||||||
func test_departReturn_GamesAlongCorridor_IncludesNearbyGames() {
|
func test_departReturn_GamesAlongCorridor_IncludesNearbyGames() {
|
||||||
// Setup: Chicago to St. Louis corridor
|
// Setup: Chicago to St. Louis corridor
|
||||||
// Include games that are "along the way" (directional)
|
// Include games that are "along the way" (directional)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let springfieldId = UUID()
|
let springfieldId = "stadium_springfield_\(UUID().uuidString)"
|
||||||
let stLouisId = UUID()
|
let stLouisId = "stadium_stlouis_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID() // This is NOT along the route (north of Chicago)
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" // This is NOT along the route (north of Chicago)
|
||||||
|
|
||||||
// Chicago to St. Louis is ~300 miles south
|
// Chicago to St. Louis is ~300 miles south
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -280,9 +280,9 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.4 - No games along route returns failure")
|
@Test("6.4 - No games along route returns failure")
|
||||||
func test_departReturn_NoGamesAlongRoute_ThrowsError() {
|
func test_departReturn_NoGamesAlongRoute_ThrowsError() {
|
||||||
// Setup: Start/end cities have no games
|
// Setup: Start/end cities have no games
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let stLouisId = UUID()
|
let stLouisId = "stadium_stlouis_\(UUID().uuidString)"
|
||||||
let seattleId = UUID() // Games here, but not along Chicago-St. Louis route
|
let seattleId = "stadium_seattle_\(UUID().uuidString)" // Games here, but not along Chicago-St. Louis route
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stLouis = makeStadium(id: stLouisId, city: "St. Louis", lat: 38.6270, lon: -90.1994)
|
let stLouis = makeStadium(id: stLouisId, city: "St. Louis", lat: 38.6270, lon: -90.1994)
|
||||||
@@ -325,7 +325,7 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.5 - Invalid city (no stadiums) returns failure")
|
@Test("6.5 - Invalid city (no stadiums) returns failure")
|
||||||
func test_departReturn_InvalidCity_ThrowsError() {
|
func test_departReturn_InvalidCity_ThrowsError() {
|
||||||
// Setup: Start location is a city with no stadium
|
// Setup: Start location is a city with no stadium
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [chicagoId: chicago]
|
let stadiums = [chicagoId: chicago]
|
||||||
|
|
||||||
@@ -357,10 +357,10 @@ struct ScenarioCPlannerTests {
|
|||||||
func test_departReturn_ExtremeDistance_RespectsConstraints() {
|
func test_departReturn_ExtremeDistance_RespectsConstraints() {
|
||||||
// Setup: NYC to LA route (~2,800 miles)
|
// Setup: NYC to LA route (~2,800 miles)
|
||||||
// With 8 hours/day at 60 mph = 480 miles/day, this takes ~6 days just driving
|
// With 8 hours/day at 60 mph = 480 miles/day, this takes ~6 days just driving
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let laId = UUID()
|
let laId = "stadium_la_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID() // Along the route
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)" // Along the route
|
||||||
let denverID = UUID() // Along the route
|
let denverID = "stadium_denver_\(UUID().uuidString)" // Along the route
|
||||||
|
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
|
||||||
@@ -426,9 +426,9 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.7 - Must-stop location is included in route")
|
@Test("6.7 - Must-stop location is included in route")
|
||||||
func test_departReturn_WithMustStopLocation_IncludesStop() {
|
func test_departReturn_WithMustStopLocation_IncludesStop() {
|
||||||
// Setup: Boston to DC with must-stop in Philadelphia
|
// Setup: Boston to DC with must-stop in Philadelphia
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
let phillyId = UUID()
|
let phillyId = "stadium_philly_\(UUID().uuidString)"
|
||||||
let dcId = UUID()
|
let dcId = "stadium_dc_\(UUID().uuidString)"
|
||||||
|
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
||||||
let philly = makeStadium(id: phillyId, city: "Philadelphia", lat: 39.9526, lon: -75.1652)
|
let philly = makeStadium(id: phillyId, city: "Philadelphia", lat: 39.9526, lon: -75.1652)
|
||||||
@@ -476,8 +476,8 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.8 - Must-stop with no nearby games is still included")
|
@Test("6.8 - Must-stop with no nearby games is still included")
|
||||||
func test_departReturn_MustStopNoNearbyGames_IncludesStopAnyway() {
|
func test_departReturn_MustStopNoNearbyGames_IncludesStopAnyway() {
|
||||||
// Setup: Boston to DC with must-stop in a city without games
|
// Setup: Boston to DC with must-stop in a city without games
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
let dcId = UUID()
|
let dcId = "stadium_dc_\(UUID().uuidString)"
|
||||||
|
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
||||||
let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369)
|
let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369)
|
||||||
@@ -528,10 +528,10 @@ struct ScenarioCPlannerTests {
|
|||||||
@Test("6.9 - Multiple must-stops are all included")
|
@Test("6.9 - Multiple must-stops are all included")
|
||||||
func test_departReturn_MultipleMustStops_AllIncluded() {
|
func test_departReturn_MultipleMustStops_AllIncluded() {
|
||||||
// Setup: Boston to DC with must-stops in NYC and Philadelphia
|
// Setup: Boston to DC with must-stops in NYC and Philadelphia
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let phillyId = UUID()
|
let phillyId = "stadium_philly_\(UUID().uuidString)"
|
||||||
let dcId = UUID()
|
let dcId = "stadium_dc_\(UUID().uuidString)"
|
||||||
|
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -588,10 +588,10 @@ struct ScenarioCPlannerTests {
|
|||||||
func test_departReturn_MustStopConflictsWithRoute_FindsCompromise() {
|
func test_departReturn_MustStopConflictsWithRoute_FindsCompromise() {
|
||||||
// Setup: Boston to DC with must-stop that's slightly off the optimal route
|
// Setup: Boston to DC with must-stop that's slightly off the optimal route
|
||||||
// Cleveland is west of the Boston-DC corridor but could be included with detour
|
// Cleveland is west of the Boston-DC corridor but could be included with detour
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
let dcId = UUID()
|
let dcId = "stadium_dc_\(UUID().uuidString)"
|
||||||
let clevelandId = UUID()
|
let clevelandId = "stadium_cleveland_\(UUID().uuidString)"
|
||||||
let pittsburghId = UUID()
|
let pittsburghId = "stadium_pittsburgh_\(UUID().uuidString)"
|
||||||
|
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
||||||
let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369)
|
let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -51,9 +51,9 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
/// Creates a team
|
/// Creates a team
|
||||||
private func makeTeam(
|
private func makeTeam(
|
||||||
id: UUID = UUID(),
|
id: String = "team_test_\(UUID().uuidString)",
|
||||||
name: String,
|
name: String,
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Team {
|
) -> Team {
|
||||||
Team(
|
Team(
|
||||||
@@ -71,10 +71,10 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID,
|
homeTeamId: String,
|
||||||
awayTeamId: UUID,
|
awayTeamId: String,
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -93,10 +93,10 @@ struct ScenarioDPlannerTests {
|
|||||||
private func makePlanningRequest(
|
private func makePlanningRequest(
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
followTeamId: UUID?,
|
followTeamId: String?,
|
||||||
allGames: [Game],
|
allGames: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
teams: [UUID: Team] = [:],
|
teams: [String: Team] = [:],
|
||||||
selectedRegions: Set<Region> = [],
|
selectedRegions: Set<Region> = [],
|
||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
useHomeLocation: Bool = false,
|
useHomeLocation: Bool = false,
|
||||||
@@ -132,12 +132,12 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.1.1 - Single team with home games returns trip with those games")
|
@Test("D.1.1 - Single team with home games returns trip with those games")
|
||||||
func test_followTeam_HomeGames_ReturnsTrip() {
|
func test_followTeam_HomeGames_ReturnsTrip() {
|
||||||
// Setup: Team with 2 home games
|
// Setup: Team with 2 home games
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
let team = makeTeam(id: teamId, name: "Chicago Cubs", stadiumId: stadiumId)
|
let team = makeTeam(id: teamId, name: "Chicago Cubs", stadiumId: stadiumId)
|
||||||
|
|
||||||
let game1 = makeGame(
|
let game1 = makeGame(
|
||||||
@@ -179,8 +179,8 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.1.2 - Team with away games includes those games")
|
@Test("D.1.2 - Team with away games includes those games")
|
||||||
func test_followTeam_AwayGames_IncludesAwayGames() {
|
func test_followTeam_AwayGames_IncludesAwayGames() {
|
||||||
// Setup: Team with one home game and one away game (2 cities for simpler route)
|
// Setup: Team with one home game and one away game (2 cities for simpler route)
|
||||||
let homeStadiumId = UUID()
|
let homeStadiumId = "stadium_home_\(UUID().uuidString)"
|
||||||
let awayStadiumId = UUID()
|
let awayStadiumId = "stadium_away_\(UUID().uuidString)"
|
||||||
|
|
||||||
let homeStadium = makeStadium(id: homeStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let homeStadium = makeStadium(id: homeStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let awayStadium = makeStadium(id: awayStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let awayStadium = makeStadium(id: awayStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -190,8 +190,8 @@ struct ScenarioDPlannerTests {
|
|||||||
awayStadiumId: awayStadium
|
awayStadiumId: awayStadium
|
||||||
]
|
]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Home game
|
// Home game
|
||||||
let homeGame = makeGame(
|
let homeGame = makeGame(
|
||||||
@@ -237,8 +237,8 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.1.3 - Team games filtered by selected regions")
|
@Test("D.1.3 - Team games filtered by selected regions")
|
||||||
func test_followTeam_RegionFilter_FiltersGames() {
|
func test_followTeam_RegionFilter_FiltersGames() {
|
||||||
// Setup: Team with games in multiple regions
|
// Setup: Team with games in multiple regions
|
||||||
let eastStadiumId = UUID()
|
let eastStadiumId = "stadium_east_\(UUID().uuidString)"
|
||||||
let centralStadiumId = UUID()
|
let centralStadiumId = "stadium_central_\(UUID().uuidString)"
|
||||||
|
|
||||||
// East region (> -85 longitude)
|
// East region (> -85 longitude)
|
||||||
let eastStadium = makeStadium(id: eastStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let eastStadium = makeStadium(id: eastStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -247,8 +247,8 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [eastStadiumId: eastStadium, centralStadiumId: centralStadium]
|
let stadiums = [eastStadiumId: eastStadium, centralStadiumId: centralStadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
|
|
||||||
let eastGame = makeGame(
|
let eastGame = makeGame(
|
||||||
stadiumId: eastStadiumId,
|
stadiumId: eastStadiumId,
|
||||||
@@ -292,14 +292,14 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.2.1 - No team selected returns missingTeamSelection failure")
|
@Test("D.2.1 - No team selected returns missingTeamSelection failure")
|
||||||
func test_followTeam_NoTeamSelected_ReturnsMissingTeamSelection() {
|
func test_followTeam_NoTeamSelected_ReturnsMissingTeamSelection() {
|
||||||
// Setup: No team ID
|
// Setup: No team ID
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let game = makeGame(
|
let game = makeGame(
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
homeTeamId: UUID(),
|
homeTeamId: "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID(),
|
awayTeamId: "team_opponent_\(UUID().uuidString)",
|
||||||
dateTime: makeDate(day: 5, hour: 19)
|
dateTime: makeDate(day: 5, hour: 19)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,17 +323,17 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.2.2 - Team with no games in date range returns noGamesInRange failure")
|
@Test("D.2.2 - Team with no games in date range returns noGamesInRange failure")
|
||||||
func test_followTeam_NoGamesInRange_ReturnsNoGamesFailure() {
|
func test_followTeam_NoGamesInRange_ReturnsNoGamesFailure() {
|
||||||
// Setup: Team's games are outside date range
|
// Setup: Team's games are outside date range
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Game is in July, but we search June
|
// Game is in July, but we search June
|
||||||
let game = makeGame(
|
let game = makeGame(
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
homeTeamId: teamId,
|
homeTeamId: teamId,
|
||||||
awayTeamId: UUID(),
|
awayTeamId: "team_opponent_\(UUID().uuidString)",
|
||||||
dateTime: makeDate(month: 7, day: 15, hour: 19)
|
dateTime: makeDate(month: 7, day: 15, hour: 19)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -357,13 +357,13 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.2.3 - Team not involved in any games returns noGamesInRange failure")
|
@Test("D.2.3 - Team not involved in any games returns noGamesInRange failure")
|
||||||
func test_followTeam_TeamNotInGames_ReturnsNoGamesFailure() {
|
func test_followTeam_TeamNotInGames_ReturnsNoGamesFailure() {
|
||||||
// Setup: Games exist but team isn't playing
|
// Setup: Games exist but team isn't playing
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let otherTeam1 = UUID()
|
let otherTeam1 = "team_other1_\(UUID().uuidString)"
|
||||||
let otherTeam2 = UUID()
|
let otherTeam2 = "team_other2_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Game between other teams
|
// Game between other teams
|
||||||
let game = makeGame(
|
let game = makeGame(
|
||||||
@@ -393,12 +393,12 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.2.4 - Repeat city filter removes duplicate city visits")
|
@Test("D.2.4 - Repeat city filter removes duplicate city visits")
|
||||||
func test_followTeam_RepeatCityFilter_RemovesDuplicates() {
|
func test_followTeam_RepeatCityFilter_RemovesDuplicates() {
|
||||||
// Setup: Team has multiple games at same stadium
|
// Setup: Team has multiple games at same stadium
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
|
|
||||||
let game1 = makeGame(
|
let game1 = makeGame(
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
@@ -445,16 +445,16 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.2.5 - Missing date range returns missingDateRange failure")
|
@Test("D.2.5 - Missing date range returns missingDateRange failure")
|
||||||
func test_followTeam_MissingDateRange_ReturnsMissingDateRangeFailure() {
|
func test_followTeam_MissingDateRange_ReturnsMissingDateRangeFailure() {
|
||||||
// Setup: Invalid date range (end before start)
|
// Setup: Invalid date range (end before start)
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
|
|
||||||
let game = makeGame(
|
let game = makeGame(
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
homeTeamId: teamId,
|
homeTeamId: teamId,
|
||||||
awayTeamId: UUID(),
|
awayTeamId: "team_opponent_\(UUID().uuidString)",
|
||||||
dateTime: makeDate(day: 5, hour: 19)
|
dateTime: makeDate(day: 5, hour: 19)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -481,16 +481,16 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.3.1 - Route connects team games chronologically")
|
@Test("D.3.1 - Route connects team games chronologically")
|
||||||
func test_followTeam_RouteIsChronological() {
|
func test_followTeam_RouteIsChronological() {
|
||||||
// Setup: Team with games in 2 nearby cities chronologically
|
// Setup: Team with games in 2 nearby cities chronologically
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
|
|
||||||
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee]
|
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Games in chronological order: Chicago → Milwaukee
|
// Games in chronological order: Chicago → Milwaukee
|
||||||
let game1 = makeGame(
|
let game1 = makeGame(
|
||||||
@@ -534,16 +534,16 @@ struct ScenarioDPlannerTests {
|
|||||||
@Test("D.3.2 - Travel segments connect stops correctly")
|
@Test("D.3.2 - Travel segments connect stops correctly")
|
||||||
func test_followTeam_TravelSegmentsConnectStops() {
|
func test_followTeam_TravelSegmentsConnectStops() {
|
||||||
// Setup: Team with 2 games in different cities
|
// Setup: Team with 2 games in different cities
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let bostonId = UUID()
|
let bostonId = "stadium_boston_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589)
|
||||||
|
|
||||||
let stadiums = [nycId: nyc, bostonId: boston]
|
let stadiums = [nycId: nyc, bostonId: boston]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponentId = UUID()
|
let opponentId = "team_opponent_\(UUID().uuidString)"
|
||||||
|
|
||||||
let game1 = makeGame(
|
let game1 = makeGame(
|
||||||
stadiumId: nycId,
|
stadiumId: nycId,
|
||||||
@@ -597,9 +597,9 @@ struct ScenarioDPlannerTests {
|
|||||||
// Setup: Simulates Houston → Chicago → Anaheim (Astros July 20-29 scenario)
|
// Setup: Simulates Houston → Chicago → Anaheim (Astros July 20-29 scenario)
|
||||||
// Houston to Chicago: ~1000 miles, Chicago to Anaheim: ~2000 miles
|
// Houston to Chicago: ~1000 miles, Chicago to Anaheim: ~2000 miles
|
||||||
// With 4+ days between each leg, both should be feasible
|
// With 4+ days between each leg, both should be feasible
|
||||||
let houstonId = UUID()
|
let houstonId = "stadium_houston_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let anaheimId = UUID()
|
let anaheimId = "stadium_anaheim_\(UUID().uuidString)"
|
||||||
|
|
||||||
let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698)
|
let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698)
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -607,10 +607,10 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim]
|
let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
let opponent3 = UUID()
|
let opponent3 = "team_opponent3_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Houston home games: July 20-22
|
// Houston home games: July 20-22
|
||||||
let houstonGame = makeGame(
|
let houstonGame = makeGame(
|
||||||
@@ -674,9 +674,9 @@ struct ScenarioDPlannerTests {
|
|||||||
func test_followTeam_ThreeCityRoute_InsufficientTime_ExcludesUnreachableCities() {
|
func test_followTeam_ThreeCityRoute_InsufficientTime_ExcludesUnreachableCities() {
|
||||||
// Setup: Same cities but games too close together
|
// Setup: Same cities but games too close together
|
||||||
// Chicago to Anaheim needs ~37 hours driving, but only 1 day between games
|
// Chicago to Anaheim needs ~37 hours driving, but only 1 day between games
|
||||||
let houstonId = UUID()
|
let houstonId = "stadium_houston_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let anaheimId = UUID()
|
let anaheimId = "stadium_anaheim_\(UUID().uuidString)"
|
||||||
|
|
||||||
let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698)
|
let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698)
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -684,10 +684,10 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim]
|
let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
let opponent3 = UUID()
|
let opponent3 = "team_opponent3_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Houston: July 20
|
// Houston: July 20
|
||||||
let houstonGame = makeGame(
|
let houstonGame = makeGame(
|
||||||
@@ -744,17 +744,17 @@ struct ScenarioDPlannerTests {
|
|||||||
func test_followTeam_PicksOptimalGamePerCity_ForRouteFeasibility() {
|
func test_followTeam_PicksOptimalGamePerCity_ForRouteFeasibility() {
|
||||||
// Setup: Team has 3 games in each city (series)
|
// Setup: Team has 3 games in each city (series)
|
||||||
// With allowRepeatCities=false, router should pick games that make the route work
|
// With allowRepeatCities=false, router should pick games that make the route work
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let anaheimId = UUID()
|
let anaheimId = "stadium_anaheim_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827)
|
let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827)
|
||||||
|
|
||||||
let stadiums = [chicagoId: chicago, anaheimId: anaheim]
|
let stadiums = [chicagoId: chicago, anaheimId: anaheim]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Chicago series: July 24, 25, 26
|
// Chicago series: July 24, 25, 26
|
||||||
let chicagoGame1 = makeGame(
|
let chicagoGame1 = makeGame(
|
||||||
@@ -826,8 +826,8 @@ struct ScenarioDPlannerTests {
|
|||||||
func test_followTeam_FiveDaySegment_AtLimit_Succeeds() {
|
func test_followTeam_FiveDaySegment_AtLimit_Succeeds() {
|
||||||
// Setup: ~38 hours of driving with exactly 5 days between games
|
// Setup: ~38 hours of driving with exactly 5 days between games
|
||||||
// 5 days × 8 hours = 40 hours max, which should pass
|
// 5 days × 8 hours = 40 hours max, which should pass
|
||||||
let seattleId = UUID()
|
let seattleId = "stadium_seattle_\(UUID().uuidString)"
|
||||||
let miamiId = UUID()
|
let miamiId = "stadium_denver_\(UUID().uuidString)"
|
||||||
|
|
||||||
// Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles
|
// Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles
|
||||||
// At 60 mph = ~72 hours - this is too far even for 5 days
|
// At 60 mph = ~72 hours - this is too far even for 5 days
|
||||||
@@ -837,9 +837,9 @@ struct ScenarioDPlannerTests {
|
|||||||
|
|
||||||
let stadiums = [seattleId: seattle, miamiId: denver]
|
let stadiums = [seattleId: seattle, miamiId: denver]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let seattleGame = makeGame(
|
let seattleGame = makeGame(
|
||||||
stadiumId: seattleId,
|
stadiumId: seattleId,
|
||||||
@@ -889,17 +889,17 @@ struct ScenarioDPlannerTests {
|
|||||||
// Setup: Distance that would take > 40 hours to drive
|
// Setup: Distance that would take > 40 hours to drive
|
||||||
// Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles
|
// Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles
|
||||||
// At 60 mph = ~72 hours - exceeds 40 hour (5 day) limit
|
// At 60 mph = ~72 hours - exceeds 40 hour (5 day) limit
|
||||||
let seattleId = UUID()
|
let seattleId = "stadium_seattle_\(UUID().uuidString)"
|
||||||
let miamiId = UUID()
|
let miamiId = "stadium_miami_\(UUID().uuidString)"
|
||||||
|
|
||||||
let seattle = makeStadium(id: seattleId, city: "Seattle", lat: 47.6062, lon: -122.3321)
|
let seattle = makeStadium(id: seattleId, city: "Seattle", lat: 47.6062, lon: -122.3321)
|
||||||
let miami = makeStadium(id: miamiId, city: "Miami", lat: 25.7617, lon: -80.1918)
|
let miami = makeStadium(id: miamiId, city: "Miami", lat: 25.7617, lon: -80.1918)
|
||||||
|
|
||||||
let stadiums = [seattleId: seattle, miamiId: miami]
|
let stadiums = [seattleId: seattle, miamiId: miami]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let seattleGame = makeGame(
|
let seattleGame = makeGame(
|
||||||
stadiumId: seattleId,
|
stadiumId: seattleId,
|
||||||
@@ -950,17 +950,17 @@ struct ScenarioDPlannerTests {
|
|||||||
// Setup: Same Chicago→Anaheim route but with 2 drivers
|
// Setup: Same Chicago→Anaheim route but with 2 drivers
|
||||||
// With 2 drivers × 8 hours = 16 hours/day
|
// With 2 drivers × 8 hours = 16 hours/day
|
||||||
// Chicago to Anaheim in 3 days = 48 hours available (vs 24 hours with 1 driver)
|
// Chicago to Anaheim in 3 days = 48 hours available (vs 24 hours with 1 driver)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let anaheimId = UUID()
|
let anaheimId = "stadium_anaheim_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827)
|
let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827)
|
||||||
|
|
||||||
let stadiums = [chicagoId: chicago, anaheimId: anaheim]
|
let stadiums = [chicagoId: chicago, anaheimId: anaheim]
|
||||||
|
|
||||||
let teamId = UUID()
|
let teamId = "team_test_\(UUID().uuidString)"
|
||||||
let opponent1 = UUID()
|
let opponent1 = "team_opponent1_\(UUID().uuidString)"
|
||||||
let opponent2 = UUID()
|
let opponent2 = "team_opponent2_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicagoGame = makeGame(
|
let chicagoGame = makeGame(
|
||||||
stadiumId: chicagoId,
|
stadiumId: chicagoId,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ struct TripPlanningEngineTests {
|
|||||||
|
|
||||||
/// Creates a stadium at a known location
|
/// Creates a stadium at a known location
|
||||||
private func makeStadium(
|
private func makeStadium(
|
||||||
id: UUID = UUID(),
|
id: String = "stadium_test_\(UUID().uuidString)",
|
||||||
city: String,
|
city: String,
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -55,10 +55,10 @@ struct TripPlanningEngineTests {
|
|||||||
|
|
||||||
/// Creates a game at a stadium
|
/// Creates a game at a stadium
|
||||||
private func makeGame(
|
private func makeGame(
|
||||||
id: UUID = UUID(),
|
id: String = "game_test_\(UUID().uuidString)",
|
||||||
stadiumId: UUID,
|
stadiumId: String,
|
||||||
homeTeamId: UUID = UUID(),
|
homeTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
awayTeamId: UUID = UUID(),
|
awayTeamId: String = "team_test_\(UUID().uuidString)",
|
||||||
dateTime: Date,
|
dateTime: Date,
|
||||||
sport: Sport = .mlb
|
sport: Sport = .mlb
|
||||||
) -> Game {
|
) -> Game {
|
||||||
@@ -78,7 +78,7 @@ struct TripPlanningEngineTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
maxDrivingHoursPerDriver: Double = 8.0,
|
maxDrivingHoursPerDriver: Double = 8.0,
|
||||||
allowRepeatCities: Bool = true
|
allowRepeatCities: Bool = true
|
||||||
@@ -104,11 +104,11 @@ struct TripPlanningEngineTests {
|
|||||||
|
|
||||||
/// Creates a PlanningRequest for Scenario B (selected games)
|
/// Creates a PlanningRequest for Scenario B (selected games)
|
||||||
private func makeScenarioBRequest(
|
private func makeScenarioBRequest(
|
||||||
mustSeeGameIds: Set<UUID>,
|
mustSeeGameIds: Set<String>,
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
maxDrivingHoursPerDriver: Double = 8.0,
|
maxDrivingHoursPerDriver: Double = 8.0,
|
||||||
allowRepeatCities: Bool = true
|
allowRepeatCities: Bool = true
|
||||||
@@ -140,7 +140,7 @@ struct TripPlanningEngineTests {
|
|||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
games: [Game],
|
games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [String: Stadium],
|
||||||
numberOfDrivers: Int = 1,
|
numberOfDrivers: Int = 1,
|
||||||
maxDrivingHoursPerDriver: Double = 8.0
|
maxDrivingHoursPerDriver: Double = 8.0
|
||||||
) -> PlanningRequest {
|
) -> PlanningRequest {
|
||||||
@@ -169,8 +169,8 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.1 - Engine delegates to Scenario A correctly")
|
@Test("7.1 - Engine delegates to Scenario A correctly")
|
||||||
func test_engine_ScenarioA_DelegatesCorrectly() {
|
func test_engine_ScenarioA_DelegatesCorrectly() {
|
||||||
// Setup: Date range only request (Scenario A)
|
// Setup: Date range only request (Scenario A)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -202,8 +202,8 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.2 - Engine delegates to Scenario B correctly")
|
@Test("7.2 - Engine delegates to Scenario B correctly")
|
||||||
func test_engine_ScenarioB_DelegatesCorrectly() {
|
func test_engine_ScenarioB_DelegatesCorrectly() {
|
||||||
// Setup: Selected games request (Scenario B)
|
// Setup: Selected games request (Scenario B)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -244,9 +244,9 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.3 - Engine delegates to Scenario C correctly")
|
@Test("7.3 - Engine delegates to Scenario C correctly")
|
||||||
func test_engine_ScenarioC_DelegatesCorrectly() {
|
func test_engine_ScenarioC_DelegatesCorrectly() {
|
||||||
// Setup: Start/end locations request (Scenario C)
|
// Setup: Start/end locations request (Scenario C)
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let clevelandId = UUID()
|
let clevelandId = "stadium_cleveland_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944)
|
let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944)
|
||||||
@@ -294,8 +294,8 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.4 - Scenarios are mutually exclusive")
|
@Test("7.4 - Scenarios are mutually exclusive")
|
||||||
func test_engine_ScenariosAreMutuallyExclusive() {
|
func test_engine_ScenariosAreMutuallyExclusive() {
|
||||||
// Setup: Create requests that could theoretically match multiple scenarios
|
// Setup: Create requests that could theoretically match multiple scenarios
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let clevelandId = UUID()
|
let clevelandId = "stadium_cleveland_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944)
|
let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944)
|
||||||
@@ -350,9 +350,9 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.5 - Result contains travel segments")
|
@Test("7.5 - Result contains travel segments")
|
||||||
func test_engine_Result_ContainsTravelSegments() {
|
func test_engine_Result_ContainsTravelSegments() {
|
||||||
// Setup: Multi-city trip that requires travel
|
// Setup: Multi-city trip that requires travel
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
let detroitId = UUID()
|
let detroitId = "stadium_detroit_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -396,8 +396,8 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.6 - Result contains itinerary days")
|
@Test("7.6 - Result contains itinerary days")
|
||||||
func test_engine_Result_ContainsItineraryDays() {
|
func test_engine_Result_ContainsItineraryDays() {
|
||||||
// Setup: Multi-day trip
|
// Setup: Multi-day trip
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -445,7 +445,7 @@ struct TripPlanningEngineTests {
|
|||||||
func test_engine_Result_IncludesWarnings_WhenApplicable() {
|
func test_engine_Result_IncludesWarnings_WhenApplicable() {
|
||||||
// Setup: Request that would normally violate repeat cities
|
// Setup: Request that would normally violate repeat cities
|
||||||
// but allowRepeatCities=true so it should succeed without warnings
|
// but allowRepeatCities=true so it should succeed without warnings
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
|
|
||||||
@@ -497,8 +497,8 @@ struct TripPlanningEngineTests {
|
|||||||
func test_engine_NumberOfDrivers_AffectsMaxDailyDriving() {
|
func test_engine_NumberOfDrivers_AffectsMaxDailyDriving() {
|
||||||
// Setup: Long distance trip that requires significant driving
|
// Setup: Long distance trip that requires significant driving
|
||||||
// NYC to Chicago is ~790 miles (~13 hours of driving)
|
// NYC to Chicago is ~790 miles (~13 hours of driving)
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
|
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
@@ -593,8 +593,8 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.10 - AllowRepeatCities is propagated to DAG")
|
@Test("7.10 - AllowRepeatCities is propagated to DAG")
|
||||||
func test_engine_AllowRepeatCities_PropagatedToDAG() {
|
func test_engine_AllowRepeatCities_PropagatedToDAG() {
|
||||||
// Setup: Games that would require visiting the same city twice
|
// Setup: Games that would require visiting the same city twice
|
||||||
let chicagoId = UUID()
|
let chicagoId = "stadium_chicago_\(UUID().uuidString)"
|
||||||
let milwaukeeId = UUID()
|
let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)"
|
||||||
|
|
||||||
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
|
||||||
@@ -657,8 +657,8 @@ struct TripPlanningEngineTests {
|
|||||||
func test_engine_ImpossibleConstraints_ReturnsNoResult() {
|
func test_engine_ImpossibleConstraints_ReturnsNoResult() {
|
||||||
// Setup: Create an impossible constraint scenario
|
// Setup: Create an impossible constraint scenario
|
||||||
// Games at the same time on same day in cities far apart (can't make both)
|
// Games at the same time on same day in cities far apart (can't make both)
|
||||||
let nycId = UUID()
|
let nycId = "stadium_nyc_\(UUID().uuidString)"
|
||||||
let laId = UUID()
|
let laId = "stadium_la_\(UUID().uuidString)"
|
||||||
|
|
||||||
// NYC to LA is ~2,800 miles - impossible to drive same day
|
// NYC to LA is ~2,800 miles - impossible to drive same day
|
||||||
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
|
||||||
@@ -711,7 +711,7 @@ struct TripPlanningEngineTests {
|
|||||||
@Test("7.12 - Empty input returns error")
|
@Test("7.12 - Empty input returns error")
|
||||||
func test_engine_EmptyInput_ThrowsError() {
|
func test_engine_EmptyInput_ThrowsError() {
|
||||||
// Setup: Request with no games
|
// Setup: Request with no games
|
||||||
let stadiumId = UUID()
|
let stadiumId = "stadium_test_\(UUID().uuidString)"
|
||||||
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
|
||||||
let stadiums = [stadiumId: stadium]
|
let stadiums = [stadiumId: stadium]
|
||||||
|
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
var modelContainer: ModelContainer!
|
var modelContainer: ModelContainer!
|
||||||
var modelContext: ModelContext!
|
var modelContext: ModelContext!
|
||||||
|
|
||||||
// Test UUIDs for stadiums
|
// Canonical IDs for test stadiums
|
||||||
let fenwayUUID = UUID(uuidString: "11111111-1111-1111-1111-111111111111")!
|
let fenwayId = "stadium_mlb_bos"
|
||||||
let wrigleyUUID = UUID(uuidString: "22222222-2222-2222-2222-222222222222")!
|
let wrigleyId = "stadium_mlb_chc"
|
||||||
let msgUUID = UUID(uuidString: "33333333-3333-3333-3333-333333333333")!
|
let msgId = "stadium_nba_nyk"
|
||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
try await super.setUp()
|
try await super.setUp()
|
||||||
@@ -231,8 +231,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
private func setupTestData() async {
|
private func setupTestData() async {
|
||||||
// Create Fenway Park stadium
|
// Create Fenway Park stadium
|
||||||
let fenway = CanonicalStadium(
|
let fenway = CanonicalStadium(
|
||||||
canonicalId: "stadium_mlb_bos",
|
canonicalId: fenwayId,
|
||||||
uuid: fenwayUUID,
|
|
||||||
name: "Fenway Park",
|
name: "Fenway Park",
|
||||||
city: "Boston",
|
city: "Boston",
|
||||||
state: "MA",
|
state: "MA",
|
||||||
@@ -245,8 +244,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Create Wrigley Field stadium
|
// Create Wrigley Field stadium
|
||||||
let wrigley = CanonicalStadium(
|
let wrigley = CanonicalStadium(
|
||||||
canonicalId: "stadium_mlb_chc",
|
canonicalId: wrigleyId,
|
||||||
uuid: wrigleyUUID,
|
|
||||||
name: "Wrigley Field",
|
name: "Wrigley Field",
|
||||||
city: "Chicago",
|
city: "Chicago",
|
||||||
state: "IL",
|
state: "IL",
|
||||||
@@ -259,8 +257,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Create MSG stadium
|
// Create MSG stadium
|
||||||
let msg = CanonicalStadium(
|
let msg = CanonicalStadium(
|
||||||
canonicalId: "stadium_nba_nyk",
|
canonicalId: msgId,
|
||||||
uuid: msgUUID,
|
|
||||||
name: "Madison Square Garden",
|
name: "Madison Square Garden",
|
||||||
city: "New York",
|
city: "New York",
|
||||||
state: "NY",
|
state: "NY",
|
||||||
@@ -317,10 +314,9 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
// Create engine
|
// Create engine
|
||||||
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
||||||
|
|
||||||
// Create a visit to Fenway using the actual stadium UUID format
|
// Create a visit to Fenway using the canonical stadium ID
|
||||||
let visit = StadiumVisit(
|
let visit = StadiumVisit(
|
||||||
canonicalStadiumId: fenwayUUID.uuidString,
|
stadiumId: fenwayId,
|
||||||
stadiumUUID: fenwayUUID,
|
|
||||||
stadiumNameAtVisit: "Fenway Park",
|
stadiumNameAtVisit: "Fenway Park",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .mlb
|
sport: .mlb
|
||||||
@@ -347,8 +343,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
||||||
|
|
||||||
let visit = StadiumVisit(
|
let visit = StadiumVisit(
|
||||||
canonicalStadiumId: wrigleyUUID.uuidString,
|
stadiumId: wrigleyId,
|
||||||
stadiumUUID: wrigleyUUID,
|
|
||||||
stadiumNameAtVisit: "Wrigley Field",
|
stadiumNameAtVisit: "Wrigley Field",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .mlb
|
sport: .mlb
|
||||||
@@ -386,8 +381,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Add a visit
|
// Add a visit
|
||||||
let visit = StadiumVisit(
|
let visit = StadiumVisit(
|
||||||
canonicalStadiumId: fenwayUUID.uuidString,
|
stadiumId: fenwayId,
|
||||||
stadiumUUID: fenwayUUID,
|
|
||||||
stadiumNameAtVisit: "Fenway Park",
|
stadiumNameAtVisit: "Fenway Park",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .mlb
|
sport: .mlb
|
||||||
@@ -406,10 +400,9 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared)
|
||||||
|
|
||||||
// Add TD Garden for NHL
|
// Add TD Garden for NHL
|
||||||
let tdGardenUUID = UUID()
|
let tdGardenId = "stadium_nhl_bos"
|
||||||
let tdGarden = CanonicalStadium(
|
let tdGarden = CanonicalStadium(
|
||||||
canonicalId: "stadium_nhl_bos",
|
canonicalId: tdGardenId,
|
||||||
uuid: tdGardenUUID,
|
|
||||||
name: "TD Garden",
|
name: "TD Garden",
|
||||||
city: "Boston",
|
city: "Boston",
|
||||||
state: "MA",
|
state: "MA",
|
||||||
@@ -437,8 +430,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Visit MLB stadium only - not enough
|
// Visit MLB stadium only - not enough
|
||||||
let mlbVisit = StadiumVisit(
|
let mlbVisit = StadiumVisit(
|
||||||
canonicalStadiumId: fenwayUUID.uuidString,
|
stadiumId: fenwayId,
|
||||||
stadiumUUID: fenwayUUID,
|
|
||||||
stadiumNameAtVisit: "Fenway Park",
|
stadiumNameAtVisit: "Fenway Park",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .mlb
|
sport: .mlb
|
||||||
@@ -453,8 +445,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Visit NBA stadium - still not enough
|
// Visit NBA stadium - still not enough
|
||||||
let nbaVisit = StadiumVisit(
|
let nbaVisit = StadiumVisit(
|
||||||
canonicalStadiumId: msgUUID.uuidString,
|
stadiumId: msgId,
|
||||||
stadiumUUID: msgUUID,
|
|
||||||
stadiumNameAtVisit: "MSG",
|
stadiumNameAtVisit: "MSG",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .nba
|
sport: .nba
|
||||||
@@ -469,8 +460,7 @@ final class AchievementEngineIntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Visit NHL stadium - now earned!
|
// Visit NHL stadium - now earned!
|
||||||
let nhlVisit = StadiumVisit(
|
let nhlVisit = StadiumVisit(
|
||||||
canonicalStadiumId: tdGardenUUID.uuidString,
|
stadiumId: tdGardenId,
|
||||||
stadiumUUID: tdGardenUUID,
|
|
||||||
stadiumNameAtVisit: "TD Garden",
|
stadiumNameAtVisit: "TD Garden",
|
||||||
visitDate: Date(),
|
visitDate: Date(),
|
||||||
sport: .nhl
|
sport: .nhl
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct DuplicateGameIdTests {
|
|||||||
|
|
||||||
private func makeStadium(sport: Sport = .mlb) -> Stadium {
|
private func makeStadium(sport: Sport = .mlb) -> Stadium {
|
||||||
Stadium(
|
Stadium(
|
||||||
id: UUID(),
|
id: "stadium_test_\(UUID().uuidString)",
|
||||||
name: "Test Stadium",
|
name: "Test Stadium",
|
||||||
city: "Test City",
|
city: "Test City",
|
||||||
state: "TS",
|
state: "TS",
|
||||||
@@ -31,9 +31,9 @@ struct DuplicateGameIdTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeTeam(sport: Sport = .mlb, stadiumId: UUID) -> Team {
|
private func makeTeam(sport: Sport = .mlb, stadiumId: String) -> Team {
|
||||||
Team(
|
Team(
|
||||||
id: UUID(),
|
id: "team_test_\(UUID().uuidString)",
|
||||||
name: "Test Team",
|
name: "Test Team",
|
||||||
abbreviation: "TST",
|
abbreviation: "TST",
|
||||||
sport: sport,
|
sport: sport,
|
||||||
@@ -42,7 +42,7 @@ struct DuplicateGameIdTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeGame(id: UUID, homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID, dateTime: Date) -> Game {
|
private func makeGame(id: String, homeTeamId: String, awayTeamId: String, stadiumId: String, dateTime: Date) -> Game {
|
||||||
Game(
|
Game(
|
||||||
id: id,
|
id: id,
|
||||||
homeTeamId: homeTeamId,
|
homeTeamId: homeTeamId,
|
||||||
@@ -59,16 +59,16 @@ struct DuplicateGameIdTests {
|
|||||||
@Test("Duplicate games are deduplicated at load time")
|
@Test("Duplicate games are deduplicated at load time")
|
||||||
func gamesArray_DeduplicatesById() {
|
func gamesArray_DeduplicatesById() {
|
||||||
// Simulate the deduplication logic used in StubDataProvider
|
// Simulate the deduplication logic used in StubDataProvider
|
||||||
let gameId = UUID()
|
let gameId = "game_test_\(UUID().uuidString)"
|
||||||
let dateTime = Date()
|
let dateTime = Date()
|
||||||
|
|
||||||
let game1 = makeGame(id: gameId, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: UUID(), dateTime: dateTime)
|
let game1 = makeGame(id: gameId, homeTeamId: "team_home_\(UUID().uuidString)", awayTeamId: "team_away_\(UUID().uuidString)", stadiumId: "stadium_test_\(UUID().uuidString)", dateTime: dateTime)
|
||||||
let game2 = makeGame(id: gameId, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: UUID(), dateTime: dateTime.addingTimeInterval(3600))
|
let game2 = makeGame(id: gameId, homeTeamId: "team_home_\(UUID().uuidString)", awayTeamId: "team_away_\(UUID().uuidString)", stadiumId: "stadium_test_\(UUID().uuidString)", dateTime: dateTime.addingTimeInterval(3600))
|
||||||
|
|
||||||
let games = [game1, game2]
|
let games = [game1, game2]
|
||||||
|
|
||||||
// Deduplication logic from StubDataProvider
|
// Deduplication logic from StubDataProvider
|
||||||
var seenIds = Set<UUID>()
|
var seenIds = Set<String>()
|
||||||
let uniqueGames = games.filter { game in
|
let uniqueGames = games.filter { game in
|
||||||
if seenIds.contains(game.id) {
|
if seenIds.contains(game.id) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user