From a89ad06ba5b739202617a1b6f665a47b0c2dba68 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 13 Sep 2026 14:42:54 +0200 Subject: [PATCH] fix(roadtrip): a night is over in the morning, not two driving days later MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With daily travel times set, a stay was spent only against the hours the window is open. Twenty-four hours then took two of them to run down: a single night ended on the day after next, and every stop behind it moved with it. Night time is time. The hours between one window closing and the next opening now count towards the stay, so a night that starts just after eight is over just after eight the next morning and the day carries on from there. An eleven-hour visit from eight is over at seven in the evening rather than saving its last hour for tomorrow — the traveller is still there after the driving stops, they simply do not drive on until the window opens again. --- shared/src/roadtrip/dayWindow.spec.ts | 33 +++++++++++++++++++++++---- shared/src/roadtrip/dayWindow.ts | 11 +++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/shared/src/roadtrip/dayWindow.spec.ts b/shared/src/roadtrip/dayWindow.spec.ts index af89057d7..14c75e1c4 100644 --- a/shared/src/roadtrip/dayWindow.spec.ts +++ b/shared/src/roadtrip/dayWindow.spec.ts @@ -308,16 +308,40 @@ describe('daily travel window', () => { expect(planned.issue).toBe('conflict'); }); - it('pauses a long visit at its location and carries the remaining stay into tomorrow', () => { + it('pauses a long visit at its location and resumes the next morning', () => { + // Eleven hours from eight in the morning is over at seven in the evening, an hour + // after the window shuts. The stay does not pause overnight and pick up its last + // hour tomorrow — night time is time — so the morning starts free. const first = stop(1, { dwellMinutes: 660 }); const planned = calculate([first, stop(2)], [60]); expect(planned.chains[0]!.stops[1]!).toMatchObject({ lat: first.lat, lng: first.lng }); expect(planned.chains[0]!.schedule.entries[0]!.departure).toBe('18:00'); - expect(planned.chains[1]!.schedule.entries[0]!.departure).toBe('09:00'); - expect(planned.chains[1]!.schedule.entries[1]!.arrival).toBe('10:00'); + expect(planned.chains[1]!.schedule.entries[0]!.departure).toBe('08:00'); + expect(planned.chains[1]!.schedule.entries[1]!.arrival).toBe('09:00'); expect(first.dwellMinutes).toBe(660); }); + it('a night booked as a full day is over the next morning, not two days later', () => { + // Twenty-four hours from just after eight is over just after eight tomorrow. Counted + // against the driving window alone it took two of them to run down, so a single + // night pushed everything after it onto the day after next. + const hotel = stop(1, { time: '08:04', dwellMinutes: 24 * 60 }); + const planned = planDayWindow( + [day(1, [hotel, stop(2)]), day(2), day(3)], + { start: 480, end: 1200 }, + (a, b) => leg(10, a, b), + 'metric', + labels, + ); + + expect(planned.issue).toBeNull(); + // One night inserted, not two: the stay ends inside the next day's window. + const dayTwo = planned.chains.find(c => c.dayNumber === 2)!; + expect(dayTwo.schedule.entries.find(e => e.departure === '08:04')).toBeTruthy(); + // And the stop after it is reached that same morning. + expect(dayTwo.stops.some(s => s.assignmentId === 2)).toBe(true); + }); + it('does not place a night pause halfway through a ferry crossing', () => { const a = stop(1); const b = stop(2); @@ -337,7 +361,8 @@ describe('daily travel window', () => { it('bounds extreme driving and stay durations', () => { expect(calculate([stop(1), stop(2)], [600 * 367]).issue).toBe('tooLong'); - expect(calculate([stop(1, { dwellMinutes: 600 * 367 }), stop(2)], [1]).issue).toBe('tooLong'); + // Counted against the clock, so a year of standing still is 1440 minutes a day. + expect(calculate([stop(1, { dwellMinutes: 1440 * 400 }), stop(2)], [1]).issue).toBe('tooLong'); }); it('maps insertions on generated days back to real stored stops', () => { diff --git a/shared/src/roadtrip/dayWindow.ts b/shared/src/roadtrip/dayWindow.ts index 196f8e63e..8d76427db 100644 --- a/shared/src/roadtrip/dayWindow.ts +++ b/shared/src/roadtrip/dayWindow.ts @@ -277,6 +277,11 @@ export function planDayWindow( append(stop, clock); previous = stop; + // How long the traveller is here, spent against the clock rather than against the + // driving hours. The night between two of them is not a pause in the stay: standing + // somewhere for twenty-four hours takes twenty-four hours, and counting only the + // hours the window is open stretched a single night over three days. + const overnight = Math.max(0, 1440 - window.end + window.start); let dwell = Math.max(0, stop.dwellMinutes ?? 0); while (dwell > 0) { const remaining = targets.has(number) ? dwell : Math.max(0, window.end - clock); @@ -288,6 +293,12 @@ export function planDayWindow( if (dwell > 0) { previous = night(previous); if (issue) return failed(issue); + // The hours the window was shut passed too. + dwell = Math.max(0, dwell - overnight); + if (dwell === 0) { + const resumed = chainAt(number).schedule.entries; + resumed[resumed.length - 1]!.departure = formatClock(Math.round(clock)); + } } } if (target === position || (!targets.has(number) && stop.endDay && i < stops.length - 1)) {