Files
Sportstime/SportsTime/Planning/Validators/DrivingFeasibilityValidator.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

201 lines
6.9 KiB
Swift

//
// DrivingFeasibilityValidator.swift
// SportsTime
//
import Foundation
/// Validates driving feasibility based on daily hour limits.
/// Priority 4 in the rule hierarchy.
///
/// A route is valid ONLY IF:
/// - Daily driving time maxDailyDrivingHours for EVERY day
/// - Games are reachable between scheduled times
struct DrivingFeasibilityValidator {
// MARK: - Validation Result
struct ValidationResult {
let isValid: Bool
let violations: [ConstraintViolation]
let failedSegment: SegmentFailure?
static let valid = ValidationResult(isValid: true, violations: [], failedSegment: nil)
static func drivingExceeded(
segment: String,
requiredHours: Double,
limitHours: Double
) -> ValidationResult {
let violation = ConstraintViolation(
type: .drivingTime,
description: "\(segment) requires \(String(format: "%.1f", requiredHours)) hours driving (limit: \(String(format: "%.1f", limitHours)) hours)",
severity: .error
)
let failure = SegmentFailure(
segmentDescription: segment,
requiredHours: requiredHours,
limitHours: limitHours
)
return ValidationResult(isValid: false, violations: [violation], failedSegment: failure)
}
static func gameUnreachable(
gameId: UUID,
arrivalTime: Date,
gameTime: Date
) -> ValidationResult {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
let violation = ConstraintViolation(
type: .gameReachability,
description: "Cannot arrive (\(formatter.string(from: arrivalTime))) before game starts (\(formatter.string(from: gameTime)))",
severity: .error
)
return ValidationResult(isValid: false, violations: [violation], failedSegment: nil)
}
}
struct SegmentFailure {
let segmentDescription: String
let requiredHours: Double
let limitHours: Double
}
// MARK: - Properties
let constraints: DrivingConstraints
// MARK: - Initialization
init(constraints: DrivingConstraints = .default) {
self.constraints = constraints
}
init(from preferences: TripPreferences) {
self.constraints = DrivingConstraints(from: preferences)
}
// MARK: - Validation
/// Validates that a single travel segment is feasible within daily driving limits.
///
/// - Parameters:
/// - drivingHours: Required driving hours for this segment
/// - origin: Description of the origin location
/// - destination: Description of the destination location
/// - Returns: ValidationResult indicating if the segment is feasible
func validateSegment(
drivingHours: Double,
origin: String,
destination: String
) -> ValidationResult {
let maxDaily = constraints.maxDailyDrivingHours
// A segment is valid if it can be completed in one day OR
// can be split across multiple days with overnight stops
if drivingHours <= maxDaily {
return .valid
}
// Check if it can be reasonably split across multiple days
// We allow up to 2 driving days for a single segment
let maxTwoDayDriving = maxDaily * 2
if drivingHours <= maxTwoDayDriving {
return .valid
}
// Segment requires more than 2 days of driving - too long
return .drivingExceeded(
segment: "\(origin)\(destination)",
requiredHours: drivingHours,
limitHours: maxDaily
)
}
/// Validates that a game can be reached in time given departure time and driving duration.
///
/// - Parameters:
/// - gameId: ID of the game to reach
/// - gameTime: When the game starts
/// - departureTime: When we leave the previous stop
/// - drivingHours: Hours of driving required
/// - bufferHours: Buffer time needed before game (default 1 hour for parking, etc.)
/// - Returns: ValidationResult indicating if we can reach the game in time
func validateGameReachability(
gameId: UUID,
gameTime: Date,
departureTime: Date,
drivingHours: Double,
bufferHours: Double = 1.0
) -> ValidationResult {
// Calculate arrival time
let drivingSeconds = drivingHours * 3600
let bufferSeconds = bufferHours * 3600
let arrivalTime = departureTime.addingTimeInterval(drivingSeconds)
let requiredArrivalTime = gameTime.addingTimeInterval(-bufferSeconds)
if arrivalTime <= requiredArrivalTime {
return .valid
} else {
return .gameUnreachable(
gameId: gameId,
arrivalTime: arrivalTime,
gameTime: gameTime
)
}
}
/// Validates an entire itinerary's travel segments for driving feasibility.
///
/// - Parameter segments: Array of travel segments to validate
/// - Returns: ValidationResult with first failure found, or valid if all pass
func validateItinerary(segments: [TravelSegment]) -> ValidationResult {
for segment in segments {
let result = validateSegment(
drivingHours: segment.estimatedDrivingHours,
origin: segment.fromLocation.name,
destination: segment.toLocation.name
)
if !result.isValid {
return result
}
}
return .valid
}
/// Calculates how many travel days are needed for a given driving distance.
///
/// - Parameter drivingHours: Total hours of driving required
/// - Returns: Number of calendar days the travel will span
func travelDaysRequired(for drivingHours: Double) -> Int {
let maxDaily = constraints.maxDailyDrivingHours
if drivingHours <= maxDaily {
return 1
}
return Int(ceil(drivingHours / maxDaily))
}
/// Determines if an overnight stop is needed between two points.
///
/// - Parameters:
/// - drivingHours: Hours of driving between points
/// - departureTime: When we plan to leave
/// - Returns: True if an overnight stop is recommended
func needsOvernightStop(drivingHours: Double, departureTime: Date) -> Bool {
// If driving exceeds daily limit, we need an overnight
if drivingHours > constraints.maxDailyDrivingHours {
return true
}
// Also check if arrival would be unreasonably late
let arrivalTime = departureTime.addingTimeInterval(drivingHours * 3600)
let calendar = Calendar.current
let arrivalHour = calendar.component(.hour, from: arrivalTime)
// Arriving after 11 PM suggests we should have stopped
return arrivalHour >= 23
}
}