fix(roadtrip): a check-in is a time somebody chose, so print it as one

The rail prints an hour in ink when a person picked it and in grey when the chain
worked it out. Every scheduler anchors on `time ?? checkInTime`, so a booked night
starts its day on the check-in exactly the way a pinned stop does — but two of the
three then asked only about `time` when labelling the result, and printed that
chosen hour grey.

The answer lived in three places and two of them disagreed with the anchor they
had just used, so it is one function now. Nothing but the rail reads the flag.

Only visible since a booked night became a stop of its own; before that no stop
carried a check-in.
This commit is contained in:
Maurice
2026-09-13 13:10:58 +02:00
parent 274dbea069
commit 6845db4cb8
4 changed files with 45 additions and 4 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ import type { RoadtripDayBoundary } from './day-boundary.schema';
import type { SpillChain } from './nightSpill';
import type { RoadtripDay, RoadtripStop, RoutedLeg } from './planning-types';
import type { DistanceUnit } from './planning-types';
import { formatClock, parseClock } from './roadtripModel';
import { formatClock, hasChosenArrival, parseClock } from './roadtripModel';
import { formatDurationShort } from './roadtripModel';
import { formatDistance } from './units';
@@ -168,7 +168,7 @@ export function planDayWindow(
chain.schedule.entries.push({
arrival: formatClock(Math.round(arrival)),
departure: formatClock(Math.round(departure)),
anchored: !stop.automaticNight && stop.time !== null,
anchored: hasChosenArrival(stop),
dayOffset: Math.floor(arrival / 1440),
});
};
+2 -2
View File
@@ -1,6 +1,6 @@
import type { QuietDay, RoadtripDay, RoadtripStop } from './planning-types';
import type { RouteSegment } from './planning-types';
import { computeSchedule, parseClock, type Schedule } from './roadtripModel';
import { computeSchedule, parseClock, type Schedule, hasChosenArrival } from './roadtripModel';
export interface SpillMark {
automatic?: boolean;
@@ -147,7 +147,7 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
entries: placed.map((p) => ({
arrival: p.arrival,
departure: p.departure,
anchored: p.stop.time !== null && p.stop.time !== undefined,
anchored: hasChosenArrival(p.stop),
dayOffset: p.dayOffset,
})),
warnings: placed.flatMap((p, i) =>
+25
View File
@@ -1,6 +1,7 @@
import {
computeSchedule,
formatClock,
hasChosenArrival,
formatDurationShort,
insertIndexForAlong,
deriveDriveWarnings,
@@ -822,3 +823,27 @@ it('uses checkout as departure even without an arrival or daily start', () => {
expect(schedule.entries[2]!.arrival).toBe('10:00');
expect(schedule.entries[2]!.dayOffset).toBe(1);
});
describe('hasChosenArrival', () => {
// The rail prints a chosen hour in ink and a computed one in grey. Every scheduler
// anchors on `time ?? checkInTime`, so a booked night's check-in is as chosen as a
// pinned stop; two of the three used to ask only about `time` and printed it grey.
it('counts a pinned time', () => {
expect(hasChosenArrival({ time: '09:00' })).toBe(true);
});
it('counts a check-in, which is the hour a booked night starts its day on', () => {
expect(hasChosenArrival({ time: null, checkInTime: '08:00' })).toBe(true);
});
it('leaves a stop the chain worked out alone', () => {
expect(hasChosenArrival({ time: null, checkInTime: null })).toBe(false);
expect(hasChosenArrival({})).toBe(false);
});
it('never counts a night the planner inserted itself', () => {
// Its clock comes from the driving limits, not from anybody's decision.
expect(hasChosenArrival({ time: '22:00', automaticNight: { phase: 'end' } })).toBe(false);
});
});
+16
View File
@@ -38,6 +38,22 @@ export function formatDurationShort(seconds: number): string {
return `${m} min`;
}
/**
* Whether this stop's arrival is a time somebody chose, rather than one the chain
* worked out from the stop before it.
*
* A check-in counts. Every schedule already anchors on `time ?? checkInTime`, so a
* booked night starts its day exactly like a pinned stop does; asking only about
* `time` here made the rail print that chosen hour in the grey it reserves for
* computed ones. One function because the answer was spelled out separately in each
* of the three schedulers, and two of them spelled it differently from the anchor
* they had just used.
*/
export function hasChosenArrival(stop: { time?: string | null; checkInTime?: string | null; automaticNight?: unknown }): boolean {
if (stop.automaticNight) return false;
return (stop.time ?? null) !== null || (stop.checkInTime ?? null) !== null;
}
export function parseClock(value: string | null | undefined): number | null {
if (!value) return null;
const m = /^(\d{1,2}):(\d{2})/.exec(value.trim());