fix: correct travel segment placement for next-day departures
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>
This commit is contained in:
@@ -45,7 +45,9 @@ enum TravelPlacement {
|
||||
|
||||
minDay = max(fromDayNum + 1, 1)
|
||||
maxDay = min(toDayNum, tripDays.count)
|
||||
defaultDay = minDay
|
||||
// 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
|
||||
@@ -56,7 +58,9 @@ enum TravelPlacement {
|
||||
if minDay <= maxDay {
|
||||
clampedDefault = max(minDay, min(defaultDay, maxDay))
|
||||
} else {
|
||||
clampedDefault = minDay
|
||||
// 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
|
||||
|
||||
@@ -827,46 +827,21 @@ struct TripDetailView: View {
|
||||
var sections: [ItinerarySection] = []
|
||||
let days = tripDays
|
||||
|
||||
// Pre-calculate 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] → trip.stops[i+1].
|
||||
var travelByDay: [Int: TravelSegment] = [:]
|
||||
// Use TravelPlacement for consistent day calculation (shared with tests).
|
||||
var travelByDay = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days)
|
||||
|
||||
// Apply user overrides on top of computed defaults.
|
||||
for (segmentIndex, segment) in trip.travelSegments.enumerated() {
|
||||
let travelId = stableTravelAnchorId(segment)
|
||||
guard let override = travelOverrides[travelId] else { continue }
|
||||
|
||||
// Use stop dates for precise placement (handles repeat cities)
|
||||
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)
|
||||
let toDayNum = dayNumber(for: toStop.arrivalDate)
|
||||
|
||||
// Travel goes after the from stop's last day, up to the to stop's first day
|
||||
minDay = max(fromDayNum + 1, 1)
|
||||
maxDay = min(toDayNum, days.count)
|
||||
defaultDay = minDay
|
||||
} else {
|
||||
// Fallback: segment doesn't align with stops
|
||||
minDay = 1
|
||||
maxDay = days.count
|
||||
defaultDay = 1
|
||||
}
|
||||
|
||||
let validRange = minDay <= maxDay ? minDay...maxDay : minDay...minDay
|
||||
|
||||
// Check for user override - only use if within valid range
|
||||
if let override = travelOverrides[travelId],
|
||||
// Validate override is within valid day range
|
||||
if let validRange = validDayRange(for: travelId),
|
||||
validRange.contains(override.day) {
|
||||
// Remove from computed position
|
||||
travelByDay = travelByDay.filter { $0.value.id != segment.id }
|
||||
// Place at overridden position
|
||||
travelByDay[override.day] = segment
|
||||
} else {
|
||||
// Use default (clamped to valid range)
|
||||
let clampedDefault = max(validRange.lowerBound, min(defaultDay, validRange.upperBound))
|
||||
travelByDay[clampedDefault] = segment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user