fix(roadtrip): a night is over in the morning, not two driving days later

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.
This commit is contained in:
Maurice
2026-09-13 14:42:54 +02:00
parent 7197ec1c96
commit a89ad06ba5
2 changed files with 40 additions and 4 deletions
+29 -4
View File
@@ -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', () => {
+11
View File
@@ -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)) {