@tricoteuses/senat 2.10.0 → 2.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +22 -22
- package/README.md +116 -116
- package/lib/loaders.d.ts +6 -1
- package/lib/loaders.js +54 -0
- package/lib/model/agenda.js +0 -2
- package/lib/model/compte_rendu.d.ts +9 -2
- package/lib/model/compte_rendu.js +223 -211
- package/lib/model/util.d.ts +1 -0
- package/lib/model/util.js +3 -0
- package/lib/scripts/retrieve_agenda.js +25 -6
- package/lib/scripts/retrieve_comptes_rendus.d.ts +6 -1
- package/lib/scripts/retrieve_comptes_rendus.js +230 -77
- package/lib/scripts/retrieve_comptes_rendus_seance.d.ts +6 -0
- package/lib/scripts/retrieve_comptes_rendus_seance.js +273 -0
- package/lib/scripts/retrieve_videos.js +1 -9
- package/lib/types/agenda.d.ts +19 -2
- package/lib/types/compte_rendu.d.ts +1 -1
- package/lib/utils/cr_spliting.d.ts +7 -0
- package/lib/utils/cr_spliting.js +125 -0
- package/lib/utils/reunion_grouping.d.ts +6 -0
- package/lib/utils/reunion_grouping.js +359 -0
- package/lib/validators/senat.d.ts +0 -0
- package/lib/validators/senat.js +24 -0
- package/package.json +98 -98
- package/lib/raw_types/kysely-table-types.d.ts +0 -5
- package/lib/raw_types/kysely-table-types.js +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function computeIntervalsBySlot($, idx, firstSlotOfDay) {
|
|
2
|
+
const all = $("body *").toArray();
|
|
3
|
+
const cuts = [{ pos: 0, hhmm: undefined }];
|
|
4
|
+
$('a[name]').each((_, a) => {
|
|
5
|
+
const name = (a.attribs?.["name"] || "").trim();
|
|
6
|
+
if (!/^su/i.test(name))
|
|
7
|
+
return;
|
|
8
|
+
const pos = idx.get(a);
|
|
9
|
+
if (pos == null)
|
|
10
|
+
return;
|
|
11
|
+
const hhmm = hhmmFromSuName(name); // "SU1620" -> "1620"
|
|
12
|
+
cuts.push({ pos, hhmm });
|
|
13
|
+
});
|
|
14
|
+
cuts.sort((a, b) => a.pos - b.pos);
|
|
15
|
+
cuts.push({ pos: all.length, hhmm: undefined });
|
|
16
|
+
let initialSlot = firstSlotOfDay;
|
|
17
|
+
if (!initialSlot) {
|
|
18
|
+
const openHHMM = extractOpeningHHMM($);
|
|
19
|
+
if (openHHMM)
|
|
20
|
+
initialSlot = slotOfHHMM(openHHMM);
|
|
21
|
+
}
|
|
22
|
+
if (!initialSlot)
|
|
23
|
+
initialSlot = "MATIN";
|
|
24
|
+
const intervals = [];
|
|
25
|
+
let lastSlot = initialSlot;
|
|
26
|
+
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
27
|
+
const start = cuts[i].pos;
|
|
28
|
+
const end = cuts[i + 1].pos;
|
|
29
|
+
if (end <= start)
|
|
30
|
+
continue;
|
|
31
|
+
// i=0 initialSlot
|
|
32
|
+
// i>0 : if current cut has SU -> slotOfHHMM, otherwise lastSlot
|
|
33
|
+
const slot = i === 0 ? initialSlot : (cuts[i].hhmm ? slotOfHHMM(cuts[i].hhmm) : lastSlot);
|
|
34
|
+
intervals.push({ slot, start, end });
|
|
35
|
+
lastSlot = slot;
|
|
36
|
+
}
|
|
37
|
+
return intervals;
|
|
38
|
+
}
|
|
39
|
+
function hhmmFromSuName(name) {
|
|
40
|
+
const m = name.match(/^SU(\d{2})(\d{2})$/i);
|
|
41
|
+
if (!m)
|
|
42
|
+
return;
|
|
43
|
+
return `${m[1]}:${m[2]}`;
|
|
44
|
+
}
|
|
45
|
+
function slotOfHHMM(hhmm) {
|
|
46
|
+
if (!hhmm)
|
|
47
|
+
return "MATIN";
|
|
48
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
49
|
+
const v = h + m / 60;
|
|
50
|
+
if (v < 12)
|
|
51
|
+
return "MATIN";
|
|
52
|
+
if (v < 18.5)
|
|
53
|
+
return "APRES-MIDI";
|
|
54
|
+
return "SOIR";
|
|
55
|
+
}
|
|
56
|
+
// Looks for text like "(La séance est ouverte à quinze heures.)" and extracts "HH:MM"
|
|
57
|
+
function extractOpeningHHMM($) {
|
|
58
|
+
let txt = "";
|
|
59
|
+
$("span.info_entre_parentheses, .info_entre_parentheses").each((_, el) => {
|
|
60
|
+
const t = ($(el).text() || "").replace(/\s+/g, " ").trim();
|
|
61
|
+
if (!txt && /\bs[eé]ance est ouverte\b/i.test(t))
|
|
62
|
+
txt = t;
|
|
63
|
+
});
|
|
64
|
+
if (!txt)
|
|
65
|
+
return undefined;
|
|
66
|
+
const inner = txt.match(/\(.*?ouverte\s+à\s+([^)]+?)\)/i)?.[1];
|
|
67
|
+
if (!inner)
|
|
68
|
+
return undefined;
|
|
69
|
+
return parseFrenchClockToHHMM(inner);
|
|
70
|
+
}
|
|
71
|
+
// Convert "quinze heures trente", "15 heures 30", "dix-sept heures moins le quart", etc. en "HHMM"
|
|
72
|
+
function parseFrenchClockToHHMM(input) {
|
|
73
|
+
const s = (input || "").toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").trim();
|
|
74
|
+
if (!s)
|
|
75
|
+
return undefined;
|
|
76
|
+
const digitMatch = s.match(/(\d{1,2})\s*heures?(?:\s*(\d{1,2}))?/);
|
|
77
|
+
if (digitMatch) {
|
|
78
|
+
const h = Math.min(24, Math.max(0, parseInt(digitMatch[1], 10)));
|
|
79
|
+
const m = digitMatch[2] ? Math.min(59, Math.max(0, parseInt(digitMatch[2], 10))) : 0;
|
|
80
|
+
return `${String(h).padStart(2, "0")}${String(m).padStart(2, "0")}`;
|
|
81
|
+
}
|
|
82
|
+
const NUM = new Map([
|
|
83
|
+
["zero", 0], ["une", 1], ["un", 1], ["deux", 2], ["trois", 3], ["quatre", 4], ["cinq", 5], ["six", 6],
|
|
84
|
+
["sept", 7], ["huit", 8], ["neuf", 9], ["dix", 10], ["onze", 11], ["douze", 12], ["treize", 13],
|
|
85
|
+
["quatorze", 14], ["quinze", 15], ["seize", 16], ["dix-sept", 17], ["dix sept", 17], ["dix-huit", 18],
|
|
86
|
+
["dix huit", 18], ["dix-neuf", 19], ["dix neuf", 19], ["vingt", 20], ["vingt et une", 21],
|
|
87
|
+
["vingt-et-une", 21], ["vingt et un", 21], ["vingt-et-un", 21], ["vingt-deux", 22], ["vingt deux", 22],
|
|
88
|
+
["vingt-trois", 23], ["vingt trois", 23], ["vingt-quatre", 24], ["vingt quatre", 24],
|
|
89
|
+
]);
|
|
90
|
+
const hourWordMatch = s.match(/([a-z\- ]+?)\s*heures?/);
|
|
91
|
+
if (!hourWordMatch)
|
|
92
|
+
return undefined;
|
|
93
|
+
const hourWord = hourWordMatch[1].trim();
|
|
94
|
+
let hour = NUM.get(hourWord);
|
|
95
|
+
if (hour == null) {
|
|
96
|
+
const cleaned = hourWord.replace(/\s+/g, " ");
|
|
97
|
+
hour = NUM.get(cleaned);
|
|
98
|
+
}
|
|
99
|
+
if (hour == null)
|
|
100
|
+
return undefined;
|
|
101
|
+
let minutes = 0;
|
|
102
|
+
if (/\bet (demie|demi)\b/.test(s))
|
|
103
|
+
minutes = 30;
|
|
104
|
+
else if (/\bet quart\b/.test(s))
|
|
105
|
+
minutes = 15;
|
|
106
|
+
else if (/\bmoins le quart\b/.test(s)) {
|
|
107
|
+
hour = (hour + 23) % 24;
|
|
108
|
+
minutes = 45;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const MIN = new Map([
|
|
112
|
+
["cinq", 5], ["dix", 10], ["quinze", 15], ["vingt", 20], ["vingt-cinq", 25], ["vingt cinq", 25],
|
|
113
|
+
["trente", 30], ["trente-cinq", 35], ["trente cinq", 35], ["quarante", 40], ["quarante-cinq", 45],
|
|
114
|
+
["quarante cinq", 45], ["cinquante", 50], ["cinquante-cinq", 55], ["cinquante cinq", 55],
|
|
115
|
+
]);
|
|
116
|
+
const minWordMatch = s.match(/heures?\s+([a-z\- ]+?)(?:[).,;]|$)/);
|
|
117
|
+
if (minWordMatch) {
|
|
118
|
+
const mw = minWordMatch[1].trim();
|
|
119
|
+
const m1 = MIN.get(mw);
|
|
120
|
+
if (m1 != null)
|
|
121
|
+
minutes = m1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return `${String(hour).padStart(2, "0")}${String(minutes).padStart(2, "0")}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AgendaEvent, GroupedReunion, TimeSlot } from "../types/agenda";
|
|
2
|
+
export declare function groupNonSPByTypeOrganeHour(events: AgendaEvent[]): Record<"IDC" | "IDM" | "IDO" | "IDI", GroupedReunion[]>;
|
|
3
|
+
export declare function groupSeancePubliqueBySlot(events: AgendaEvent[]): GroupedReunion[];
|
|
4
|
+
export declare function makeGroupUid(date: string, slot: TimeSlot): string;
|
|
5
|
+
export declare function formatYYYYMMDD(dateYYYYMMDD: string): string;
|
|
6
|
+
export declare function makeReunionUid(agenda: AgendaEvent): string;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { norm } from "../model/util";
|
|
3
|
+
const PARIS = "Europe/Paris";
|
|
4
|
+
const STOPWORDS = new Set([
|
|
5
|
+
"de", "du", "des",
|
|
6
|
+
"la", "le", "les", "l",
|
|
7
|
+
"d",
|
|
8
|
+
"et",
|
|
9
|
+
"en",
|
|
10
|
+
"au", "aux",
|
|
11
|
+
"pour",
|
|
12
|
+
"sur", "sous", "à", "a", "aux",
|
|
13
|
+
]);
|
|
14
|
+
export function groupNonSPByTypeOrganeHour(events) {
|
|
15
|
+
const out = { IDC: [], IDM: [], IDO: [], IDI: [] };
|
|
16
|
+
if (!events?.length)
|
|
17
|
+
return out;
|
|
18
|
+
const nonSP = events.filter(e => !isSeancePublique(e?.type));
|
|
19
|
+
if (nonSP.length === 0)
|
|
20
|
+
return out;
|
|
21
|
+
const buckets = new Map();
|
|
22
|
+
for (const e of nonSP) {
|
|
23
|
+
const kind = classifyAgendaType(e?.type);
|
|
24
|
+
if (!kind || kind === "SP")
|
|
25
|
+
continue;
|
|
26
|
+
const { startISO, endISO } = deriveTimesForEvent(e);
|
|
27
|
+
const hourShort = hourShortFromISO(startISO) ?? hourShortFromOriginal(e.timeOriginal);
|
|
28
|
+
const key = [e.date, kind, hourShort || "NA"].join("|");
|
|
29
|
+
if (!buckets.has(key))
|
|
30
|
+
buckets.set(key, []);
|
|
31
|
+
buckets.get(key).push({ ...e, startTime: startISO ?? e.startTime, endTime: endISO ?? e.endTime });
|
|
32
|
+
}
|
|
33
|
+
for (const [key, list] of buckets) {
|
|
34
|
+
const [date, kindStr, hourShort] = key.split("|");
|
|
35
|
+
const kind = kindStr;
|
|
36
|
+
const enriched = list.map(ev => {
|
|
37
|
+
const { startISO, endISO } = deriveTimesForEvent(ev);
|
|
38
|
+
return { ev, startISO: startISO ?? ev.startTime, endISO: endISO ?? ev.endTime };
|
|
39
|
+
}).sort((a, b) => {
|
|
40
|
+
const ta = a.startISO ? parseISO(a.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
41
|
+
const tb = b.startISO ? parseISO(b.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
42
|
+
return ta - tb;
|
|
43
|
+
});
|
|
44
|
+
const startTime = enriched.find(x => !!x.startISO)?.startISO ?? null;
|
|
45
|
+
const endTime = enriched.reduce((acc, x) => {
|
|
46
|
+
const de = x.endISO ? parseISO(x.endISO)?.toMillis() : null;
|
|
47
|
+
const accMs = acc ? parseISO(acc)?.toMillis() : null;
|
|
48
|
+
if (de != null && (accMs == null || de > accMs))
|
|
49
|
+
return x.endISO;
|
|
50
|
+
return acc;
|
|
51
|
+
}, null);
|
|
52
|
+
const any = enriched[0]?.ev;
|
|
53
|
+
const hour = hourShort !== "NA" ? hourShort : (hourShortFromISO(startTime) ?? hourShortFromOriginal(any?.timeOriginal));
|
|
54
|
+
const uid = makeTypeGroupUid(date, kind, hour ?? null, any?.organe || undefined);
|
|
55
|
+
const suffix = (kind === "COM" ? "IDC" : kind === "MC" ? "IDM" : kind === 'OD' ? 'IDO' : "IDI");
|
|
56
|
+
const group = {
|
|
57
|
+
uid,
|
|
58
|
+
chambre: "SN",
|
|
59
|
+
date,
|
|
60
|
+
type: any?.type || "",
|
|
61
|
+
organe: any?.organe || undefined,
|
|
62
|
+
startTime,
|
|
63
|
+
endTime,
|
|
64
|
+
captationVideo: enriched.some(x => x.ev.captationVideo === true),
|
|
65
|
+
titre: compactTitleList(enriched.map(x => x.ev.titre || "").filter(Boolean), 8),
|
|
66
|
+
objet: joinObjets(enriched.map(x => x.ev)),
|
|
67
|
+
events: enriched.map(x => x.ev),
|
|
68
|
+
};
|
|
69
|
+
out[suffix].push(group);
|
|
70
|
+
}
|
|
71
|
+
for (const k of Object.keys(out)) {
|
|
72
|
+
out[k].sort((a, b) => {
|
|
73
|
+
const da = DateTime.fromISO(`${a.date}T${a.startTime || "00:00:00.000+02:00"}`, { zone: PARIS }).toMillis();
|
|
74
|
+
const db = DateTime.fromISO(`${b.date}T${b.startTime || "00:00:00.000+02:00"}`, { zone: PARIS }).toMillis();
|
|
75
|
+
return da - db || (a.organe || "").localeCompare(b.organe || "");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
export function groupSeancePubliqueBySlot(events) {
|
|
81
|
+
if (!events?.length)
|
|
82
|
+
return [];
|
|
83
|
+
const sp = events.filter(e => isSeancePublique(e?.type));
|
|
84
|
+
if (sp.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
const byDate = new Map();
|
|
87
|
+
for (const e of sp) {
|
|
88
|
+
const d = norm(e.date);
|
|
89
|
+
if (!d)
|
|
90
|
+
continue;
|
|
91
|
+
if (!byDate.has(d))
|
|
92
|
+
byDate.set(d, []);
|
|
93
|
+
byDate.get(d).push(e);
|
|
94
|
+
}
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const [date, dayEvents] of byDate) {
|
|
97
|
+
const enriched = dayEvents.map((e) => {
|
|
98
|
+
const { startISO, endISO, slot } = deriveTimesForEvent(e);
|
|
99
|
+
return { ev: e, startISO, endISO, slot };
|
|
100
|
+
});
|
|
101
|
+
enriched.sort((a, b) => {
|
|
102
|
+
const da = a.startISO ? parseISO(a.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
103
|
+
const db = b.startISO ? parseISO(b.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
104
|
+
return da - db;
|
|
105
|
+
});
|
|
106
|
+
const bySlot = new Map();
|
|
107
|
+
for (const it of enriched) {
|
|
108
|
+
let s = it.slot;
|
|
109
|
+
if (s === "UNKNOWN" && it.startISO) {
|
|
110
|
+
const dt = parseISO(it.startISO);
|
|
111
|
+
if (dt)
|
|
112
|
+
s = slotOf(dt);
|
|
113
|
+
}
|
|
114
|
+
if (!bySlot.has(s))
|
|
115
|
+
bySlot.set(s, []);
|
|
116
|
+
bySlot.get(s).push(it);
|
|
117
|
+
}
|
|
118
|
+
for (const [slot, list] of bySlot) {
|
|
119
|
+
const sorted = list.slice().sort((a, b) => {
|
|
120
|
+
const da = a.startISO ? parseISO(a.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
121
|
+
const db = b.startISO ? parseISO(b.startISO)?.toMillis() ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
122
|
+
return da - db;
|
|
123
|
+
});
|
|
124
|
+
const startTime = sorted.find((x) => !!x.startISO)?.startISO ?? null;
|
|
125
|
+
const endTime = sorted.reduce((acc, x) => {
|
|
126
|
+
const de = x.endISO ? parseISO(x.endISO)?.toMillis() : null;
|
|
127
|
+
const accMs = acc ? parseISO(acc)?.toMillis() : null;
|
|
128
|
+
if (de != null && (accMs == null || de > accMs))
|
|
129
|
+
return x.endISO;
|
|
130
|
+
return acc;
|
|
131
|
+
}, null);
|
|
132
|
+
const titres = sorted.map((x) => x.ev.titre || "").filter(Boolean);
|
|
133
|
+
const captationVideo = sorted.some((x) => x.ev.captationVideo === true);
|
|
134
|
+
out.push({
|
|
135
|
+
uid: makeGroupUid(date, slot),
|
|
136
|
+
chambre: "SN",
|
|
137
|
+
date,
|
|
138
|
+
slot,
|
|
139
|
+
type: "Séance publique",
|
|
140
|
+
startTime,
|
|
141
|
+
endTime,
|
|
142
|
+
captationVideo,
|
|
143
|
+
titre: compactTitleList(titres, 5),
|
|
144
|
+
objet: joinObjets(sorted.map((x) => x.ev)),
|
|
145
|
+
events: sorted.map((x) => x.ev),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
out.sort((a, b) => {
|
|
150
|
+
const da = DateTime.fromISO(`${a.date}T${a.startTime || "00:00:00.000+02:00"}`, { zone: PARIS }).toMillis();
|
|
151
|
+
const db = DateTime.fromISO(`${b.date}T${b.startTime || "00:00:00.000+02:00"}`, { zone: PARIS }).toMillis();
|
|
152
|
+
return da - db || a.slot.localeCompare(b.slot);
|
|
153
|
+
});
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function normalizeNoAccents(s) {
|
|
157
|
+
return (s || "")
|
|
158
|
+
.trim()
|
|
159
|
+
.normalize("NFKD")
|
|
160
|
+
.replace(/[\u0300-\u036f]/g, "");
|
|
161
|
+
}
|
|
162
|
+
function isSeancePublique(typeLabel) {
|
|
163
|
+
const s = normalizeNoAccents(typeLabel || "").toLowerCase();
|
|
164
|
+
return /\bseance\b.*\bpublique\b/.test(s);
|
|
165
|
+
}
|
|
166
|
+
function classifyAgendaType(typeLabel) {
|
|
167
|
+
const s = normalizeNoAccents(typeLabel || "").toLowerCase();
|
|
168
|
+
if (/\bseance\b.*\bpublique\b/.test(s))
|
|
169
|
+
return "SP";
|
|
170
|
+
if (/\bcommissions\b/.test(s))
|
|
171
|
+
return "COM";
|
|
172
|
+
if (/\bmission\b.*\bcontrole\b/.test(s))
|
|
173
|
+
return "MC";
|
|
174
|
+
if (/\boffices\b|\bdelegations\b/.test(s))
|
|
175
|
+
return "OD";
|
|
176
|
+
if (/\instances\b|\decisionelles\b/.test(s))
|
|
177
|
+
return "ID";
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function typeToSuffixStrict(kind) {
|
|
181
|
+
switch (kind) {
|
|
182
|
+
case "SP": return "IDS";
|
|
183
|
+
case "COM": return "IDC";
|
|
184
|
+
case "MC": return "IDM";
|
|
185
|
+
case "OD": return "IDO";
|
|
186
|
+
case "ID": return "IDI";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function hourShortFromISO(iso) {
|
|
190
|
+
if (!iso)
|
|
191
|
+
return null;
|
|
192
|
+
const dt = parseISO(iso);
|
|
193
|
+
if (!dt)
|
|
194
|
+
return null;
|
|
195
|
+
const z = DateTime.fromISO(iso, { zone: PARIS });
|
|
196
|
+
const H = String(z.hour);
|
|
197
|
+
const mm = String(z.minute).padStart(2, "0");
|
|
198
|
+
return `${H}${mm}`;
|
|
199
|
+
}
|
|
200
|
+
function hourShortFromOriginal(s) {
|
|
201
|
+
if (!s)
|
|
202
|
+
return null;
|
|
203
|
+
const clean = normalizeNoAccents(s).toLowerCase();
|
|
204
|
+
const m = clean.match(/(\d{1,2})\s*[h:]\s*(\d{2})/);
|
|
205
|
+
if (m) {
|
|
206
|
+
const H = String(parseInt(m[1], 10));
|
|
207
|
+
const mm = m[2].padStart(2, "0");
|
|
208
|
+
return `${H}${mm}`;
|
|
209
|
+
}
|
|
210
|
+
const m2 = clean.match(/(\d{1,2})\s*h\b/);
|
|
211
|
+
if (m2)
|
|
212
|
+
return `${parseInt(m2[1], 10)}00`;
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function organeInitials(input, maxLen = 8) {
|
|
216
|
+
if (!input)
|
|
217
|
+
return "";
|
|
218
|
+
const clean = normalizeNoAccents(input)
|
|
219
|
+
.replace(/['’]/g, " ")
|
|
220
|
+
.replace(/[^A-Za-z0-9\s]/g, " ")
|
|
221
|
+
.replace(/\s+/g, " ")
|
|
222
|
+
.trim();
|
|
223
|
+
if (!clean)
|
|
224
|
+
return "";
|
|
225
|
+
const parts = clean.split(" ");
|
|
226
|
+
const letters = [];
|
|
227
|
+
for (const raw of parts) {
|
|
228
|
+
const w = raw.toLowerCase();
|
|
229
|
+
if (!w)
|
|
230
|
+
continue;
|
|
231
|
+
if (STOPWORDS.has(w))
|
|
232
|
+
continue;
|
|
233
|
+
// if all uppercase, keep it
|
|
234
|
+
if (/^[A-Z0-9]{2,}$/.test(raw)) {
|
|
235
|
+
letters.push(raw.toUpperCase());
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// otherwise, take first letter if alphanumeric
|
|
239
|
+
const ch = raw[0];
|
|
240
|
+
if (/[A-Za-z0-9]/.test(ch))
|
|
241
|
+
letters.push(ch.toUpperCase());
|
|
242
|
+
}
|
|
243
|
+
const out = letters.join("");
|
|
244
|
+
return out.slice(0, maxLen);
|
|
245
|
+
}
|
|
246
|
+
function makeTypeGroupUid(dateISO, kind, hourShort, organe) {
|
|
247
|
+
const ymd = dateISO ? formatYYYYMMDD(dateISO) : "00000000";
|
|
248
|
+
const suffix = typeToSuffixStrict(kind);
|
|
249
|
+
const hh = hourShort ?? "NA";
|
|
250
|
+
const org = organe ? organeInitials(organe) : "";
|
|
251
|
+
return `RUSN${ymd}${suffix}${org ? org : ""}-${hh}`;
|
|
252
|
+
}
|
|
253
|
+
function parseISO(isoLike) {
|
|
254
|
+
if (!isoLike)
|
|
255
|
+
return null;
|
|
256
|
+
const dt = DateTime.fromISO(isoLike, { zone: PARIS });
|
|
257
|
+
return dt.isValid ? dt : null;
|
|
258
|
+
}
|
|
259
|
+
function slotOf(dt) {
|
|
260
|
+
if (!dt)
|
|
261
|
+
return "UNKNOWN";
|
|
262
|
+
const h = dt.hour + dt.minute / 60;
|
|
263
|
+
if (h < 12.5)
|
|
264
|
+
return "MATIN";
|
|
265
|
+
if (h < 19.0)
|
|
266
|
+
return "APRES-MIDI";
|
|
267
|
+
return "SOIR";
|
|
268
|
+
}
|
|
269
|
+
function trimWords(s, max = 40) {
|
|
270
|
+
const words = norm(s).split(/\s+/).filter(Boolean);
|
|
271
|
+
return words.length <= max ? words.join(" ") : words.slice(0, max).join(" ");
|
|
272
|
+
}
|
|
273
|
+
function compactTitleList(titres, maxTitles = 5) {
|
|
274
|
+
const uniq = Array.from(new Set(titres.map(t => norm(t)).filter(Boolean)));
|
|
275
|
+
return uniq.slice(0, maxTitles).join(" · ") || "(sans titre)";
|
|
276
|
+
}
|
|
277
|
+
export function makeGroupUid(date, slot) {
|
|
278
|
+
const ymd = date ? formatYYYYMMDD(date) : "00000000";
|
|
279
|
+
return `RUSN${ymd}IDS-${slot}`;
|
|
280
|
+
}
|
|
281
|
+
export function formatYYYYMMDD(dateYYYYMMDD) {
|
|
282
|
+
const [y, m, d] = dateYYYYMMDD.split("-");
|
|
283
|
+
return `${y}${m}${d}`;
|
|
284
|
+
}
|
|
285
|
+
export function makeReunionUid(agenda) {
|
|
286
|
+
const ymd = agenda.date ? formatYYYYMMDD(agenda.date) : "00000000";
|
|
287
|
+
return `${ymd}-${agenda.id}`;
|
|
288
|
+
}
|
|
289
|
+
function joinObjets(events) {
|
|
290
|
+
const objets = events
|
|
291
|
+
.map(e => (e.objet || "").trim())
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
.map(s => trimWords(s, 40));
|
|
294
|
+
if (objets.length === 0)
|
|
295
|
+
return "";
|
|
296
|
+
return objets.join(" · ");
|
|
297
|
+
}
|
|
298
|
+
// Extract hours/minutes from French text like "à 10 h 30", "de 10 h à 12 h", etc.
|
|
299
|
+
function parseTimeOriginalFR(timeOriginal) {
|
|
300
|
+
if (!timeOriginal)
|
|
301
|
+
return { start: null, end: null };
|
|
302
|
+
const txt = (timeOriginal || "")
|
|
303
|
+
.replace(/\u00A0/g, " ") // nbsp → space
|
|
304
|
+
.replace(/\s+/g, " ") // espaces multiples
|
|
305
|
+
.toLowerCase()
|
|
306
|
+
.trim();
|
|
307
|
+
// 1) "de 10 h 30 à 12 heures", "de 10h30 à 12h", "de 9 h à 11 h 15", etc.
|
|
308
|
+
const reRange = /\bde\s+(\d{1,2})\s*(?:h|:)?\s*(\d{1,2})?\s*(?:heures?)?\s*à\s*(\d{1,2})\s*(?:h|:)?\s*(\d{1,2})?\s*(?:heures?)?/i;
|
|
309
|
+
const mRange = txt.match(reRange);
|
|
310
|
+
if (mRange) {
|
|
311
|
+
const h1 = clampHour(+mRange[1]), m1 = clampMinute(mRange[2] ? +mRange[2] : 0);
|
|
312
|
+
const h2 = clampHour(+mRange[3]), m2 = clampMinute(mRange[4] ? +mRange[4] : 0);
|
|
313
|
+
return { start: toIsoTime(h1, m1), end: toIsoTime(h2, m2) };
|
|
314
|
+
}
|
|
315
|
+
// 2) "à 10 h 30", "à 10h", "A 10h30", "A 9 heures", etc.
|
|
316
|
+
const reAt = /\b(?:a|à)\s*(\d{1,2})\s*(?:h|:)?\s*(\d{1,2})?\s*(?:heures?)?/i;
|
|
317
|
+
const mAt = txt.match(reAt);
|
|
318
|
+
if (mAt) {
|
|
319
|
+
const h = clampHour(+mAt[1]), m = clampMinute(mAt[2] ? +mAt[2] : 0);
|
|
320
|
+
return { start: toIsoTime(h, m), end: null };
|
|
321
|
+
}
|
|
322
|
+
// 3) "10 h 30", "15h", "9 heures" sans 'à' / 'de ... à ...'
|
|
323
|
+
const reBare = /\b(\d{1,2})\s*(?:h|:)?\s*(\d{1,2})?\s*(?:heures?)?\b/;
|
|
324
|
+
const mBare = txt.match(reBare);
|
|
325
|
+
if (mBare) {
|
|
326
|
+
const h = clampHour(+mBare[1]), m = clampMinute(mBare[2] ? +mBare[2] : 0);
|
|
327
|
+
return { start: toIsoTime(h, m), end: null };
|
|
328
|
+
}
|
|
329
|
+
return { start: null, end: null };
|
|
330
|
+
}
|
|
331
|
+
function clampHour(h) { return Math.max(0, Math.min(23, h)); }
|
|
332
|
+
function clampMinute(m) { return Math.max(0, Math.min(59, m)); }
|
|
333
|
+
function toIsoTime(h, m) {
|
|
334
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:00.000+02:00`;
|
|
335
|
+
}
|
|
336
|
+
function slotFromTimesOrText(startISO, timeOriginal) {
|
|
337
|
+
if (startISO) {
|
|
338
|
+
const dt = parseISO(startISO);
|
|
339
|
+
if (dt)
|
|
340
|
+
return slotOf(dt);
|
|
341
|
+
}
|
|
342
|
+
const t = (timeOriginal || "").toLowerCase();
|
|
343
|
+
if (/\b(apr(?:è|e)s[-\s]?midi)\b/.test(t))
|
|
344
|
+
return "APRES-MIDI";
|
|
345
|
+
if (/\b(soir(?:ée)?)\b/.test(t))
|
|
346
|
+
return "SOIR";
|
|
347
|
+
if (/\b(matin(?:ée)?)\b/.test(t))
|
|
348
|
+
return "MATIN";
|
|
349
|
+
return "UNKNOWN";
|
|
350
|
+
}
|
|
351
|
+
function deriveTimesForEvent(ev) {
|
|
352
|
+
const directStart = ev.startTime ?? null;
|
|
353
|
+
const directEnd = ev.endTime ?? null;
|
|
354
|
+
const fromText = parseTimeOriginalFR(ev.timeOriginal);
|
|
355
|
+
const startISO = directStart ?? fromText.start ?? null;
|
|
356
|
+
const endISO = directEnd ?? fromText.end ?? null;
|
|
357
|
+
const slot = slotFromTimesOrText(startISO, ev.timeOriginal);
|
|
358
|
+
return { startISO, endISO, slot };
|
|
359
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// import { validateNonEmptyTrimmedString } from "@biryani/core"
|
|
3
|
+
// const acteurUidRegExp = /^PA\d+$/
|
|
4
|
+
// const organeUidRegExp = /^PO\d+$/
|
|
5
|
+
// export function validateSenateurUid(input: any): [any, any] {
|
|
6
|
+
// const [value, error] = validateNonEmptyTrimmedString(input)
|
|
7
|
+
// if (error !== null) {
|
|
8
|
+
// return [value, error]
|
|
9
|
+
// }
|
|
10
|
+
// if (!acteurUidRegExp.test(value)) {
|
|
11
|
+
// return [value, 'Invalid "acteur" unique ID']
|
|
12
|
+
// }
|
|
13
|
+
// return [value, null]
|
|
14
|
+
// }
|
|
15
|
+
// export function validateOrganeUid(input: any): [any, any] {
|
|
16
|
+
// const [value, error] = validateNonEmptyTrimmedString(input)
|
|
17
|
+
// if (error !== null) {
|
|
18
|
+
// return [value, error]
|
|
19
|
+
// }
|
|
20
|
+
// if (!organeUidRegExp.test(value)) {
|
|
21
|
+
// return [value, 'Invalid "organe" unique ID']
|
|
22
|
+
// }
|
|
23
|
+
// return [value, null]
|
|
24
|
+
// }
|