@@ -84,7 +84,7 @@ protected static function fromPossibleFormats($input, array $formats)
8484 }
8585 foreach ($ formats as $ format ) {
8686 $ date = DateTimeImmutable::createFromFormat ('! ' .$ format , $ input );
87- if ($ date AND $ date ->format ($ format ) === $ input ) {
87+ if ($ date and $ date ->format ($ format ) === $ input ) {
8888 return $ date ;
8989 }
9090 }
@@ -126,14 +126,77 @@ public static function fromYmdHis(string $input): DateTimeImmutable
126126 throw new \InvalidArgumentException ($ input .' is not in the format Y-m-d H:i:s ' );
127127 }
128128
129- public static function fromStrictFormat (string $ value , string $ format ): \ DateTimeImmutable
129+ public static function fromStrictFormat (string $ value , string $ format ): DateTimeImmutable
130130 {
131- $ date = \ DateTimeImmutable::createFromFormat ('! ' .$ format , $ value );
131+ $ date = DateTimeImmutable::createFromFormat ('! ' .$ format , $ value );
132132 if ($ date && ($ date ->format ($ format ) === $ value )) {
133133 return $ date ;
134134 }
135135
136136 throw new \InvalidArgumentException ("` $ value` is not a valid date/time in the format ` $ format` " );
137137 }
138138
139+ /**
140+ * Parses a time string in full ISO 8601 / RFC3339 format with optional milliseconds and timezone offset
141+ *
142+ * Can parse strings with any millisecond precision, truncating anything beyond 6 digits (which is the maximum
143+ * precision PHP supports). Copes with either `Z` or `+00:00` for the UTC timezone.
144+ *
145+ * Example valid inputs:
146+ * - 2023-05-03T10:02:03Z
147+ * - 2023-05-03T10:02:03.123456Z
148+ * - 2023-05-03T10:02:03.123456789Z
149+ * - 2023-05-03T10:02:03.123456789+01:00
150+ * - 2023-05-03T10:02:03.123456789-01:30
151+ *
152+ * @param string $value
153+ *
154+ * @return DateTimeImmutable
155+ */
156+ public static function fromIso (string $ value ): DateTimeImmutable
157+ {
158+ // Cope with Z for Zulu time instead of +00:00 - PHP offers `p` for this, but that then doesn't accept '+00:00'
159+ $ fixed_value = preg_replace ('/Z/i ' , '+00:00 ' , $ value );
160+
161+ // Pad / truncate milliseconds to 6 digits as that's the precision PHP can support
162+ // Regex is a bit dull here, but we need to be sure we can reliably find the (possibly absent)
163+ // millisecond segment without the risk of modifying unexpected parts of the string especially in
164+ // invalid values. Note that this will always replace the millis even in a 6-digit string, but it's simpler
165+ // than making the regex test for 0-5 or 7+ digits.
166+ $ fixed_value = preg_replace_callback (
167+ '/(?P<hms>T\d{2}:\d{2}:\d{2})(\.(?P<millis>\d+))?(?P<tz_prefix>[+-])/ ' ,
168+ // Can't use sprintf because we want to truncate the milliseconds, not round them
169+ // So it's simpler to just handle this as a string and cut / pad as required.
170+ fn ($ matches ) => $ matches ['hms ' ]
171+ .'. '
172+ .substr (str_pad ($ matches ['millis ' ], 6 , '0 ' ), 0 , 6 )
173+ .$ matches ['tz_prefix ' ],
174+ $ fixed_value
175+ );
176+
177+ // Not using fromStrictFormat as I want to throw with the original value, not the parsed value
178+ $ date = DateTimeImmutable::createFromFormat ('!Y-m-d\TH:i:s.uP ' , $ fixed_value );
179+ if (DateString::isoMS ($ date ?: NULL ) === $ fixed_value ) {
180+ return $ date ;
181+ }
182+ throw new \InvalidArgumentException ("` $ value` cannot be parsed as a valid ISO date-time " );
183+ }
184+
185+ /**
186+ * Remove microseconds from a time (or current time, if nothing passed)
187+ *
188+ * @param DateTimeImmutable $time
189+ *
190+ * @return DateTimeImmutable
191+ */
192+ public static function zeroMicros (DateTimeImmutable $ time = new DateTimeImmutable ()): DateTimeImmutable
193+ {
194+ return $ time ->setTime (
195+ hour: $ time ->format ('H ' ),
196+ minute: $ time ->format ('i ' ),
197+ second: $ time ->format ('s ' ),
198+ microsecond: 0
199+ );
200+ }
201+
139202}
0 commit comments