Files
Sportstime/SportsTime/Planning/Validators/DateRangeValidator.swift
Trey t 9088b46563 Initial commit: SportsTime trip planning app
- Three-scenario planning engine (A: date range, B: selected games, C: directional routes)
- GeographicRouteExplorer with anchor game support for route exploration
- Shared ItineraryBuilder for travel segment calculation
- TravelEstimator for driving time/distance estimation
- SwiftUI views for trip creation and detail display
- CloudKit integration for schedule data
- Python scraping scripts for sports schedules

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 00:46:40 -06:00

128 lines
4.3 KiB
Swift

//
// DateRangeValidator.swift
// SportsTime
//
import Foundation
/// Validates that all games fall within the specified date range.
/// Priority 1 in the rule hierarchy - checked first before any other constraints.
struct DateRangeValidator {
// MARK: - Validation Result
struct ValidationResult {
let isValid: Bool
let violations: [ConstraintViolation]
let gamesOutsideRange: [UUID]
static let valid = ValidationResult(isValid: true, violations: [], gamesOutsideRange: [])
static func invalid(games: [UUID]) -> ValidationResult {
let violations = games.map { gameId in
ConstraintViolation(
type: .dateRange,
description: "Game \(gameId.uuidString.prefix(8)) falls outside the specified date range",
severity: .error
)
}
return ValidationResult(isValid: false, violations: violations, gamesOutsideRange: games)
}
}
// MARK: - Validation
/// Validates that ALL selected games (must-see games) fall within the date range.
/// This is a HARD constraint - if any selected game is outside the range, planning fails.
///
/// - Parameters:
/// - mustSeeGameIds: Set of game IDs that MUST be included in the trip
/// - allGames: All available games to check against
/// - startDate: Start of the valid date range (inclusive)
/// - endDate: End of the valid date range (inclusive)
/// - Returns: ValidationResult indicating success or failure with specific violations
func validate(
mustSeeGameIds: Set<UUID>,
allGames: [Game],
startDate: Date,
endDate: Date
) -> ValidationResult {
// If no must-see games, validation passes
guard !mustSeeGameIds.isEmpty else {
return .valid
}
// Find all must-see games that fall outside the range
let gamesOutsideRange = allGames
.filter { mustSeeGameIds.contains($0.id) }
.filter { game in
game.dateTime < startDate || game.dateTime > endDate
}
.map { $0.id }
if gamesOutsideRange.isEmpty {
return .valid
} else {
return .invalid(games: gamesOutsideRange)
}
}
/// Validates games for Scenario B (Selected Games mode).
/// ALL selected games MUST be within the date range - no exceptions.
///
/// - Parameters:
/// - request: The planning request containing preferences and games
/// - Returns: ValidationResult with explicit failure if any selected game is out of range
func validateForScenarioB(_ request: PlanningRequest) -> ValidationResult {
return validate(
mustSeeGameIds: request.preferences.mustSeeGameIds,
allGames: request.availableGames,
startDate: request.preferences.startDate,
endDate: request.preferences.endDate
)
}
/// Checks if there are any games available within the date range.
/// Used to determine if planning can proceed at all.
///
/// - Parameters:
/// - games: All available games
/// - startDate: Start of the valid date range
/// - endDate: End of the valid date range
/// - sports: Sports to filter by
/// - Returns: True if at least one game exists in the range
func hasGamesInRange(
games: [Game],
startDate: Date,
endDate: Date,
sports: Set<Sport>
) -> Bool {
games.contains { game in
game.dateTime >= startDate &&
game.dateTime <= endDate &&
sports.contains(game.sport)
}
}
/// Returns all games that fall within the specified date range.
///
/// - Parameters:
/// - games: All available games
/// - startDate: Start of the valid date range
/// - endDate: End of the valid date range
/// - sports: Sports to filter by
/// - Returns: Array of games within the range
func gamesInRange(
games: [Game],
startDate: Date,
endDate: Date,
sports: Set<Sport>
) -> [Game] {
games.filter { game in
game.dateTime >= startDate &&
game.dateTime <= endDate &&
sports.contains(game.sport)
}
}
}