- Fix repeat-city travel placement: use stop indices instead of global city name matching so Follow Team trips with repeat cities show travel correctly - Add TravelPlacement helper and regression tests (7 tests) - Add alternate app icons for each theme, auto-switch on theme change - Fix index-out-of-range crash in AnimatedSportsBackground (19 configs, was iterating 20) - Add marketing video configs, engine, and new video components - Add docs and data exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
86 lines
2.7 KiB
Swift
86 lines
2.7 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)
|
|
defaultDay = minDay
|
|
} else {
|
|
minDay = 1
|
|
maxDay = tripDays.count
|
|
defaultDay = 1
|
|
}
|
|
|
|
let clampedDefault: Int
|
|
if minDay <= maxDay {
|
|
clampedDefault = max(minDay, min(defaultDay, maxDay))
|
|
} else {
|
|
clampedDefault = minDay
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|