Fix team-first future window selection

This commit is contained in:
Trey t
2026-04-03 15:31:52 -05:00
parent 0fa3db5401
commit 188076717b
3 changed files with 169 additions and 11 deletions

View File

@@ -59,6 +59,13 @@ final class ScenarioEPlanner: ScenarioPlanner {
/// Maximum number of results to return
private let maxResultsToReturn = 10
/// Current date used to filter out past windows. Injectable for testing.
private let currentDate: Date
init(currentDate: Date = Date()) {
self.currentDate = currentDate
}
// MARK: - ScenarioPlanner Protocol
func plan(request: PlanningRequest) -> ItineraryResult {
@@ -181,7 +188,7 @@ final class ScenarioEPlanner: ScenarioPlanner {
//
var allItineraryOptions: [ItineraryOption] = []
for (windowIndex, window) in windowsToEvaluate.enumerated() {
for window in windowsToEvaluate {
// Collect games in this window
var gamesByTeamInWindow: [String: [Game]] = [:]
var hasAllTeamsInWindow = true
@@ -296,13 +303,10 @@ final class ScenarioEPlanner: ScenarioPlanner {
allItineraryOptions.append(option)
}
// Early exit if we have enough options
if allItineraryOptions.count >= maxResultsToReturn * 5 {
#if DEBUG
print("🔍 ScenarioE: Early exit at window \(windowIndex + 1) with \(allItineraryOptions.count) options")
#endif
break
}
// No early exit evaluate all sampled windows so results
// span the full season instead of clustering around early dates.
// The 50-window sample cap + final dedup + top-10 ranking are
// sufficient throttles (beam search on ~10 games/window is fast).
}
//
@@ -384,9 +388,12 @@ final class ScenarioEPlanner: ScenarioPlanner {
let earliestDay = calendar.startOfDay(for: earliest)
let latestDay = calendar.startOfDay(for: latest)
// Skip past windows users only want future trips
let today = calendar.startOfDay(for: currentDate)
// Generate sliding windows
var validWindows: [DateInterval] = []
var currentStart = earliestDay
var currentStart = max(earliestDay, today)
while let windowEnd = calendar.date(byAdding: .day, value: windowDurationDays, to: currentStart),
windowEnd <= calendar.date(byAdding: .day, value: 1, to: latestDay)! {

View File

@@ -17,7 +17,7 @@ struct ScenarioEPlannerTests {
// MARK: - Test Data
private let planner = ScenarioEPlanner()
private let planner = ScenarioEPlanner(currentDate: TestClock.now)
// East Coast coordinates
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
@@ -1177,6 +1177,157 @@ struct ScenarioEPlannerTests {
#expect(result.isSuccess || result.failure?.reason != .noGamesInRange)
}
// MARK: - Past Date Filtering Tests
@Test("teamFirst: past-only games return noValidRoutes")
func teamFirst_pastOnlyGames_returnsNoResults() {
// Simulate: currentDate is June 1, but all games are in March (past)
let calendar = TestClock.calendar
let currentDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 12)
let pastDate = TestFixtures.date(year: 2026, month: 3, day: 10, hour: 19)
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
let pastGame1 = makeGame(id: "past-yankees", homeTeamId: "yankees", awayTeamId: "opp", stadiumId: "nyc", dateTime: pastDate)
let pastGame2 = makeGame(id: "past-redsox", homeTeamId: "redsox", awayTeamId: "opp", stadiumId: "boston",
dateTime: calendar.date(byAdding: .day, value: 2, to: pastDate)!)
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
selectedTeamIds: ["yankees", "redsox"]
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [pastGame1, pastGame2],
teams: ["yankees": makeTeam(id: "yankees", name: "Yankees"),
"redsox": makeTeam(id: "redsox", name: "Red Sox")],
stadiums: ["nyc": nycStadium, "boston": bostonStadium]
)
let planner = ScenarioEPlanner(currentDate: currentDate)
let result = planner.plan(request: request)
// All games are in the past no valid windows should exist
guard case .failure(let failure) = result else {
Issue.record("Expected failure when all games are in the past")
return
}
#expect(failure.reason == .noValidRoutes, "Should fail with noValidRoutes when all windows are in the past")
}
@Test("teamFirst: mix of past and future games only returns future windows")
func teamFirst_mixPastFuture_onlyReturnsFutureWindows() {
let calendar = TestClock.calendar
// Current date: March 15, 2026
let currentDate = TestFixtures.date(year: 2026, month: 3, day: 15, hour: 12)
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
// Past games (early March)
let pastGame1 = makeGame(id: "past-yankees", homeTeamId: "yankees", awayTeamId: "opp", stadiumId: "nyc",
dateTime: TestFixtures.date(year: 2026, month: 3, day: 2, hour: 19))
let pastGame2 = makeGame(id: "past-redsox", homeTeamId: "redsox", awayTeamId: "opp", stadiumId: "boston",
dateTime: TestFixtures.date(year: 2026, month: 3, day: 3, hour: 19))
// Future games (late March / April)
let futureGame1 = makeGame(id: "future-yankees", homeTeamId: "yankees", awayTeamId: "opp", stadiumId: "nyc",
dateTime: TestFixtures.date(year: 2026, month: 3, day: 20, hour: 19))
let futureGame2 = makeGame(id: "future-redsox", homeTeamId: "redsox", awayTeamId: "opp", stadiumId: "boston",
dateTime: TestFixtures.date(year: 2026, month: 3, day: 22, hour: 19))
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
selectedTeamIds: ["yankees", "redsox"]
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [pastGame1, pastGame2, futureGame1, futureGame2],
teams: ["yankees": makeTeam(id: "yankees", name: "Yankees"),
"redsox": makeTeam(id: "redsox", name: "Red Sox")],
stadiums: ["nyc": nycStadium, "boston": bostonStadium]
)
let planner = ScenarioEPlanner(currentDate: currentDate)
let result = planner.plan(request: request)
if case .success(let options) = result {
// All returned stops should be on or after currentDate
for option in options {
for stop in option.stops {
#expect(stop.arrivalDate >= calendar.startOfDay(for: currentDate),
"All stops should be in the future, got \(stop.arrivalDate)")
}
}
}
// Failure is acceptable if routing constraints prevent a valid route
}
@Test("teamFirst: evaluates all sampled windows across full season")
func teamFirst_evaluatesAllSampledWindows_fullSeasonCoverage() {
let calendar = TestClock.calendar
let currentDate = TestFixtures.date(year: 2026, month: 4, day: 1, hour: 12)
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
// Create games spread across April-September (6 months)
// Each team gets a home game every ~10 days
var allGames: [Game] = []
for monthOffset in 0..<6 {
let month = 4 + monthOffset
for dayOffset in stride(from: 1, through: 25, by: 10) {
let gameDate = TestFixtures.date(year: 2026, month: month, day: dayOffset, hour: 19)
allGames.append(makeGame(
id: "yankees-\(month)-\(dayOffset)",
homeTeamId: "yankees", awayTeamId: "opp", stadiumId: "nyc",
dateTime: gameDate
))
let gameDate2 = calendar.date(byAdding: .day, value: 1, to: gameDate)!
allGames.append(makeGame(
id: "redsox-\(month)-\(dayOffset)",
homeTeamId: "redsox", awayTeamId: "opp", stadiumId: "boston",
dateTime: gameDate2
))
}
}
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
selectedTeamIds: ["yankees", "redsox"]
)
let request = PlanningRequest(
preferences: prefs,
availableGames: allGames,
teams: ["yankees": makeTeam(id: "yankees", name: "Yankees"),
"redsox": makeTeam(id: "redsox", name: "Red Sox")],
stadiums: ["nyc": nycStadium, "boston": bostonStadium]
)
let planner = ScenarioEPlanner(currentDate: currentDate)
let result = planner.plan(request: request)
guard case .success(let options) = result else {
Issue.record("Expected success with games spread across full season")
return
}
#expect(!options.isEmpty, "Should find options across the season")
// Verify results span multiple months (not clustered in first month)
let months = Set(options.flatMap { option in
option.stops.map { calendar.component(.month, from: $0.arrivalDate) }
})
#expect(months.count >= 2, "Results should span at least 2 months, got months: \(months.sorted())")
}
// MARK: - Helper Methods
private func makeStadium(

View File

@@ -17,7 +17,7 @@ struct TeamFirstIntegrationTests {
// MARK: - Test Data
private let planner = ScenarioEPlanner()
private let planner = ScenarioEPlanner(currentDate: TestClock.now)
// MLB stadiums with realistic coordinates
private let yankeeStadiumCoord = CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262)