Files
Sportstime/SportsTime/Features/Trip/Views/TravelPlacement.swift
Trey t 8e937a5646 feat: fix travel placement bug, add theme-based alternate icons, fix animated background crash
- 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>
2026-02-06 09:36:34 -06:00

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
}
}