Travel segments appeared one day too late on featured/suggested trips when stops had next-morning departures. The placement formula double- counted by using fromDayNum+1 as both minDay and defaultDay, then the invalid-range fallback used minDay (the overshooting value) instead of the arrival day. Also replaced TripDetailView's inline copy of the placement logic with TravelPlacement.computeTravelByDay() so the UI uses the same tested algorithm. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
3.0 KiB
Swift
90 lines
3.0 KiB
Swift
//
|
|
// TravelPlacement.swift
|
|
// SportsTime
|
|
//
|
|
// Computes which day number each travel segment should be displayed on.
|
|
// Extracted from TripDetailView for testability.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
enum TravelPlacement {
|
|
|
|
/// Result of computing travel placement for a trip.
|
|
struct Placement {
|
|
let day: Int
|
|
let segmentIndex: Int
|
|
}
|
|
|
|
/// Computes which day each travel segment belongs to.
|
|
///
|
|
/// Uses stop indices (not city name matching) so repeat cities work correctly.
|
|
/// `trip.travelSegments[i]` connects `trip.stops[i]` to `trip.stops[i+1]`.
|
|
///
|
|
/// - Parameters:
|
|
/// - trip: The trip containing stops and travel segments.
|
|
/// - tripDays: Array of dates (one per trip day, start-of-day normalized).
|
|
/// - Returns: Dictionary mapping day number (1-based) to TravelSegment.
|
|
static func computeTravelByDay(
|
|
trip: Trip,
|
|
tripDays: [Date]
|
|
) -> [Int: TravelSegment] {
|
|
var travelByDay: [Int: TravelSegment] = [:]
|
|
|
|
for (segmentIndex, segment) in trip.travelSegments.enumerated() {
|
|
let minDay: Int
|
|
let maxDay: Int
|
|
let defaultDay: Int
|
|
|
|
if segmentIndex < trip.stops.count - 1 {
|
|
let fromStop = trip.stops[segmentIndex]
|
|
let toStop = trip.stops[segmentIndex + 1]
|
|
|
|
let fromDayNum = dayNumber(for: fromStop.departureDate, in: tripDays)
|
|
let toDayNum = dayNumber(for: toStop.arrivalDate, in: tripDays)
|
|
|
|
minDay = max(fromDayNum + 1, 1)
|
|
maxDay = min(toDayNum, tripDays.count)
|
|
// Cap default at the arrival day to prevent overshoot when
|
|
// departure is already the next morning (fromDayNum+1 > toDayNum).
|
|
defaultDay = min(fromDayNum + 1, toDayNum)
|
|
} else {
|
|
minDay = 1
|
|
maxDay = tripDays.count
|
|
defaultDay = 1
|
|
}
|
|
|
|
let clampedDefault: Int
|
|
if minDay <= maxDay {
|
|
clampedDefault = max(minDay, min(defaultDay, maxDay))
|
|
} else {
|
|
// Invalid range (departure day is at or past arrival day).
|
|
// Fall back to arrival day, clamped within trip bounds.
|
|
clampedDefault = max(1, min(defaultDay, tripDays.count))
|
|
}
|
|
|
|
travelByDay[clampedDefault] = segment
|
|
}
|
|
|
|
return travelByDay
|
|
}
|
|
|
|
/// Convert a date to a 1-based day number within the trip days array.
|
|
/// Returns 0 if before trip start, tripDays.count + 1 if after trip end.
|
|
static func dayNumber(for date: Date, in tripDays: [Date]) -> Int {
|
|
let calendar = Calendar.current
|
|
let target = calendar.startOfDay(for: date)
|
|
|
|
for (index, tripDay) in tripDays.enumerated() {
|
|
if calendar.startOfDay(for: tripDay) == target {
|
|
return index + 1
|
|
}
|
|
}
|
|
|
|
if let firstDay = tripDays.first, target < firstDay {
|
|
return 0
|
|
}
|
|
return tripDays.count + 1
|
|
}
|
|
}
|