feat: parse iCalendar feed into Event[] (T1)
Add a pure parser parserEvenements(flux, aujourdhui) backed by vendored
ical.js v2.2.1 (ESM, no build). Returns events at the frozen Event contract
{ uid, titre, description, lieu, debut, fin, categories, journeeEntiere }:
- register embedded VTIMEZONE before toJSDate (correct offset, e.g. Africa/Lagos)
- dedupe by UID, keep the master (RECURRENCE-ID exceptions ignored)
- filter to today and future (local start of day)
- coerce missing text fields to ""
Edge cases deferred to T3 (RRULE expansion, all-day hour) are documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fd28d3b2d3
commit
1878ba08ec
@@ -0,0 +1,62 @@
|
||||
// Parseur iCalendar : flux brut → Event[] au contrat figé (T1).
|
||||
// Fonction pure. `aujourdhui` est injecté (obligatoire) pour des tests
|
||||
// déterministes. Voir docs/DECISIONS.md pour le contrat et les cas différés à T3.
|
||||
//
|
||||
// Event = {
|
||||
// uid: string, titre: string, description: string, lieu: string,
|
||||
// debut: Date, fin: Date, categories: string[], journeeEntiere: boolean
|
||||
// }
|
||||
|
||||
import ICAL from "./vendor/ical.js";
|
||||
|
||||
export function parserEvenements(flux, aujourdhui) {
|
||||
const composant = new ICAL.Component(ICAL.parse(flux));
|
||||
|
||||
// Enregistrer les fuseaux embarqués : sans cela toJSDate() calcule un
|
||||
// mauvais offset pour les TZID non standards (ex. Africa/Lagos).
|
||||
composant.getAllSubcomponents("vtimezone").forEach((vt) => {
|
||||
const tzid = vt.getFirstPropertyValue("tzid");
|
||||
if (tzid && !ICAL.TimezoneService.has(tzid)) {
|
||||
ICAL.TimezoneService.register(vt);
|
||||
}
|
||||
});
|
||||
|
||||
// Dédoublonnage par UID. Les exceptions d'occurrence (RECURRENCE-ID) ne sont
|
||||
// pas développées en T1 : on les ignore et on garde le maître.
|
||||
const parUid = new Map();
|
||||
for (const vevent of composant.getAllSubcomponents("vevent")) {
|
||||
const ev = new ICAL.Event(vevent);
|
||||
if (ev.isRecurrenceException()) continue;
|
||||
if (parUid.has(ev.uid)) continue;
|
||||
parUid.set(ev.uid, toEvent(ev, vevent));
|
||||
}
|
||||
|
||||
const seuil = new Date(aujourdhui);
|
||||
seuil.setHours(0, 0, 0, 0);
|
||||
|
||||
// Ordre du flux conservé (tri différé à la liste).
|
||||
return [...parUid.values()].filter(
|
||||
(event) => event.debut.getTime() >= seuil.getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
function toEvent(ev, vevent) {
|
||||
return {
|
||||
uid: ev.uid,
|
||||
titre: ev.summary ?? "",
|
||||
description: ev.description ?? "",
|
||||
lieu: ev.location ?? "",
|
||||
debut: ev.startDate.toJSDate(),
|
||||
fin: ev.endDate.toJSDate(),
|
||||
categories: lireCategories(vevent),
|
||||
journeeEntiere: ev.startDate.isDate === true,
|
||||
};
|
||||
}
|
||||
|
||||
// CATEGORIES peut prendre deux formes (valeurs séparées par virgule et/ou
|
||||
// propriétés répétées) : getAllProperties + getValues couvre les deux.
|
||||
function lireCategories(vevent) {
|
||||
return vevent
|
||||
.getAllProperties("categories")
|
||||
.flatMap((prop) => prop.getValues());
|
||||
}
|
||||
Reference in New Issue
Block a user