From c6b89c5b46bf48d8956c385a3e7132fb8d191b73 Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 1 Jul 2026 03:33:04 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20de=20nouveaux=20patterns?= =?UTF-8?q?=20de=20d=C3=A9tection=20(fin=20de=20d=C3=A9p=C3=B4t,=20fin=20d?= =?UTF-8?q?es=20candidatures,=20remise,=20r=C3=A9pondre=20avant=20le,=20ou?= =?UTF-8?q?vert=20jusqu'au,=20candidatures=20jusqu'au).=20Am=C3=A9lioratio?= =?UTF-8?q?n=20de=20parseFrDate=20:=20support=20JJ.MM.AAAA,=20ISO=20AAAA-M?= =?UTF-8?q?M-JJ,=20"1er=20mois".=20Normalisation=20du=20texte=20(retours?= =?UTF-8?q?=20=C3=A0=20la=20ligne).=20Fonction=20findDateInStr=20pour=20ex?= =?UTF-8?q?traire=20une=20date=20m=C3=AAme=20quand=20du=20texte=20suit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/__manus__/version.json | 4 +- server/rssEngine.ts | 75 ++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 38c241b..d607bd2 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "4a018fb6", - "timestamp": 1782828810514 + "version": "0f628f8c", + "timestamp": 1782891184609 } \ No newline at end of file diff --git a/server/rssEngine.ts b/server/rssEngine.ts index 136243a..c4f58d2 100644 --- a/server/rssEngine.ts +++ b/server/rssEngine.ts @@ -316,14 +316,15 @@ const MONTHS_FR: Record = { }; 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})/); + const s = dateStr.trim(); + // Format JJ/MM/AAAA ou JJ-MM-AAAA ou JJ.MM.AAAA + const numMatch = s.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); + // Format JJ mois AAAA ou "1er mois AAAA" + const textMatch = s.match(/(\d{1,2})(?:er|e)?\s+([a-z\u00e9\u00e8\u00ea\u00e0\u00f9\u00fb]+)\.?\s+(\d{4})/i); if (textMatch) { const month = MONTHS_FR[textMatch[2].toLowerCase().replace(/\.$/, "")]; if (month !== undefined) { @@ -331,35 +332,65 @@ function parseFrDate(dateStr: string): Date | null { return isNaN(d.getTime()) ? null : d; } } + // Format AAAA-MM-JJ (ISO) + const isoMatch = s.match(/(\d{4})-(\d{2})-(\d{2})/); + if (isoMatch) { + const d = new Date(parseInt(isoMatch[1]), parseInt(isoMatch[2]) - 1, parseInt(isoMatch[3])); + return isNaN(d.getTime()) ? null : d; + } + return null; +} + +// Extrait la première date valide trouvée dans une chaîne +function findDateInStr(s: string): Date | null { + // Essayer directement + const d = parseFrDate(s); + if (d) return d; + // Chercher un sous-motif date dans la chaîne (utile quand du texte suit la date) + const sub = s.match(/(\d{1,2}(?:er|e)?[\s.\/\-][\w\s.]+\d{4}|\d{4}-\d{2}-\d{2}|\d{1,2}[\/\.\-]\d{1,2}[\/\.\-]\d{4})/); + if (sub) return parseFrDate(sub[1]); 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, + // Normaliser : supprimer les retours à la ligne et les espaces multiples + const t = text.replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " "); + + const patterns: RegExp[] = [ + // "date limite (de dépôt|de candidature|d'envoi|de soumission|de remise)? ..." + /date\s+limite(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "fin (de dépôt|des candidatures|de soumission|d'inscription|de réception)?" + /fin\s+(?:de\s+)?(?:d[eé]p[oô]t|des\s+candidatures|de\s+soumission|d['\u2019]inscription|de\s+r[eé]ception|des\s+dossiers)(?:[^:\d]{0,30})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "clôture (des candidatures|des dossiers|de l'appel|le|au|:)?" + /cl[oô]ture(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, // "clôture le ..." - /cl[oô]ture\s+le\s+([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, + /cl[oô]ture\s+le\s+([\d][\w\s.\/\-]+\d{4})/i, // "avant le ..." - /avant\s+le\s+([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, + /avant\s+le\s+([\d][\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, + /jusqu['\u2019]au\s+([\d][\w\s.\/\-]+\d{4})/i, + // "dépôt (des dossiers|des candidatures|des projets)? ..." + /d[eé]p[oô]t(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "délai (de réponse|de dépôt|de soumission)? ..." + /d[eé]lai(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "réception (des dossiers|des candidatures)? ..." + /r[eé]ception(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "soumission (des candidatures|des projets)? ..." + /soumission(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "remise (des offres|des dossiers|des candidatures)? ..." + /remise(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i, + // "candidatures (attendues|acceptées|ouvertes)? jusqu'au ..." + /candidatures?(?:[^\d]{0,40})?jusqu['\u2019]au\s+([\d][\w\s.\/\-]+\d{4})/i, + // "répondre avant le ..." + /r[eé]pondre\s+avant\s+le\s+([\d][\w\s.\/\-]+\d{4})/i, + // "ouvert jusqu'au ..." + /ouvert(?:e)?\s+jusqu['\u2019]au\s+([\d][\w\s.\/\-]+\d{4})/i, ]; for (const pattern of patterns) { - const match = text.match(pattern); + const match = t.match(pattern); if (match?.[1]) { - const d = parseFrDate(match[1].trim()); + const d = findDateInStr(match[1].trim()); if (d && d.getFullYear() >= 2020 && d.getFullYear() <= 2035) return d; } }