// // BruteForceRouteVerifier.swift // SportsTimeTests // // Exhaustively enumerates all route permutations to verify optimality. // Used for inputs with ≤8 stops where brute force is feasible. // import Foundation import CoreLocation @testable import SportsTime // MARK: - Route Verifier struct BruteForceRouteVerifier { // MARK: - Route Comparison Result struct VerificationResult { let isOptimal: Bool let proposedRouteDistance: Double let optimalRouteDistance: Double let optimalRoute: [String]? let improvement: Double? // Percentage improvement if not optimal let permutationsChecked: Int var improvementPercentage: Double? { guard let improvement = improvement else { return nil } return improvement * 100 } } // MARK: - Verification /// Verify that a proposed route is optimal (or near-optimal) by checking all permutations /// - Parameters: /// - proposedRoute: The route to verify (ordered list of stop IDs) /// - stops: Dictionary mapping stop IDs to their coordinates /// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal) /// - Returns: Verification result static func verify( proposedRoute: [String], stops: [String: CLLocationCoordinate2D], tolerance: Double = 0 ) -> VerificationResult { guard proposedRoute.count <= TestConstants.bruteForceMaxStops else { fatalError("BruteForceRouteVerifier should only be used for ≤\(TestConstants.bruteForceMaxStops) stops") } guard proposedRoute.count >= 2 else { // Single stop or empty - trivially optimal return VerificationResult( isOptimal: true, proposedRouteDistance: 0, optimalRouteDistance: 0, optimalRoute: proposedRoute, improvement: nil, permutationsChecked: 1 ) } let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops) // Find optimal route by checking all permutations let allPermutations = permutations(of: proposedRoute) var optimalDistance = Double.infinity var optimalRoute: [String] = [] for permutation in allPermutations { let distance = calculateRouteDistance(permutation, stops: stops) if distance < optimalDistance { optimalDistance = distance optimalRoute = permutation } } let isOptimal: Bool var improvement: Double? = nil if tolerance == 0 { // Exact optimality check with floating point tolerance isOptimal = abs(proposedDistance - optimalDistance) < 0.001 } else { // Within tolerance let maxAllowed = optimalDistance * (1 + tolerance) isOptimal = proposedDistance <= maxAllowed } if !isOptimal && optimalDistance > 0 { improvement = (proposedDistance - optimalDistance) / optimalDistance } return VerificationResult( isOptimal: isOptimal, proposedRouteDistance: proposedDistance, optimalRouteDistance: optimalDistance, optimalRoute: optimalRoute, improvement: improvement, permutationsChecked: allPermutations.count ) } /// Verify a route is optimal with a fixed start and end point static func verifyWithFixedEndpoints( proposedRoute: [String], stops: [String: CLLocationCoordinate2D], startId: String, endId: String, tolerance: Double = 0 ) -> VerificationResult { guard proposedRoute.first == startId && proposedRoute.last == endId else { // Invalid route - doesn't match required endpoints return VerificationResult( isOptimal: false, proposedRouteDistance: Double.infinity, optimalRouteDistance: 0, optimalRoute: nil, improvement: nil, permutationsChecked: 0 ) } // Get intermediate stops (excluding start and end) let intermediateStops = proposedRoute.dropFirst().dropLast() guard intermediateStops.count <= TestConstants.bruteForceMaxStops - 2 else { fatalError("BruteForceRouteVerifier: too many intermediate stops") } let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops) // Generate all permutations of intermediate stops let allPermutations = permutations(of: Array(intermediateStops)) var optimalDistance = Double.infinity var optimalRoute: [String] = [] for permutation in allPermutations { var fullRoute = [startId] fullRoute.append(contentsOf: permutation) fullRoute.append(endId) let distance = calculateRouteDistance(fullRoute, stops: stops) if distance < optimalDistance { optimalDistance = distance optimalRoute = fullRoute } } let isOptimal: Bool var improvement: Double? = nil if tolerance == 0 { isOptimal = abs(proposedDistance - optimalDistance) < 0.001 } else { let maxAllowed = optimalDistance * (1 + tolerance) isOptimal = proposedDistance <= maxAllowed } if !isOptimal && optimalDistance > 0 { improvement = (proposedDistance - optimalDistance) / optimalDistance } return VerificationResult( isOptimal: isOptimal, proposedRouteDistance: proposedDistance, optimalRouteDistance: optimalDistance, optimalRoute: optimalRoute, improvement: improvement, permutationsChecked: allPermutations.count ) } /// Check if there's an obviously better route (significantly shorter) static func hasObviouslyBetterRoute( proposedRoute: [String], stops: [String: CLLocationCoordinate2D], threshold: Double = 0.1 // 10% improvement threshold ) -> (hasBetter: Bool, improvement: Double?) { let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold) return (!result.isOptimal, result.improvement) } // MARK: - Distance Calculation /// Calculate total route distance using haversine formula static func calculateRouteDistance( _ route: [String], stops: [String: CLLocationCoordinate2D] ) -> Double { guard route.count >= 2 else { return 0 } var totalDistance: Double = 0 for i in 0..<(route.count - 1) { guard let from = stops[route[i]], let to = stops[route[i + 1]] else { continue } totalDistance += haversineDistanceMiles(from: from, to: to) } return totalDistance } /// Haversine distance between two coordinates in miles static func haversineDistanceMiles( from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ) -> Double { let earthRadiusMiles = TestConstants.earthRadiusMiles let lat1 = from.latitude * .pi / 180 let lat2 = to.latitude * .pi / 180 let deltaLat = (to.latitude - from.latitude) * .pi / 180 let deltaLon = (to.longitude - from.longitude) * .pi / 180 let a = sin(deltaLat / 2) * sin(deltaLat / 2) + cos(lat1) * cos(lat2) * sin(deltaLon / 2) * sin(deltaLon / 2) let c = 2 * atan2(sqrt(a), sqrt(1 - a)) return earthRadiusMiles * c } // MARK: - Permutation Generation /// Generate all permutations of an array (Heap's algorithm) static func permutations(of array: [T]) -> [[T]] { var result: [[T]] = [] var arr = array func generate(_ n: Int) { if n == 1 { result.append(arr) return } for i in 0.. Int { guard n > 1 else { return 1 } return (1...n).reduce(1, *) } } // MARK: - Convenience Extensions extension BruteForceRouteVerifier { /// Verify a trip's route is optimal static func verifyTrip(_ trip: Trip) -> VerificationResult { var stops: [String: CLLocationCoordinate2D] = [:] for stop in trip.stops { if let coord = stop.coordinate { stops[stop.id.uuidString] = coord } } let routeIds = trip.stops.map { $0.id.uuidString } return verify(proposedRoute: routeIds, stops: stops) } /// Verify a list of stadiums forms an optimal route static func verifyStadiumRoute(_ stadiums: [Stadium]) -> VerificationResult { let stops = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0.coordinate) }) let routeIds = stadiums.map { $0.id } return verify(proposedRoute: routeIds, stops: stops) } } // MARK: - Test Assertions extension BruteForceRouteVerifier.VerificationResult { /// Returns a detailed failure message if not optimal var failureMessage: String? { guard !isOptimal else { return nil } var message = "Route is not optimal. " message += "Proposed: \(String(format: "%.1f", proposedRouteDistance)) miles, " message += "Optimal: \(String(format: "%.1f", optimalRouteDistance)) miles" if let improvement = improvementPercentage { message += " (\(String(format: "%.1f", improvement))% longer)" } message += ". Checked \(permutationsChecked) permutations." return message } }