Checkpoint: Ajout de la fonction extractDateCloture dans rssEngine.ts : détection par expressions régulières des patterns courants en français (date limite, clôture le, avant le, jusqu'au, dépôt des dossiers, délai de réponse, réception, soumission). La date extraite est insérée dans le champ dateCloture lors de la collecte RSS des AAP.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "598c445e",
|
"version": "4a018fb6",
|
||||||
"timestamp": 1782826461485
|
"timestamp": 1782828810514
|
||||||
}
|
}
|
||||||
@@ -298,6 +298,74 @@ function parseDate(dateStr?: string): Date | null {
|
|||||||
return isNaN(d.getTime()) ? null : d;
|
return isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tente d'extraire une date de clôture depuis le texte d'un AAP.
|
||||||
|
* Patterns reconnus (fr) :
|
||||||
|
* "date limite : 30 septembre 2026"
|
||||||
|
* "clôture le 30/09/2026"
|
||||||
|
* "avant le 30 sept. 2026"
|
||||||
|
* "jusqu'au 30-09-2026"
|
||||||
|
* "dépôt des dossiers : 30 septembre 2026"
|
||||||
|
* "délai de réponse : 30 septembre 2026"
|
||||||
|
* Retourne un objet Date ou null si non détecté.
|
||||||
|
*/
|
||||||
|
const MONTHS_FR: Record<string, number> = {
|
||||||
|
janvier: 0, février: 1, fevrier: 1, mars: 2, avril: 3, mai: 4, juin: 5,
|
||||||
|
juillet: 6, août: 7, aout: 7, septembre: 8, octobre: 9, novembre: 10, décembre: 11, decembre: 11,
|
||||||
|
janv: 0, févr: 1, fevr: 1, avr: 3, juil: 6, sept: 8, oct: 9, nov: 10, déc: 11, dec: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseFrDate(dateStr: string): Date | null {
|
||||||
|
// Format JJ/MM/AAAA ou JJ-MM-AAAA
|
||||||
|
const numMatch = dateStr.match(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})/);
|
||||||
|
if (numMatch) {
|
||||||
|
const d = new Date(parseInt(numMatch[3]), parseInt(numMatch[2]) - 1, parseInt(numMatch[1]));
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
// Format JJ mois AAAA (ex: "30 septembre 2026")
|
||||||
|
const textMatch = dateStr.match(/(\d{1,2})\s+([a-zéèêàù]+)\.?\s+(\d{4})/i);
|
||||||
|
if (textMatch) {
|
||||||
|
const month = MONTHS_FR[textMatch[2].toLowerCase().replace(/\.$/, "")];
|
||||||
|
if (month !== undefined) {
|
||||||
|
const d = new Date(parseInt(textMatch[3]), month, parseInt(textMatch[1]));
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractDateCloture(text: string): Date | null {
|
||||||
|
const patterns = [
|
||||||
|
// "date limite (de dépôt|de candidature|d'envoi|de soumission)? : ..."
|
||||||
|
/date\s+limite(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "clôture (des candidatures|des dossiers|le|au)? : ..."
|
||||||
|
/cl[oô]ture(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "clôture le ..."
|
||||||
|
/cl[oô]ture\s+le\s+([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "avant le ..."
|
||||||
|
/avant\s+le\s+([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "jusqu'au ..."
|
||||||
|
/jusqu['\u2019]au\s+([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "dépôt des dossiers : ..."
|
||||||
|
/d[eé]p[oô]t(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "délai de réponse : ..."
|
||||||
|
/d[eé]lai(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "réception des dossiers : ..."
|
||||||
|
/r[eé]ception(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
// "soumission (des candidatures)? : ..."
|
||||||
|
/soumission(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = text.match(pattern);
|
||||||
|
if (match?.[1]) {
|
||||||
|
const d = parseFrDate(match[1].trim());
|
||||||
|
if (d && d.getFullYear() >= 2020 && d.getFullYear() <= 2035) return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function stripHtml(html: string): string {
|
function stripHtml(html: string): string {
|
||||||
return html
|
return html
|
||||||
.replace(/<[^>]*>/g, "")
|
.replace(/<[^>]*>/g, "")
|
||||||
@@ -534,6 +602,9 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
// Générer le résumé IA pour les AAP pertinents
|
// Générer le résumé IA pour les AAP pertinents
|
||||||
const iaResume = await generateSummary(title, description);
|
const iaResume = await generateSummary(title, description);
|
||||||
|
|
||||||
|
// Extraire la date de clôture depuis le texte
|
||||||
|
const dateCloture = extractDateCloture(fullText);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.insert(aapItems).values({
|
await db.insert(aapItems).values({
|
||||||
dedupKey,
|
dedupKey,
|
||||||
@@ -544,6 +615,7 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
|
|||||||
departements: JSON.stringify(departement ? [departement] : []),
|
departements: JSON.stringify(departement ? [departement] : []),
|
||||||
lien: link || null,
|
lien: link || null,
|
||||||
datePublication: pubDate,
|
datePublication: pubDate,
|
||||||
|
dateCloture: dateCloture || null,
|
||||||
iaRelevant: aiResult.relevant,
|
iaRelevant: aiResult.relevant,
|
||||||
iaClassifiedBy: aiResult.classifiedBy,
|
iaClassifiedBy: aiResult.classifiedBy,
|
||||||
iaReason: aiResult.reason,
|
iaReason: aiResult.reason,
|
||||||
|
|||||||
Reference in New Issue
Block a user