@@ -80,4 +80,85 @@ public static function validateDateNotFuture(string $dateAmerican): bool
8080 }
8181 return true ;
8282 }
83+
84+ public static function validateDateUTCWithoutTimezone (string $ date ): bool
85+ {
86+ $ format = 'Y-m-d\TH:i:s ' ;
87+ $ d = DateTime::createFromFormat ($ format , $ date );
88+ return $ d && $ d ->format ($ format ) === $ date ;
89+ }
90+
91+ public static function validateDateIso8601 (string $ input ): bool
92+ {
93+ if (empty ($ input )) {
94+ return false ;
95+ }
96+ return self ::isCalendarDateTime ($ input ) || self ::isDuration ($ input ) || self ::isWeekDate ($ input )
97+ || self ::isOrdinalDate ($ input ) || self ::isInterval ($ input );
98+ }
99+
100+ private static function isCalendarDateTime (string $ input ): bool
101+ {
102+ $ pattern = '/^\d{4}((-?\d{2})(-?\d{2})?)?(T\d{2}(:?\d{2}(:?\d{2}(\.\d+)?)?)?(Z|[+-]\d{2}(:?\d{2})?)?)?$/ ' ;
103+ if (!preg_match ($ pattern , $ input )) {
104+ return false ;
105+ }
106+ try {
107+ $ date = new \DateTime ($ input );
108+ if (str_contains ($ input , '- ' )) {
109+ $ parts = explode ('T ' , $ input )[0 ];
110+ if (count (explode ('- ' , $ parts )) === 3 && $ date ->format ('Y-m-d ' ) !== $ parts ) {
111+ return false ;
112+ }
113+ }
114+ return true ;
115+ } catch (\Exception ) {
116+ return false ;
117+ }
118+ }
119+
120+ private static function isWeekDate (string $ input ): bool
121+ {
122+ $ pattern = '/^(\d{4})-?W(0[1-9]|[1-4][0-9]|5[0-3])(-?([1-7]))?$/ ' ;
123+ if (!preg_match ($ pattern , $ input , $ matches )) {
124+ return false ;
125+ }
126+ $ year = (int )$ matches [1 ];
127+ $ week = (int )$ matches [2 ];
128+ if ($ week === 53 ) {
129+ $ date = new \DateTime ();
130+ $ date ->setISODate ($ year , 53 );
131+ return $ date ->format ('W ' ) === '53 ' ;
132+ }
133+ return true ;
134+ }
135+
136+ private static function isOrdinalDate (string $ input ): bool
137+ {
138+ if (!preg_match ('/^(\d{4})-?(\d{3})$/ ' , $ input , $ matches )) {
139+ return false ;
140+ }
141+ $ year = (int )$ matches [1 ];
142+ $ dayOfYear = (int )$ matches [2 ];
143+ $ isLeap = ($ year % 4 === 0 && ($ year % 100 !== 0 || $ year % 400 === 0 ));
144+ return $ dayOfYear >= 1 && $ dayOfYear <= ($ isLeap ? 366 : 365 );
145+ }
146+
147+ private static function isDuration (string $ input ): bool
148+ {
149+ $ pattern = '/^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/ ' ;
150+ return (bool ) preg_match ($ pattern , $ input );
151+ }
152+
153+ private static function isInterval (string $ input ): bool
154+ {
155+ if (!str_contains ($ input , '/ ' )) {
156+ return false ;
157+ }
158+ $ parts = explode ('/ ' , $ input );
159+ if (count ($ parts ) !== 2 ) {
160+ return false ;
161+ }
162+ return (self ::validateDateIso8601 ($ parts [0 ]) && self ::validateDateIso8601 ($ parts [1 ]));
163+ }
83164}
0 commit comments