- Replace O(2^n) GeographicRouteExplorer with O(n) GameDAGRouter using DAG + beam search - Add geographic diversity to route selection (returns routes from distinct regions) - Add trip options selector UI (TripOptionsView, TripOptionCard) to choose between routes - Simplify itinerary display: separate games and travel segments by date - Remove complex ItineraryDay bundling, query games/travel directly per day - Update ScenarioA/B/C planners to use GameDAGRouter - Add new test suites for planners and travel estimator 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
586 lines
25 KiB
Swift
586 lines
25 KiB
Swift
//
|
|
// TravelEstimatorTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// 50 comprehensive tests for TravelEstimator covering:
|
|
// - Haversine distance calculations (miles and meters)
|
|
// - Travel segment estimation from stops
|
|
// - Travel segment estimation from LocationInputs
|
|
// - Fallback distance when coordinates missing
|
|
// - Travel day calculations
|
|
// - Edge cases and boundary conditions
|
|
//
|
|
|
|
import Testing
|
|
@testable import SportsTime
|
|
import Foundation
|
|
import CoreLocation
|
|
|
|
// MARK: - TravelEstimator Tests
|
|
|
|
struct TravelEstimatorTests {
|
|
|
|
// MARK: - Test Data Helpers
|
|
|
|
private func makeStop(
|
|
city: String,
|
|
latitude: Double? = nil,
|
|
longitude: Double? = nil,
|
|
arrivalDate: Date = Date(),
|
|
departureDate: Date? = nil
|
|
) -> ItineraryStop {
|
|
let coordinate = (latitude != nil && longitude != nil)
|
|
? CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
|
: nil
|
|
|
|
let location = LocationInput(
|
|
name: city,
|
|
coordinate: coordinate,
|
|
address: nil
|
|
)
|
|
|
|
return ItineraryStop(
|
|
city: city,
|
|
state: "ST",
|
|
coordinate: coordinate,
|
|
games: [],
|
|
arrivalDate: arrivalDate,
|
|
departureDate: departureDate ?? arrivalDate,
|
|
location: location,
|
|
firstGameStart: nil
|
|
)
|
|
}
|
|
|
|
private func makeLocation(
|
|
name: String,
|
|
latitude: Double? = nil,
|
|
longitude: Double? = nil
|
|
) -> LocationInput {
|
|
let coordinate = (latitude != nil && longitude != nil)
|
|
? CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
|
: nil
|
|
|
|
return LocationInput(name: name, coordinate: coordinate, address: nil)
|
|
}
|
|
|
|
private func defaultConstraints() -> DrivingConstraints {
|
|
DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)
|
|
}
|
|
|
|
private func twoDriverConstraints() -> DrivingConstraints {
|
|
DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
|
}
|
|
|
|
// MARK: - Haversine Distance (Miles) Tests
|
|
|
|
@Test("haversineDistanceMiles - same point returns zero")
|
|
func haversine_SamePoint_ReturnsZero() {
|
|
let coord = CLLocationCoordinate2D(latitude: 40.0, longitude: -100.0)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: coord, to: coord)
|
|
#expect(distance == 0.0)
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - LA to SF approximately 350 miles")
|
|
func haversine_LAToSF_ApproximatelyCorrect() {
|
|
let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
|
|
let sf = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: la, to: sf)
|
|
|
|
// Known distance is ~347 miles
|
|
#expect(distance > 340 && distance < 360, "Expected ~350 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - NY to LA approximately 2450 miles")
|
|
func haversine_NYToLA_ApproximatelyCorrect() {
|
|
let ny = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060)
|
|
let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: ny, to: la)
|
|
|
|
// Known distance is ~2450 miles
|
|
#expect(distance > 2400 && distance < 2500, "Expected ~2450 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - commutative (A to B equals B to A)")
|
|
func haversine_Commutative() {
|
|
let coord1 = CLLocationCoordinate2D(latitude: 40.0, longitude: -100.0)
|
|
let coord2 = CLLocationCoordinate2D(latitude: 35.0, longitude: -90.0)
|
|
|
|
let distance1 = TravelEstimator.haversineDistanceMiles(from: coord1, to: coord2)
|
|
let distance2 = TravelEstimator.haversineDistanceMiles(from: coord2, to: coord1)
|
|
|
|
#expect(abs(distance1 - distance2) < 0.001)
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - across equator")
|
|
func haversine_AcrossEquator() {
|
|
let north = CLLocationCoordinate2D(latitude: 10.0, longitude: -80.0)
|
|
let south = CLLocationCoordinate2D(latitude: -10.0, longitude: -80.0)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: north, to: south)
|
|
|
|
// 20 degrees latitude ≈ 1380 miles
|
|
#expect(distance > 1350 && distance < 1400, "Expected ~1380 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - across prime meridian")
|
|
func haversine_AcrossPrimeMeridian() {
|
|
let west = CLLocationCoordinate2D(latitude: 51.5, longitude: -1.0)
|
|
let east = CLLocationCoordinate2D(latitude: 51.5, longitude: 1.0)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: west, to: east)
|
|
|
|
// 2 degrees longitude at ~51.5° latitude ≈ 85 miles
|
|
#expect(distance > 80 && distance < 90, "Expected ~85 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - near north pole")
|
|
func haversine_NearNorthPole() {
|
|
let coord1 = CLLocationCoordinate2D(latitude: 89.0, longitude: 0.0)
|
|
let coord2 = CLLocationCoordinate2D(latitude: 89.0, longitude: 180.0)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: coord1, to: coord2)
|
|
|
|
// At 89° latitude, half way around the world is very short
|
|
#expect(distance > 0 && distance < 150, "Distance near pole should be short, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - Chicago to Denver approximately 920 miles")
|
|
func haversine_ChicagoToDenver() {
|
|
let chicago = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)
|
|
let denver = CLLocationCoordinate2D(latitude: 39.7392, longitude: -104.9903)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: chicago, to: denver)
|
|
|
|
// Known distance ~920 miles
|
|
#expect(distance > 900 && distance < 940, "Expected ~920 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - very short distance (same city)")
|
|
func haversine_VeryShortDistance() {
|
|
let point1 = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855) // Times Square
|
|
let point2 = CLLocationCoordinate2D(latitude: 40.7614, longitude: -73.9776) // Grand Central
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: point1, to: point2)
|
|
|
|
// ~0.5 miles
|
|
#expect(distance > 0.4 && distance < 0.6, "Expected ~0.5 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("haversineDistanceMiles - extreme longitude difference")
|
|
func haversine_ExtremeLongitudeDifference() {
|
|
let west = CLLocationCoordinate2D(latitude: 40.0, longitude: -179.0)
|
|
let east = CLLocationCoordinate2D(latitude: 40.0, longitude: 179.0)
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: west, to: east)
|
|
|
|
// 358 degrees the long way, 2 degrees the short way
|
|
// At 40° latitude, 2 degrees ≈ 105 miles
|
|
#expect(distance > 100 && distance < 110, "Expected ~105 miles, got \(distance)")
|
|
}
|
|
|
|
// MARK: - Haversine Distance (Meters) Tests
|
|
|
|
@Test("haversineDistanceMeters - same point returns zero")
|
|
func haversineMeters_SamePoint_ReturnsZero() {
|
|
let coord = CLLocationCoordinate2D(latitude: 40.0, longitude: -100.0)
|
|
let distance = TravelEstimator.haversineDistanceMeters(from: coord, to: coord)
|
|
#expect(distance == 0.0)
|
|
}
|
|
|
|
@Test("haversineDistanceMeters - LA to SF approximately 560 km")
|
|
func haversineMeters_LAToSF() {
|
|
let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
|
|
let sf = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
|
|
let distanceKm = TravelEstimator.haversineDistanceMeters(from: la, to: sf) / 1000
|
|
|
|
#expect(distanceKm > 540 && distanceKm < 580, "Expected ~560 km, got \(distanceKm)")
|
|
}
|
|
|
|
@Test("haversineDistanceMeters - consistency with miles conversion")
|
|
func haversineMeters_ConsistentWithMiles() {
|
|
let coord1 = CLLocationCoordinate2D(latitude: 40.0, longitude: -100.0)
|
|
let coord2 = CLLocationCoordinate2D(latitude: 35.0, longitude: -90.0)
|
|
|
|
let miles = TravelEstimator.haversineDistanceMiles(from: coord1, to: coord2)
|
|
let meters = TravelEstimator.haversineDistanceMeters(from: coord1, to: coord2)
|
|
|
|
// 1 mile = 1609.34 meters
|
|
let milesFromMeters = meters / 1609.34
|
|
#expect(abs(miles - milesFromMeters) < 1.0)
|
|
}
|
|
|
|
@Test("haversineDistanceMeters - one kilometer distance")
|
|
func haversineMeters_OneKilometer() {
|
|
// 1 degree latitude ≈ 111 km, so 0.009 degrees ≈ 1 km
|
|
let coord1 = CLLocationCoordinate2D(latitude: 40.0, longitude: -100.0)
|
|
let coord2 = CLLocationCoordinate2D(latitude: 40.009, longitude: -100.0)
|
|
let meters = TravelEstimator.haversineDistanceMeters(from: coord1, to: coord2)
|
|
|
|
#expect(meters > 900 && meters < 1100, "Expected ~1000 meters, got \(meters)")
|
|
}
|
|
|
|
// MARK: - Calculate Distance Miles Tests
|
|
|
|
@Test("calculateDistanceMiles - with coordinates uses haversine")
|
|
func calculateDistance_WithCoordinates_UsesHaversine() {
|
|
let stop1 = makeStop(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let stop2 = makeStop(city: "San Francisco", latitude: 37.7749, longitude: -122.4194)
|
|
|
|
let distance = TravelEstimator.calculateDistanceMiles(from: stop1, to: stop2)
|
|
|
|
// Haversine ~350 miles * 1.3 routing factor ≈ 455 miles
|
|
#expect(distance > 440 && distance < 470, "Expected ~455 miles with routing factor, got \(distance)")
|
|
}
|
|
|
|
@Test("calculateDistanceMiles - without coordinates uses fallback")
|
|
func calculateDistance_WithoutCoordinates_UsesFallback() {
|
|
let stop1 = makeStop(city: "CityA")
|
|
let stop2 = makeStop(city: "CityB")
|
|
|
|
let distance = TravelEstimator.calculateDistanceMiles(from: stop1, to: stop2)
|
|
|
|
// Fallback is 300 miles
|
|
#expect(distance == 300.0, "Expected fallback of 300 miles, got \(distance)")
|
|
}
|
|
|
|
@Test("calculateDistanceMiles - same city returns zero")
|
|
func calculateDistance_SameCity_ReturnsZero() {
|
|
let stop1 = makeStop(city: "Chicago")
|
|
let stop2 = makeStop(city: "Chicago")
|
|
|
|
let distance = TravelEstimator.calculateDistanceMiles(from: stop1, to: stop2)
|
|
#expect(distance == 0.0)
|
|
}
|
|
|
|
@Test("calculateDistanceMiles - one stop missing coordinates uses fallback")
|
|
func calculateDistance_OneMissingCoordinate_UsesFallback() {
|
|
let stop1 = makeStop(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let stop2 = makeStop(city: "San Francisco")
|
|
|
|
let distance = TravelEstimator.calculateDistanceMiles(from: stop1, to: stop2)
|
|
#expect(distance == 300.0, "Expected fallback of 300 miles, got \(distance)")
|
|
}
|
|
|
|
// MARK: - Estimate Fallback Distance Tests
|
|
|
|
@Test("estimateFallbackDistance - same city returns zero")
|
|
func fallbackDistance_SameCity_ReturnsZero() {
|
|
let stop1 = makeStop(city: "Denver")
|
|
let stop2 = makeStop(city: "Denver")
|
|
|
|
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
|
#expect(distance == 0.0)
|
|
}
|
|
|
|
@Test("estimateFallbackDistance - different cities returns 300")
|
|
func fallbackDistance_DifferentCities_Returns300() {
|
|
let stop1 = makeStop(city: "Denver")
|
|
let stop2 = makeStop(city: "Chicago")
|
|
|
|
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
|
#expect(distance == 300.0)
|
|
}
|
|
|
|
@Test("estimateFallbackDistance - case sensitive city names")
|
|
func fallbackDistance_CaseSensitive() {
|
|
let stop1 = makeStop(city: "denver")
|
|
let stop2 = makeStop(city: "Denver")
|
|
|
|
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
|
// Different case means different cities
|
|
#expect(distance == 300.0)
|
|
}
|
|
|
|
// MARK: - Estimate (from Stops) Tests
|
|
|
|
@Test("estimate stops - returns valid segment for short trip")
|
|
func estimateStops_ShortTrip_ReturnsSegment() {
|
|
let stop1 = makeStop(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let stop2 = makeStop(city: "San Diego", latitude: 32.7157, longitude: -117.1611)
|
|
|
|
let segment = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil, "Should return segment for short trip")
|
|
#expect(segment!.travelMode == .drive)
|
|
#expect(segment!.durationHours < 8.0, "LA to SD should be under 8 hours")
|
|
}
|
|
|
|
@Test("estimate stops - returns nil for extremely long trip")
|
|
func estimateStops_ExtremelyLongTrip_ReturnsNil() {
|
|
// Create stops 4000 miles apart (> 2 days of driving at 60mph)
|
|
let stop1 = makeStop(city: "New York", latitude: 40.7128, longitude: -74.0060)
|
|
// Point way out in the Pacific
|
|
let stop2 = makeStop(city: "Far Away", latitude: 35.0, longitude: -170.0)
|
|
|
|
let segment = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
|
|
#expect(segment == nil, "Should return nil for trip > 2 days of driving")
|
|
}
|
|
|
|
@Test("estimate stops - respects two-driver constraint")
|
|
func estimateStops_TwoDrivers_IncreasesCapacity() {
|
|
// Trip that exceeds 1-driver limit (16h) but fits 2-driver limit (32h)
|
|
// LA to Denver: ~850mi straight line * 1.3 routing = ~1105mi / 60mph = ~18.4 hours
|
|
let stop1 = makeStop(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let stop2 = makeStop(city: "Denver", latitude: 39.7392, longitude: -104.9903)
|
|
|
|
let oneDriver = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
let twoDrivers = TravelEstimator.estimate(from: stop1, to: stop2, constraints: twoDriverConstraints())
|
|
|
|
// ~18 hours exceeds 1-driver limit (16h max over 2 days) but fits 2-driver (32h)
|
|
#expect(oneDriver == nil, "Should fail with one driver - exceeds 16h limit")
|
|
#expect(twoDrivers != nil, "Should succeed with two drivers - within 32h limit")
|
|
}
|
|
|
|
@Test("estimate stops - calculates departure and arrival times")
|
|
func estimateStops_CalculatesTimes() {
|
|
let baseDate = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))!
|
|
let stop1 = makeStop(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437, departureDate: baseDate)
|
|
let stop2 = makeStop(city: "San Diego", latitude: 32.7157, longitude: -117.1611)
|
|
|
|
let segment = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil)
|
|
#expect(segment!.departureTime > baseDate, "Departure should be after base date (adds 8 hours)")
|
|
#expect(segment!.arrivalTime > segment!.departureTime, "Arrival should be after departure")
|
|
}
|
|
|
|
@Test("estimate stops - distance and duration are consistent")
|
|
func estimateStops_DistanceDurationConsistent() {
|
|
let stop1 = makeStop(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
|
|
let stop2 = makeStop(city: "Detroit", latitude: 42.3314, longitude: -83.0458)
|
|
|
|
let segment = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil)
|
|
// At 60 mph average, hours = miles / 60
|
|
let expectedHours = segment!.distanceMiles / 60.0
|
|
#expect(abs(segment!.durationHours - expectedHours) < 0.01)
|
|
}
|
|
|
|
@Test("estimate stops - zero distance same location")
|
|
func estimateStops_SameLocation_ZeroDistance() {
|
|
let stop1 = makeStop(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
|
|
let stop2 = makeStop(city: "Chicago", latitude: 41.8781, longitude: -87.6298)
|
|
|
|
let segment = TravelEstimator.estimate(from: stop1, to: stop2, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil)
|
|
#expect(segment!.distanceMiles == 0.0)
|
|
#expect(segment!.durationHours == 0.0)
|
|
}
|
|
|
|
// MARK: - Estimate (from LocationInputs) Tests
|
|
|
|
@Test("estimate locations - returns valid segment")
|
|
func estimateLocations_ValidLocations_ReturnsSegment() {
|
|
let from = makeLocation(name: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let to = makeLocation(name: "San Diego", latitude: 32.7157, longitude: -117.1611)
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil)
|
|
#expect(segment!.fromLocation.name == "Los Angeles")
|
|
#expect(segment!.toLocation.name == "San Diego")
|
|
}
|
|
|
|
@Test("estimate locations - returns nil for missing from coordinate")
|
|
func estimateLocations_MissingFromCoordinate_ReturnsNil() {
|
|
let from = makeLocation(name: "Unknown City")
|
|
let to = makeLocation(name: "San Diego", latitude: 32.7157, longitude: -117.1611)
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment == nil)
|
|
}
|
|
|
|
@Test("estimate locations - returns nil for missing to coordinate")
|
|
func estimateLocations_MissingToCoordinate_ReturnsNil() {
|
|
let from = makeLocation(name: "Los Angeles", latitude: 34.0522, longitude: -118.2437)
|
|
let to = makeLocation(name: "Unknown City")
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment == nil)
|
|
}
|
|
|
|
@Test("estimate locations - returns nil for both missing coordinates")
|
|
func estimateLocations_BothMissingCoordinates_ReturnsNil() {
|
|
let from = makeLocation(name: "Unknown A")
|
|
let to = makeLocation(name: "Unknown B")
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment == nil)
|
|
}
|
|
|
|
@Test("estimate locations - applies road routing factor")
|
|
func estimateLocations_AppliesRoutingFactor() {
|
|
let from = makeLocation(name: "A", latitude: 40.0, longitude: -100.0)
|
|
let to = makeLocation(name: "B", latitude: 41.0, longitude: -100.0)
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment != nil)
|
|
// Straight line distance * 1.3 routing factor
|
|
let straightLineMeters = TravelEstimator.haversineDistanceMeters(
|
|
from: from.coordinate!, to: to.coordinate!
|
|
)
|
|
let expectedMeters = straightLineMeters * 1.3
|
|
#expect(abs(segment!.distanceMeters - expectedMeters) < 1.0)
|
|
}
|
|
|
|
@Test("estimate locations - returns nil for extremely long trip")
|
|
func estimateLocations_ExtremelyLongTrip_ReturnsNil() {
|
|
let from = makeLocation(name: "New York", latitude: 40.7128, longitude: -74.0060)
|
|
let to = makeLocation(name: "Far Pacific", latitude: 35.0, longitude: -170.0)
|
|
|
|
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints())
|
|
|
|
#expect(segment == nil)
|
|
}
|
|
|
|
// MARK: - Calculate Travel Days Tests
|
|
|
|
@Test("calculateTravelDays - short trip returns single day")
|
|
func travelDays_ShortTrip_ReturnsOneDay() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 4.0)
|
|
|
|
#expect(days.count == 1)
|
|
}
|
|
|
|
@Test("calculateTravelDays - exactly 8 hours returns single day")
|
|
func travelDays_EightHours_ReturnsOneDay() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.0)
|
|
|
|
#expect(days.count == 1)
|
|
}
|
|
|
|
@Test("calculateTravelDays - 9 hours returns two days")
|
|
func travelDays_NineHours_ReturnsTwoDays() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 9.0)
|
|
|
|
#expect(days.count == 2)
|
|
}
|
|
|
|
@Test("calculateTravelDays - 16 hours returns two days")
|
|
func travelDays_SixteenHours_ReturnsTwoDays() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 16.0)
|
|
|
|
#expect(days.count == 2)
|
|
}
|
|
|
|
@Test("calculateTravelDays - 17 hours returns three days")
|
|
func travelDays_SeventeenHours_ReturnsThreeDays() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 17.0)
|
|
|
|
#expect(days.count == 3)
|
|
}
|
|
|
|
@Test("calculateTravelDays - zero hours returns single day")
|
|
func travelDays_ZeroHours_ReturnsOneDay() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 0.0)
|
|
|
|
// ceil(0 / 8) = 0, but we always start with one day
|
|
#expect(days.count == 1)
|
|
}
|
|
|
|
@Test("calculateTravelDays - days are at start of day")
|
|
func travelDays_DaysAreAtStartOfDay() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 14, minute: 30))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 10.0)
|
|
|
|
#expect(days.count == 2)
|
|
|
|
let cal = Calendar.current
|
|
for day in days {
|
|
let hour = cal.component(.hour, from: day)
|
|
let minute = cal.component(.minute, from: day)
|
|
#expect(hour == 0 && minute == 0, "Day should be at midnight")
|
|
}
|
|
}
|
|
|
|
@Test("calculateTravelDays - consecutive days are correct")
|
|
func travelDays_ConsecutiveDays() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 20.0)
|
|
|
|
#expect(days.count == 3)
|
|
|
|
let cal = Calendar.current
|
|
#expect(cal.component(.day, from: days[0]) == 5)
|
|
#expect(cal.component(.day, from: days[1]) == 6)
|
|
#expect(cal.component(.day, from: days[2]) == 7)
|
|
}
|
|
|
|
@Test("calculateTravelDays - handles month boundary")
|
|
func travelDays_HandleMonthBoundary() {
|
|
let departure = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 30, hour: 8))!
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 10.0)
|
|
|
|
#expect(days.count == 2)
|
|
|
|
let cal = Calendar.current
|
|
#expect(cal.component(.month, from: days[0]) == 4)
|
|
#expect(cal.component(.day, from: days[0]) == 30)
|
|
#expect(cal.component(.month, from: days[1]) == 5)
|
|
#expect(cal.component(.day, from: days[1]) == 1)
|
|
}
|
|
|
|
// MARK: - Driving Constraints Tests
|
|
|
|
@Test("DrivingConstraints - default values")
|
|
func constraints_DefaultValues() {
|
|
let constraints = DrivingConstraints.default
|
|
#expect(constraints.numberOfDrivers == 1)
|
|
#expect(constraints.maxHoursPerDriverPerDay == 8.0)
|
|
#expect(constraints.maxDailyDrivingHours == 8.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints - multiple drivers increase daily limit")
|
|
func constraints_MultipleDrivers() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
|
#expect(constraints.maxDailyDrivingHours == 16.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints - custom hours per driver")
|
|
func constraints_CustomHoursPerDriver() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 10.0)
|
|
#expect(constraints.maxDailyDrivingHours == 10.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints - enforces minimum 1 driver")
|
|
func constraints_MinimumOneDriver() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 0, maxHoursPerDriverPerDay: 8.0)
|
|
#expect(constraints.numberOfDrivers == 1)
|
|
}
|
|
|
|
@Test("DrivingConstraints - enforces minimum 1 hour")
|
|
func constraints_MinimumOneHour() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 0.5)
|
|
#expect(constraints.maxHoursPerDriverPerDay == 1.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints - from preferences")
|
|
func constraints_FromPreferences() {
|
|
var prefs = TripPreferences()
|
|
prefs.numberOfDrivers = 3
|
|
prefs.maxDrivingHoursPerDriver = 6.0
|
|
|
|
let constraints = DrivingConstraints(from: prefs)
|
|
#expect(constraints.numberOfDrivers == 3)
|
|
#expect(constraints.maxHoursPerDriverPerDay == 6.0)
|
|
#expect(constraints.maxDailyDrivingHours == 18.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints - from preferences with nil hours uses default")
|
|
func constraints_FromPreferencesNilHours() {
|
|
var prefs = TripPreferences()
|
|
prefs.numberOfDrivers = 2
|
|
prefs.maxDrivingHoursPerDriver = nil
|
|
|
|
let constraints = DrivingConstraints(from: prefs)
|
|
#expect(constraints.maxHoursPerDriverPerDay == 8.0)
|
|
}
|
|
}
|