Skip to content

Commit e425ac7

Browse files
mathiscapartclaude
andcommitted
test(dates): #115 — heures murales et instants aux changements d'heure 2026-2027, tous les styles figés
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7ce6fc4 commit e425ac7

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

‎src/lib/datetime.test.ts‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,152 @@ describe("instantToWall / wallToInstant — conversions à l'heure de Paris", ()
155155
).toBe("2026-10-24T22:00:00.000Z");
156156
});
157157
});
158+
159+
// Les dimanches de changement d'heure : 2026 (29 mars, 25 octobre) et 2027
160+
// (28 mars, 31 octobre), pour ne pas dépendre d'une année.
161+
const BASCULES = [
162+
{ annee: 2026, jour: "2026-03-29", libelle: "dim. 29 mars", mois: "29 mars 2026" },
163+
{ annee: 2026, jour: "2026-10-25", libelle: "dim. 25 oct.", mois: "25 oct. 2026" },
164+
{ annee: 2027, jour: "2027-03-28", libelle: "dim. 28 mars", mois: "28 mars 2027" },
165+
{ annee: 2027, jour: "2027-10-31", libelle: "dim. 31 oct.", mois: "31 oct. 2027" },
166+
];
167+
168+
describe("heure murale — le jour même des changements d'heure", () => {
169+
for (const b of BASCULES) {
170+
it(`${b.jour} : 02:30 saisie (heure inexistante ou doublée à Paris) se réaffiche 02:30`, async () => {
171+
const d = utc(`${b.jour}T02:30:00`);
172+
expect(await sameInEveryZone((m) => m.formatWall(d, "time"))).toBe("02:30");
173+
expect(await sameInEveryZone((m) => m.formatWall(d, "dateTime"))).toBe(`${b.mois}, 02:30`);
174+
expect(await sameInEveryZone((m) => m.toDatetimeLocal(d))).toBe(`${b.jour}T02:30`);
175+
});
176+
177+
it(`${b.jour} : 01:00 → 04:00 reste sur un seul jour`, async () => {
178+
const r = await sameInEveryZone((m) =>
179+
m.formatEventRange(utc(`${b.jour}T01:00:00`), utc(`${b.jour}T04:00:00`)),
180+
);
181+
expect(r).toBe(`${b.libelle} · 01:00 – 04:00`);
182+
});
183+
}
184+
});
185+
186+
describe("instant réel — aux bords exacts des changements d'heure", () => {
187+
const bords: [string, string][] = [
188+
["2026-03-29T00:59:59", "29 mars 2026, 01:59"],
189+
["2026-03-29T01:00:00", "29 mars 2026, 03:00"],
190+
["2026-10-25T00:59:59", "25 oct. 2026, 02:59"],
191+
["2026-10-25T01:00:00", "25 oct. 2026, 02:00"],
192+
["2027-03-28T00:59:59", "28 mars 2027, 01:59"],
193+
["2027-03-28T01:00:00", "28 mars 2027, 03:00"],
194+
["2027-10-31T00:59:59", "31 oct. 2027, 02:59"],
195+
["2027-10-31T01:00:00", "31 oct. 2027, 02:00"],
196+
];
197+
for (const [iso, attendu] of bords) {
198+
it(`${iso}Z → ${attendu} (date et heure de Paris)`, async () => {
199+
expect(await sameInEveryZone((m) => m.formatInstant(utc(iso), "dateTime"))).toBe(attendu);
200+
});
201+
}
202+
});
203+
204+
describe("toutes les clés de DateStyle — texte rendu figé", () => {
205+
// `satisfies` : ajouter un style à DateStyle sans l'ajouter ici casse le typecheck.
206+
const murale = {
207+
time: "15:46",
208+
dayMonth: "02 oct.",
209+
dayMonthLong: "02 octobre",
210+
dayMonthNumeric: "02/10",
211+
date: "02 oct. 2026",
212+
dateLong: "02 octobre 2026",
213+
dateNumeric: "02/10/2026",
214+
dayMonthTime: "02 oct., 15:46",
215+
dayMonthLongTime: "02 octobre à 15:46",
216+
dayMonthNumericTime: "02/10 15:46",
217+
dateTime: "02 oct. 2026, 15:46",
218+
weekdayDayMonth: "ven. 02 oct.",
219+
monthYear: "octobre 2026",
220+
} satisfies Record<import("./datetime").DateStyle, string>;
221+
222+
// 08:05 UTC en été = 10:05 à Paris.
223+
const instant = {
224+
time: "10:05",
225+
dayMonth: "14 juil.",
226+
dayMonthLong: "14 juillet",
227+
dayMonthNumeric: "14/07",
228+
date: "14 juil. 2026",
229+
dateLong: "14 juillet 2026",
230+
dateNumeric: "14/07/2026",
231+
dayMonthTime: "14 juil., 10:05",
232+
dayMonthLongTime: "14 juillet à 10:05",
233+
dayMonthNumericTime: "14/07 10:05",
234+
dateTime: "14 juil. 2026, 10:05",
235+
weekdayDayMonth: "mar. 14 juil.",
236+
monthYear: "juillet 2026",
237+
} satisfies Record<import("./datetime").DateStyle, string>;
238+
239+
it("heure murale (formatWall), sous UTC, Paris et New York", async () => {
240+
const d = utc("2026-10-02T15:46:00");
241+
const rendu = await sameInEveryZone((m) =>
242+
Object.fromEntries(Object.keys(murale).map((s) => [s, m.formatWall(d, s as never)])),
243+
);
244+
expect(rendu).toEqual(murale);
245+
});
246+
247+
it("instant réel (formatInstant), sous UTC, Paris et New York", async () => {
248+
const d = utc("2026-07-14T08:05:00");
249+
const rendu = await sameInEveryZone((m) =>
250+
Object.fromEntries(Object.keys(instant).map((s) => [s, m.formatInstant(d, s as never)])),
251+
);
252+
expect(rendu).toEqual(instant);
253+
});
254+
});
255+
256+
describe("wallToInstant(instantToWall(x)) = x", () => {
257+
// Hors heure inexistante (jamais produite par instantToWall) et hors première
258+
// occurrence de l'heure doublée (00:00–00:59:59 UTC le jour du passage à
259+
// l'heure d'hiver, voir le test suivant).
260+
const echantillon = [
261+
"2026-01-15T08:05:00.123",
262+
"2026-07-14T08:05:00",
263+
"2026-03-28T23:59:59", // veille de bascule
264+
"2026-03-29T00:30:00", // 01:30 à Paris, juste avant le saut
265+
"2026-03-29T00:59:59", // 01:59:59, dernière seconde avant le saut
266+
"2026-03-29T01:00:00", // 03:00, première seconde après le saut
267+
"2026-03-29T01:30:00",
268+
"2026-10-24T23:59:59", // 01:59:59 à Paris, avant le doublon
269+
"2026-10-25T01:00:00", // 02:00 (heure d'hiver), début de la 2e occurrence
270+
"2026-10-25T01:30:00", // 02:30, 2e occurrence
271+
"2026-10-25T02:00:00",
272+
"2027-03-28T00:30:00",
273+
"2027-03-28T01:00:00",
274+
"2027-03-28T01:30:00",
275+
"2027-10-30T23:59:59",
276+
"2027-10-31T01:30:00",
277+
"2027-10-31T02:00:00",
278+
];
279+
it("aller-retour exact sur l'échantillon, dont les deux nuits de bascule", async () => {
280+
for (const iso of echantillon) {
281+
const x = new Date(`${iso}${iso.includes(".") ? "" : ".000"}Z`);
282+
const retour = await sameInEveryZone((m) => m.wallToInstant(m.instantToWall(x)).toISOString());
283+
expect(retour, iso).toBe(x.toISOString());
284+
}
285+
});
286+
287+
it("heure doublée du 25 octobre : 02:30 murale = la 2e occurrence (01:30Z, heure d'hiver)", async () => {
288+
// 02:30 existe deux fois à Paris (00:30Z en CEST, 01:30Z en CET) : wallToInstant
289+
// retient la seconde. Conséquence documentée : la première occurrence ne
290+
// fait pas l'aller-retour (00:30Z -> 02:30 -> 01:30Z).
291+
expect(
292+
await sameInEveryZone((m) => m.wallToInstant(utc("2026-10-25T02:30:00")).toISOString()),
293+
).toBe("2026-10-25T01:30:00.000Z");
294+
expect(
295+
await sameInEveryZone((m) =>
296+
m.wallToInstant(m.instantToWall(utc("2026-10-25T00:30:00"))).toISOString(),
297+
),
298+
).toBe("2026-10-25T01:30:00.000Z");
299+
});
300+
301+
it("heure inexistante du 29 mars : 02:30 murale est décalée à 03:30 de Paris (01:30Z)", async () => {
302+
expect(
303+
await sameInEveryZone((m) => m.wallToInstant(utc("2026-03-29T02:30:00")).toISOString()),
304+
).toBe("2026-03-29T01:30:00.000Z");
305+
});
306+
});

0 commit comments

Comments
 (0)