@@ -24,18 +24,18 @@ class FormatDetector
2424 'Y-m-d H:i:s ' => '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/ ' ,
2525 'Y-m-d ' => '/^\d{4}-\d{2}-\d{2}$/ ' ,
2626
27- // Formats européens
28- 'd/m/Y H:i:s ' => '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
29- 'd.m.Y H:i:s ' => '/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
30- 'd/m/Y ' => '/^\d{2}\/\d{2}\/\d{4}$/ ' ,
31- 'd.m.Y ' => '/^\d{2}\.\d{2}\.\d{4}$/ ' ,
32-
3327 // Formats américains
3428 'm/d/Y H:i:s ' => '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
3529 'm-d-Y H:i:s ' => '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
3630 'm/d/Y ' => '/^\d{2}\/\d{2}\/\d{4}$/ ' ,
3731 'm-d-Y ' => '/^\d{2}-\d{2}-\d{4}$/ ' ,
3832
33+ // Formats européens
34+ 'd/m/Y H:i:s ' => '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
35+ 'd.m.Y H:i:s ' => '/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$/ ' ,
36+ 'd/m/Y ' => '/^\d{2}\/\d{2}\/\d{4}$/ ' ,
37+ 'd.m.Y ' => '/^\d{2}\.\d{2}\.\d{4}$/ ' ,
38+
3939 // Formats spéciaux
4040 'Ymd ' => '/^\d{8}$/ ' ,
4141 'D, d M Y H:i:s O ' => '/^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}$/ ' , // RFC 2822
@@ -54,6 +54,13 @@ public static function detect(string $dateString): ?string
5454 return 'U.u ' ;
5555 }
5656
57+ // Patterns ambigus (slash) gérés séparément
58+ $ slashPattern = '/^\d{2}\/\d{2}\/\d{4}( \d{2}:\d{2}:\d{2})?$/ ' ;
59+ if (preg_match ($ slashPattern , $ dateString ) && null !== $ format = self ::detectSlashFormat ($ dateString )) {
60+ return $ format ;
61+ }
62+
63+ // Autres patterns (non ambigus)
5764 foreach (self ::FORMAT_PATTERNS as $ format => $ pattern ) {
5865 if (preg_match ($ pattern , $ dateString )) {
5966 $ date = DateTime::createFromFormat ($ format , $ dateString );
@@ -73,18 +80,79 @@ public static function detect(string $dateString): ?string
7380 }
7481 }
7582
76- private static function guessFormatFromParsedDate (string $ dateString ): string
83+ /**
84+ * Détecte le format pour les chaînes avec slash (ambiguïté DD/MM vs MM/DD).
85+ *
86+ * @param string $dateString La chaîne à analyser (ex. : '01/08/2025').
87+ *
88+ * @return ?string Le format détecté ('d/m/Y' ou 'm/d/Y') ou null en cas d'ambiguïté.
89+ */
90+ private static function detectSlashFormat (string $ dateString ): ?string
7791 {
78- $ date = new DateTime ($ dateString );
79- $ parts = [
80- 'Y ' => $ date ->format ('Y ' ),
81- 'm ' => $ date ->format ('m ' ),
82- 'd ' => $ date ->format ('d ' ),
83- 'H ' => $ date ->format ('H ' ),
84- 'i ' => $ date ->format ('i ' ),
85- 's ' => $ date ->format ('s ' ),
92+ $ formats = [
93+ 'with_time ' => [
94+ 'd/m/Y H:i:s ' => ['regex ' => '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/ ' ],
95+ 'm/d/Y H:i:s ' => ['regex ' => '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/ ' ],
96+ ],
97+ 'without_time ' => [
98+ 'd/m/Y ' => ['regex ' => '/^\d{2}\/\d{2}\/\d{4}$/ ' ],
99+ 'm/d/Y ' => ['regex ' => '/^\d{2}\/\d{2}\/\d{4}$/ ' ],
100+ ],
86101 ];
87102
103+ // Déterminer si avec ou sans temps
104+ $ hasTime = str_contains ($ dateString , ' ' );
105+
106+ $ candidates = $ hasTime ? $ formats ['with_time ' ] : $ formats ['without_time ' ];
107+
108+ $ possibleFormats = [];
109+
110+ foreach ($ candidates as $ format => $ info ) {
111+ if (preg_match ($ info ['regex ' ], $ dateString )) {
112+ $ date = DateTime::createFromFormat ($ format , $ dateString );
113+ if ($ date && $ date ->format ($ format ) === $ dateString ) {
114+ $ possibleFormats [$ format ] = [
115+ 'date ' => $ date ,
116+ 'day ' => (int ) $ date ->format ('d ' ),
117+ 'month ' => (int ) $ date ->format ('m ' ),
118+ ];
119+ }
120+ }
121+ }
122+
123+ // Si un seul candidat valide, le retourner
124+ if (count ($ possibleFormats ) === 1 ) {
125+ return key ($ possibleFormats );
126+ }
127+
128+ // Si zéro, fallback sur parsing auto
129+ if (empty ($ possibleFormats )) {
130+ return self ::guessFormatFromParsedDate ($ dateString );
131+ }
132+
133+ // Ambiguïté : appliquer heuristique
134+ // Extraire les valeurs parsed pour chaque format
135+ $ ddmm = $ possibleFormats ['d/m/Y ' ] ?? null ; // Ou avec H:i:s
136+ $ mmdd = $ possibleFormats ['m/d/Y ' ] ?? null ; // Ou avec H:i:s
137+
138+ // Si jour >12 dans DD/MM, impossible → préférer MM/DD
139+ if ($ ddmm && $ ddmm ['day ' ] > 12 ) {
140+ return 'm/d/Y ' ;
141+ }
142+
143+ // Si mois >12 dans MM/DD, impossible → préférer DD/MM
144+ if ($ mmdd && $ mmdd ['month ' ] > 12 ) {
145+ return 'd/m/Y ' ;
146+ }
147+
148+ // Sinon ambigu (≤12 pour les deux) : biais européen DD/MM
149+ return null ; // on laisse le foreach s'en occuper suivant l'ordre de definition des regex
150+ }
151+
152+ private static function guessFormatFromParsedDate (string $ dateString ): string
153+ {
154+ $ date = new DateTime ($ dateString );
155+
88156 // Analyse la structure originale pour déterminer les séparateurs
89157 if (str_contains ($ dateString , 'T ' )) {
90158 $ format = 'Y-m-d\TH:i:s ' ;
0 commit comments