@@ -84,7 +84,7 @@ protected static function fromPossibleFormats($input, array $formats)
84
84
}
85
85
foreach ($ formats as $ format ) {
86
86
$ date = DateTimeImmutable::createFromFormat ('! ' .$ format , $ input );
87
- if ($ date AND $ date ->format ($ format ) === $ input ) {
87
+ if ($ date and $ date ->format ($ format ) === $ input ) {
88
88
return $ date ;
89
89
}
90
90
}
@@ -126,14 +126,77 @@ public static function fromYmdHis(string $input): DateTimeImmutable
126
126
throw new \InvalidArgumentException ($ input .' is not in the format Y-m-d H:i:s ' );
127
127
}
128
128
129
- public static function fromStrictFormat (string $ value , string $ format ): \ DateTimeImmutable
129
+ public static function fromStrictFormat (string $ value , string $ format ): DateTimeImmutable
130
130
{
131
- $ date = \ DateTimeImmutable::createFromFormat ('! ' .$ format , $ value );
131
+ $ date = DateTimeImmutable::createFromFormat ('! ' .$ format , $ value );
132
132
if ($ date && ($ date ->format ($ format ) === $ value )) {
133
133
return $ date ;
134
134
}
135
135
136
136
throw new \InvalidArgumentException ("` $ value` is not a valid date/time in the format ` $ format` " );
137
137
}
138
138
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
+
139
202
}
0 commit comments