Add implementation code for all 4 improvement plan phases
Production changes: - TravelEstimator: remove 300mi fallback, return nil on missing coords - TripPlanningEngine: add warnings array, empty sports warning, inverted date range rejection, must-stop filter, segment validation gate - GameDAGRouter: add routePreference parameter with preference-aware bucket ordering and sorting in selectDiverseRoutes() - ScenarioA-E: pass routePreference through to GameDAGRouter - ScenarioA: track games with missing stadium data - ScenarioE: add region filtering for home games - TravelSegment: add requiresOvernightStop and travelDays() helpers Test changes: - GameDAGRouterTests: +252 lines for route preference verification - TripPlanningEngineTests: +153 lines for segment validation, date range, empty sports - ScenarioEPlannerTests: +119 lines for region filter tests - TravelEstimatorTests: remove obsolete fallback distance tests - ItineraryBuilderTests: update nil-coords test expectation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,20 +17,19 @@ import CoreLocation
|
||||
/// - Constants:
|
||||
/// - averageSpeedMph: 60 mph
|
||||
/// - roadRoutingFactor: 1.3 (accounts for roads vs straight-line distance)
|
||||
/// - fallbackDistanceMiles: 300 miles (when coordinates unavailable)
|
||||
///
|
||||
/// - Invariants:
|
||||
/// - All distance calculations are symmetric: distance(A,B) == distance(B,A)
|
||||
/// - Road distance >= straight-line distance (roadRoutingFactor >= 1.0)
|
||||
/// - Travel duration is always distance / averageSpeedMph
|
||||
/// - Segments exceeding 5x maxDailyDrivingHours return nil (unreachable)
|
||||
/// - Missing coordinates → returns nil (no guessing with fallback distances)
|
||||
enum TravelEstimator {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static let averageSpeedMph: Double = 60.0
|
||||
private static let roadRoutingFactor: Double = 1.3 // Straight line to road distance
|
||||
private static let fallbackDistanceMiles: Double = 300.0
|
||||
|
||||
// MARK: - Travel Estimation
|
||||
|
||||
@@ -44,7 +43,7 @@ enum TravelEstimator {
|
||||
///
|
||||
/// - Expected Behavior:
|
||||
/// - With valid coordinates → calculates distance using Haversine * roadRoutingFactor
|
||||
/// - Missing coordinates → uses fallback distance (300 miles)
|
||||
/// - Missing coordinates → returns nil (no fallback guessing)
|
||||
/// - Same city (no coords) → 0 distance, 0 duration
|
||||
/// - Driving hours > 5x maxDailyDrivingHours → returns nil
|
||||
/// - Duration = distance / 60 mph
|
||||
@@ -55,7 +54,21 @@ enum TravelEstimator {
|
||||
constraints: DrivingConstraints
|
||||
) -> TravelSegment? {
|
||||
|
||||
let distanceMiles = calculateDistanceMiles(from: from, to: to)
|
||||
// If either stop is missing coordinates, the segment is infeasible
|
||||
// (unless same city, which returns 0 distance)
|
||||
guard let distanceMiles = calculateDistanceMiles(from: from, to: to) else {
|
||||
// Same city with no coords: zero-distance segment
|
||||
if from.city == to.city {
|
||||
return TravelSegment(
|
||||
fromLocation: from.location,
|
||||
toLocation: to.location,
|
||||
travelMode: .drive,
|
||||
distanceMeters: 0,
|
||||
durationSeconds: 0
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let drivingHours = distanceMiles / averageSpeedMph
|
||||
|
||||
// Maximum allowed: 5 days of driving as a conservative hard cap.
|
||||
@@ -126,22 +139,20 @@ enum TravelEstimator {
|
||||
/// - Parameters:
|
||||
/// - from: Origin stop
|
||||
/// - to: Destination stop
|
||||
/// - Returns: Distance in miles
|
||||
/// - Returns: Distance in miles, or nil if coordinates are missing
|
||||
///
|
||||
/// - Expected Behavior:
|
||||
/// - Both have coordinates → Haversine distance * 1.3
|
||||
/// - Either missing coordinates → fallback distance
|
||||
/// - Same city (no coords) → 0 miles
|
||||
/// - Different cities (no coords) → 300 miles
|
||||
/// - Either missing coordinates → nil (no fallback guessing)
|
||||
static func calculateDistanceMiles(
|
||||
from: ItineraryStop,
|
||||
to: ItineraryStop
|
||||
) -> Double {
|
||||
if let fromCoord = from.coordinate,
|
||||
let toCoord = to.coordinate {
|
||||
return haversineDistanceMiles(from: fromCoord, to: toCoord) * roadRoutingFactor
|
||||
) -> Double? {
|
||||
guard let fromCoord = from.coordinate,
|
||||
let toCoord = to.coordinate else {
|
||||
return nil
|
||||
}
|
||||
return estimateFallbackDistance(from: from, to: to)
|
||||
return haversineDistanceMiles(from: fromCoord, to: toCoord) * roadRoutingFactor
|
||||
}
|
||||
|
||||
/// Calculates straight-line distance in miles using Haversine formula.
|
||||
@@ -206,24 +217,19 @@ enum TravelEstimator {
|
||||
return earthRadiusMeters * c
|
||||
}
|
||||
|
||||
/// Fallback distance when coordinates aren't available.
|
||||
// MARK: - Overnight Stop Detection
|
||||
|
||||
/// Determines if a travel segment requires an overnight stop.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: Origin stop
|
||||
/// - to: Destination stop
|
||||
/// - Returns: Estimated distance in miles
|
||||
///
|
||||
/// - Expected Behavior:
|
||||
/// - Same city → 0 miles
|
||||
/// - Different cities → 300 miles (fallback constant)
|
||||
static func estimateFallbackDistance(
|
||||
from: ItineraryStop,
|
||||
to: ItineraryStop
|
||||
) -> Double {
|
||||
if from.city == to.city {
|
||||
return 0
|
||||
}
|
||||
return fallbackDistanceMiles
|
||||
/// - segment: The travel segment to evaluate
|
||||
/// - constraints: Driving constraints (max daily hours)
|
||||
/// - Returns: true if driving hours exceed the daily limit
|
||||
static func requiresOvernightStop(
|
||||
segment: TravelSegment,
|
||||
constraints: DrivingConstraints
|
||||
) -> Bool {
|
||||
segment.estimatedDrivingHours > constraints.maxDailyDrivingHours
|
||||
}
|
||||
|
||||
// MARK: - Travel Days
|
||||
|
||||
Reference in New Issue
Block a user