Checkpoint: Ajout de nouveaux patterns de détection (fin de dépôt, fin des candidatures, remise, répondre avant le, ouvert jusqu'au, candidatures jusqu'au). Amélioration de parseFrDate : support JJ.MM.AAAA, ISO AAAA-MM-JJ, "1er mois". Normalisation du texte (retours à la ligne). Fonction findDateInStr pour extraire une date même quand du texte suit.

This commit is contained in:
Manus
2026-07-01 03:33:04 -04:00
parent 53514b0540
commit c6b89c5b46
2 changed files with 55 additions and 24 deletions

View File

@@ -1,4 +1,4 @@
{ {
"version": "4a018fb6", "version": "0f628f8c",
"timestamp": 1782828810514 "timestamp": 1782891184609
} }

View File

@@ -316,14 +316,15 @@ const MONTHS_FR: Record<string, number> = {
}; };
function parseFrDate(dateStr: string): Date | null { function parseFrDate(dateStr: string): Date | null {
// Format JJ/MM/AAAA ou JJ-MM-AAAA const s = dateStr.trim();
const numMatch = dateStr.match(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})/); // 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) { if (numMatch) {
const d = new Date(parseInt(numMatch[3]), parseInt(numMatch[2]) - 1, parseInt(numMatch[1])); const d = new Date(parseInt(numMatch[3]), parseInt(numMatch[2]) - 1, parseInt(numMatch[1]));
return isNaN(d.getTime()) ? null : d; return isNaN(d.getTime()) ? null : d;
} }
// Format JJ mois AAAA (ex: "30 septembre 2026") // Format JJ mois AAAA ou "1er mois AAAA"
const textMatch = dateStr.match(/(\d{1,2})\s+([a-zéèêàù]+)\.?\s+(\d{4})/i); const textMatch = s.match(/(\d{1,2})(?:er|e)?\s+([a-z\u00e9\u00e8\u00ea\u00e0\u00f9\u00fb]+)\.?\s+(\d{4})/i);
if (textMatch) { if (textMatch) {
const month = MONTHS_FR[textMatch[2].toLowerCase().replace(/\.$/, "")]; const month = MONTHS_FR[textMatch[2].toLowerCase().replace(/\.$/, "")];
if (month !== undefined) { if (month !== undefined) {
@@ -331,35 +332,65 @@ function parseFrDate(dateStr: string): Date | null {
return isNaN(d.getTime()) ? null : d; 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; return null;
} }
export function extractDateCloture(text: string): Date | null { export function extractDateCloture(text: string): Date | null {
const patterns = [ // Normaliser : supprimer les retours à la ligne et les espaces multiples
// "date limite (de dépôt|de candidature|d'envoi|de soumission)? : ..." const t = text.replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ");
/date\s+limite(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i,
// "clôture (des candidatures|des dossiers|le|au)? : ..." const patterns: RegExp[] = [
/cl[oô]ture(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, // "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ô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 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'au ..."
/jusqu['\u2019]au\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 : ..." // "dépôt (des dossiers|des candidatures|des projets)? ..."
/d[eé]p[oô]t(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, /d[eé]p[oô]t(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i,
// "délai de réponse : ..." // "délai (de réponse|de dépôt|de soumission)? ..."
/d[eé]lai(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, /d[eé]lai(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i,
// "réception des dossiers : ..." // "réception (des dossiers|des candidatures)? ..."
/r[eé]ception(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, /r[eé]ception(?:[^:\d]{0,50})?[:\s]+([\d][\w\s.\/\-]+\d{4})/i,
// "soumission (des candidatures)? : ..." // "soumission (des candidatures|des projets)? ..."
/soumission(?:[^:]{0,40})?:\s*([\d]{1,2}[\s\/\-][\w\s.]+\d{4})/i, /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) { for (const pattern of patterns) {
const match = text.match(pattern); const match = t.match(pattern);
if (match?.[1]) { 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; if (d && d.getFullYear() >= 2020 && d.getFullYear() <= 2035) return d;
} }
} }