fix(journey): say when a stay runs into the next day, leave the doubtful stays of a day unticked and pin a day report to a stay of the day, not the night

This commit is contained in:
Maurice
2026-10-21 20:46:33 +02:00
parent 464d4e006c
commit 8523bbb724
5 changed files with 52 additions and 9 deletions
@@ -111,6 +111,11 @@ describe('clockOf / timeRange', () => {
expect(timeRange('2026-09-10T10:15:00+02:00', 'nonsense', false)).toBe('10:15')
})
it('FE-DAWARICH-SUGMODEL-020: a stay into the next day carries the days it ran over', () => {
expect(timeRange('2026-10-24T18:30:00+02:00', '2026-10-25T08:30:00+02:00', false)).toBe('18:30 – 08:30 +1')
expect(timeRange('2026-10-24T18:30:00+02:00', '2026-10-26T08:30:00+02:00', false)).toBe('18:30 – 08:30 +2')
})
it('FE-DAWARICH-SUGMODEL-007: no arrival, no range', () => {
expect(timeRange('nonsense', '2026-09-10T12:40:00+02:00', false)).toBe('')
})
@@ -102,12 +102,17 @@ export function clockOf(iso: string): string {
* timestamp and printed, so a traveller on a 12-hour clock read every Dawarich time in
* 24-hour while the rest of the app obeyed the setting. A required argument makes the
* next caller answer the question rather than inherit the bug.
*
* A stay that ends on a later day says so (`18:30 – 08:30 +1`), as a timetable
* does, or a night at the hotel reads as fourteen hours that never happened.
*/
export function timeRange(startedAt: string, endedAt: string, is12h: boolean): string {
const from = formatClockTime(clockOf(startedAt), is12h)
const to = formatClockTime(clockOf(endedAt), is12h)
if (!from) return ''
return to ? `${from} – ${to}` : from
if (!to) return from
const days = Math.round((Date.parse(endedAt.slice(0, 10)) - Date.parse(startedAt.slice(0, 10))) / 86_400_000)
return days > 0 ? `${from} – ${to} +${days}` : `${from} – ${to}`
}
/**
@@ -1,4 +1,4 @@
// FE-JRN-DAYIMPORT-MODEL-001 to FE-JRN-DAYIMPORT-MODEL-011
// FE-JRN-DAYIMPORT-MODEL-001 to FE-JRN-DAYIMPORT-MODEL-013
//
// JRN-02: what the traveller decided in the day take-over, before anything is
// written. Two stays at San Marco, a parking stop and Rialto, as the review
@@ -144,4 +144,25 @@ describe('journeyDayImportModel', () => {
expect(canJoin(choice, stays, 2)).toBe(false);
expect(groupsOf(choice, stays).map((g) => g.stays.map((s) => s.suggestionId))).toEqual([[1], [3]]);
});
it('FE-JRN-DAYIMPORT-MODEL-012: a stay Dawarich itself doubts starts unchosen, unless it was confirmed there', () => {
const stays = [
buildDayStay({ suggestionId: 1, name: 'Car park', confidenceBand: 'low', sourceStatus: 'suggested' }),
buildDayStay({ suggestionId: 2, name: 'Bridge', confidenceBand: 'low', sourceStatus: 'confirmed' }),
buildDayStay({ suggestionId: 3, name: 'Cathedral', confidenceBand: 'medium', sourceStatus: 'suggested' }),
];
expect([...initialChoice(stays).selected]).toEqual([2, 3]);
});
it('FE-JRN-DAYIMPORT-MODEL-013: a night at the hotel is the position only when no stay of the day has one', () => {
const night = buildDayStay({
suggestionId: 1,
startedAt: '2026-05-14T19:00:00+02:00',
endedAt: '2026-05-15T08:00:00+02:00',
durationMinutes: 780,
});
const sight = buildDayStay({ suggestionId: 2, durationMinutes: 50 });
expect(defaultAnchor([sight, night])).toBe(2);
expect(defaultAnchor([night, buildDayStay({ suggestionId: 3, lat: null, lng: null })])).toBe(1);
});
});
@@ -36,11 +36,16 @@ export function isTakeable(stay: JourneyDayStay): boolean {
}
/**
* Every open stay chosen, and the same place twice in a row already joined:
* the square somebody left for a coffee and came back to is one memory.
* Every open stay chosen but those Dawarich itself doubts (the car park passed
* on the way, more often than not), and the same place twice in a row already
* joined: the square somebody left for a coffee and came back to is one memory.
*/
export function initialChoice(stays: JourneyDayStay[]): DayImportChoice {
const selected = new Set(stays.filter((stay) => stay.status === 'open').map((stay) => stay.suggestionId));
const selected = new Set(
stays
.filter((stay) => stay.status === 'open' && !(stay.confidenceBand === 'low' && stay.sourceStatus !== 'confirmed'))
.map((stay) => stay.suggestionId)
);
const joined = new Set<number>();
stays.forEach((stay, index) => {
const before = stays[index - 1];
@@ -99,11 +104,17 @@ export function groupsOf(choice: DayImportChoice, stays: JourneyDayStay[]): DayI
return groups;
}
/** The longest stay that has a place: where a group sits on the map unless somebody picks another. */
/**
* The longest stay that has a place: where a group sits on the map unless
* somebody picks another. A night somewhere outlasts any sight of the day, so
* a stay that ends on the same day goes first and the hotel only when there is
* nothing else.
*/
export function defaultAnchor(stays: JourneyDayStay[]): number | null {
const placed = stays.filter((stay) => stay.lat !== null && stay.lng !== null);
const sameDay = placed.filter((stay) => stay.startedAt.slice(0, 10) === stay.endedAt.slice(0, 10));
let best: JourneyDayStay | null = null;
for (const stay of stays) {
if (stay.lat === null || stay.lng === null) continue;
for (const stay of sameDay.length > 0 ? sameDay : placed) {
if (!best || stay.durationMinutes > best.durationMinutes) best = stay;
}
return best?.suggestionId ?? null;
+2 -1
View File
@@ -94,7 +94,8 @@ export function buildDayStays(over: Partial<JourneyDayStays> = {}): JourneyDaySt
endedAt: '2026-05-14T08:40:00+02:00',
durationMinutes: 10,
sourceStatus: 'suggested',
confidenceBand: 'low',
// Doubtful enough to ask about, not so doubtful that the review leaves it out.
confidenceBand: 'medium',
}),
buildDayStay(),
buildDayStay({