fix: keep event cards whose last day ends after a daylight saving change

This commit is contained in:
Linus Rath
2026-10-30 05:27:46 +01:00
parent 1d4cae961e
commit 90d7afc349
3 changed files with 25 additions and 10 deletions
@@ -122,6 +122,20 @@ describe('resolveCards', () => {
expect((card as { checkin: { time?: string } }).checkin.time).toBeUndefined();
});
it('ends a date-only event end at midnight local time even across a DST change', async () => {
const event = (country?: string) => ({
'@context': ctx, '@type': 'EventReservation', reservationNumber: 'EV1',
reservationFor: {
'@type': 'Event', name: 'Herbstmesse', startDate: '2026-10-23T18:00:00+02:00', endDate: '2026-10-26',
location: { '@type': 'Place', name: 'Messe', ...(country ? { address: { addressCountry: country } } : {}) },
},
});
const [withVenue] = await resolved(event('DE'));
expect(withVenue).toMatchObject({ end: { date: '2026-10-26', time: '23:59:59', zone: 'Europe/Berlin', utc: '2026-10-26T22:59:59Z' } });
const [noVenueCountry] = await resolved(event());
expect(noVenueCountry).toMatchObject({ end: { date: '2026-10-26', time: '23:59:59', zone: 'Etc/GMT-2', utc: '2026-10-26T21:59:59Z' } });
});
it('reads bus times in the single zone of the stop\'s country', async () => {
const [card] = (await resolved({
'@context': ctx, '@type': 'BusReservation', reservationNumber: 'B',
+3 -8
View File
@@ -231,12 +231,6 @@ function base(kind: BookingKind, res: Obj, source: CardSource) {
const MISSING: Moment = { date: '' };
function offsetSuffix(minutes: number): string {
const sign = minutes < 0 ? '-' : '+';
const abs = Math.abs(minutes);
return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
}
function reservation(kind: BookingKind, res: Obj, source: CardSource): StructuredCard | undefined {
if (kind === 'flight' || kind === 'train' || kind === 'bus') return transport(kind, res, source);
const target = obj(res.reservationFor) ?? {};
@@ -275,8 +269,9 @@ function reservation(kind: BookingKind, res: Obj, source: CardSource): Structure
let end = moment(target.endDate);
// An end date without a time means the end of that day, when the
// event starts at a time; with a date-only start it's an all-day span.
if (end && !end.time && start?.time) end = { ...end, time: '23:59:59', ...(start.offsetMinutes !== undefined ? { offsetMinutes: start.offsetMinutes } : {}) };
if (end?.offsetMinutes !== undefined && end.time && !end.utc) end = moment(`${end.date}T${end.time}${offsetSuffix(end.offsetMinutes)}`);
// The end carries no offset: the day may end on the other side of a
// DST change, so resolve.ts reads it in the zone the start resolves to.
if (end && !end.time && start?.time) end = { ...end, time: '23:59:59' };
return {
...base(kind, res, source),
kind,
+8 -2
View File
@@ -116,8 +116,14 @@ function resolveCard(card: StructuredCard, zoneOf: ZoneOf | undefined): Structur
case 'event': {
const named = (m: Moment) => namedZone(card.venue, card.kind, zoneOf, m.offsetMinutes === undefined);
const start = resolveMoment(card.start, named(card.start));
const end = card.end ? resolveMoment(card.end, named(card.end)) : undefined;
if (start === 'drop' || end === 'drop') return null;
if (start === 'drop') return null;
// An end without its own offset (a date-only end turned into the end of
// that day) belongs to the start's zone when the venue names none.
const endNamed = card.end && card.end.offsetMinutes === undefined && !named(card.end) && start.zone
? { zone: start.zone, source: 'country' as const }
: card.end ? named(card.end) : undefined;
const end = card.end ? resolveMoment(card.end, endNamed) : undefined;
if (end === 'drop') return null;
if (end && !notBefore(end, start)) return null;
return { ...card, start, ...(end ? { end } : {}) };
}