fix(itinerary): add city to game items for proper constraint validation
Travel constraint validation was not working because ItineraryConstraints had no game items to validate against - games came from RichGame objects but were never converted to ItineraryItem for constraint checking. Changes: - Add city parameter to ItemKind.game enum case - Create game ItineraryItems from RichGame data in buildItineraryData() - Update isValidTravelPosition to compare against actual game sortOrders - Fix tests to use appropriate game sortOrder conventions Now travel is properly constrained to appear before arrival city games and after departure city games. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,6 @@ struct ItineraryConstraints {
|
||||
let tripDayCount: Int
|
||||
private let items: [ItineraryItem]
|
||||
|
||||
/// City extracted from game ID (format: "game-CityName-xxxx")
|
||||
private func city(forGameId gameId: String) -> String? {
|
||||
let components = gameId.components(separatedBy: "-")
|
||||
guard components.count >= 2 else { return nil }
|
||||
return components[1]
|
||||
}
|
||||
|
||||
init(tripDayCount: Int, items: [ItineraryItem]) {
|
||||
self.tripDayCount = tripDayCount
|
||||
self.items = items
|
||||
@@ -83,8 +76,10 @@ struct ItineraryConstraints {
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func isValidTravelPosition(fromCity: String, toCity: String, day: Int, sortOrder: Double) -> Bool {
|
||||
let departureGameDays = gameDays(in: fromCity)
|
||||
let arrivalGameDays = gameDays(in: toCity)
|
||||
let departureGames = games(in: fromCity)
|
||||
let arrivalGames = games(in: toCity)
|
||||
let departureGameDays = departureGames.map { $0.day }
|
||||
let arrivalGameDays = arrivalGames.map { $0.day }
|
||||
|
||||
let minDay = departureGameDays.max() ?? 1
|
||||
let maxDay = arrivalGameDays.min() ?? tripDayCount
|
||||
@@ -92,26 +87,20 @@ struct ItineraryConstraints {
|
||||
// Check day is in valid range
|
||||
guard day >= minDay && day <= maxDay else { return false }
|
||||
|
||||
// Check sortOrder constraints on edge days
|
||||
// Check sortOrder constraints on edge days.
|
||||
// Must be strictly AFTER all departure games on that day
|
||||
if departureGameDays.contains(day) {
|
||||
// On a departure game day: must be after ALL games in that city on that day
|
||||
let maxGameSortOrder = games(in: fromCity)
|
||||
.filter { $0.day == day }
|
||||
.map { $0.sortOrder }
|
||||
.max() ?? 0
|
||||
|
||||
let gamesOnDay = departureGames.filter { $0.day == day }
|
||||
let maxGameSortOrder = gamesOnDay.map { $0.sortOrder }.max() ?? 0
|
||||
if sortOrder <= maxGameSortOrder {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Must be strictly BEFORE all arrival games on that day
|
||||
if arrivalGameDays.contains(day) {
|
||||
// On an arrival game day: must be before ALL games in that city on that day
|
||||
let minGameSortOrder = games(in: toCity)
|
||||
.filter { $0.day == day }
|
||||
.map { $0.sortOrder }
|
||||
.min() ?? Double.greatestFiniteMagnitude
|
||||
|
||||
let gamesOnDay = arrivalGames.filter { $0.day == day }
|
||||
let minGameSortOrder = gamesOnDay.map { $0.sortOrder }.min() ?? 0
|
||||
if sortOrder >= minGameSortOrder {
|
||||
return false
|
||||
}
|
||||
@@ -126,8 +115,8 @@ struct ItineraryConstraints {
|
||||
|
||||
private func games(in city: String) -> [ItineraryItem] {
|
||||
return items.filter { item in
|
||||
guard case .game(let gameId) = item.kind else { return false }
|
||||
return self.city(forGameId: gameId) == city
|
||||
guard let gameCity = item.gameCity else { return false }
|
||||
return gameCity.lowercased() == city.lowercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct ItineraryItem: Identifiable, Codable, Hashable {
|
||||
|
||||
/// The type of itinerary item
|
||||
enum ItemKind: Codable, Hashable {
|
||||
case game(gameId: String)
|
||||
case game(gameId: String, city: String)
|
||||
case travel(TravelInfo)
|
||||
case custom(CustomInfo)
|
||||
}
|
||||
@@ -106,14 +106,19 @@ extension ItineraryItem {
|
||||
}
|
||||
|
||||
var gameId: String? {
|
||||
if case .game(let id) = kind { return id }
|
||||
if case .game(let id, _) = kind { return id }
|
||||
return nil
|
||||
}
|
||||
|
||||
var gameCity: String? {
|
||||
if case .game(_, let city) = kind { return city }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Display title for the item
|
||||
var displayTitle: String {
|
||||
switch kind {
|
||||
case .game(let gameId):
|
||||
case .game(let gameId, _):
|
||||
return "Game: \(gameId)"
|
||||
case .travel(let info):
|
||||
return "\(info.fromCity) → \(info.toCity)"
|
||||
|
||||
@@ -145,8 +145,9 @@ extension ItineraryItem {
|
||||
// Parse kind
|
||||
switch kindString {
|
||||
case "game":
|
||||
guard let gameId = record["gameId"] as? String else { return nil }
|
||||
self.kind = .game(gameId: gameId)
|
||||
guard let gameId = record["gameId"] as? String,
|
||||
let gameCity = record["gameCity"] as? String else { return nil }
|
||||
self.kind = .game(gameId: gameId, city: gameCity)
|
||||
|
||||
case "travel":
|
||||
guard let fromCity = record["travelFromCity"] as? String,
|
||||
@@ -188,9 +189,10 @@ extension ItineraryItem {
|
||||
record["modifiedAt"] = modifiedAt
|
||||
|
||||
switch kind {
|
||||
case .game(let gameId):
|
||||
case .game(let gameId, let city):
|
||||
record["kind"] = "game"
|
||||
record["gameId"] = gameId
|
||||
record["gameCity"] = city
|
||||
|
||||
case .travel(let info):
|
||||
record["kind"] = "travel"
|
||||
|
||||
Reference in New Issue
Block a user