// // 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, 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 ) -> 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 ) -> [Game] { games.filter { game in game.dateTime >= startDate && game.dateTime <= endDate && sports.contains(game.sport) } } }