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>
This commit is contained in:
200
SportsTime/Planning/Validators/DrivingFeasibilityValidator.swift
Normal file
200
SportsTime/Planning/Validators/DrivingFeasibilityValidator.swift
Normal file
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user