diff --git a/shared/src/roadtrip/dayWindow.ts b/shared/src/roadtrip/dayWindow.ts index d79569a6a..8ba7edb53 100644 --- a/shared/src/roadtrip/dayWindow.ts +++ b/shared/src/roadtrip/dayWindow.ts @@ -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), }); }; diff --git a/shared/src/roadtrip/nightSpill.ts b/shared/src/roadtrip/nightSpill.ts index 5e10315be..9c51af731 100644 --- a/shared/src/roadtrip/nightSpill.ts +++ b/shared/src/roadtrip/nightSpill.ts @@ -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) => diff --git a/shared/src/roadtrip/roadtripModel.spec.ts b/shared/src/roadtrip/roadtripModel.spec.ts index f0a1db319..7b163cfda 100644 --- a/shared/src/roadtrip/roadtripModel.spec.ts +++ b/shared/src/roadtrip/roadtripModel.spec.ts @@ -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); + }); +}); diff --git a/shared/src/roadtrip/roadtripModel.ts b/shared/src/roadtrip/roadtripModel.ts index 4dc3fd878..efcb9177a 100644 --- a/shared/src/roadtrip/roadtripModel.ts +++ b/shared/src/roadtrip/roadtripModel.ts @@ -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());