1 | // This is a part of Chrono. |
2 | // See README.md and LICENSE.txt for details. |
3 | |
4 | /*! |
5 | `strftime`/`strptime`-inspired date and time formatting syntax. |
6 | |
7 | ## Specifiers |
8 | |
9 | The following specifiers are available both to formatting and parsing. |
10 | |
11 | | Spec. | Example | Description | |
12 | |-------|----------|----------------------------------------------------------------------------| |
13 | | | | **DATE SPECIFIERS:** | |
14 | | `%Y` | `2001` | The full proleptic Gregorian year, zero-padded to 4 digits. chrono supports years from -262144 to 262143. Note: years before 1 BCE or after 9999 CE, require an initial sign (+/-).| |
15 | | `%C` | `20` | The proleptic Gregorian year divided by 100, zero-padded to 2 digits. [^1] | |
16 | | `%y` | `01` | The proleptic Gregorian year modulo 100, zero-padded to 2 digits. [^1] | |
17 | | | | | |
18 | | `%m` | `07` | Month number (01--12), zero-padded to 2 digits. | |
19 | | `%b` | `Jul` | Abbreviated month name. Always 3 letters. | |
20 | | `%B` | `July` | Full month name. Also accepts corresponding abbreviation in parsing. | |
21 | | `%h` | `Jul` | Same as `%b`. | |
22 | | | | | |
23 | | `%d` | `08` | Day number (01--31), zero-padded to 2 digits. | |
24 | | `%e` | ` 8` | Same as `%d` but space-padded. Same as `%_d`. | |
25 | | | | | |
26 | | `%a` | `Sun` | Abbreviated weekday name. Always 3 letters. | |
27 | | `%A` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing. | |
28 | | `%w` | `0` | Sunday = 0, Monday = 1, ..., Saturday = 6. | |
29 | | `%u` | `7` | Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601) | |
30 | | | | | |
31 | | `%U` | `28` | Week number starting with Sunday (00--53), zero-padded to 2 digits. [^2] | |
32 | | `%W` | `27` | Same as `%U`, but week 1 starts with the first Monday in that year instead.| |
33 | | | | | |
34 | | `%G` | `2001` | Same as `%Y` but uses the year number in ISO 8601 week date. [^3] | |
35 | | `%g` | `01` | Same as `%y` but uses the year number in ISO 8601 week date. [^3] | |
36 | | `%V` | `27` | Same as `%U` but uses the week number in ISO 8601 week date (01--53). [^3] | |
37 | | | | | |
38 | | `%j` | `189` | Day of the year (001--366), zero-padded to 3 digits. | |
39 | | | | | |
40 | | `%D` | `07/08/01` | Month-day-year format. Same as `%m/%d/%y`. | |
41 | | `%x` | `07/08/01` | Locale's date representation (e.g., 12/31/99). | |
42 | | `%F` | `2001-07-08` | Year-month-day format (ISO 8601). Same as `%Y-%m-%d`. | |
43 | | `%v` | ` 8-Jul-2001` | Day-month-year format. Same as `%e-%b-%Y`. | |
44 | | | | | |
45 | | | | **TIME SPECIFIERS:** | |
46 | | `%H` | `00` | Hour number (00--23), zero-padded to 2 digits. | |
47 | | `%k` | ` 0` | Same as `%H` but space-padded. Same as `%_H`. | |
48 | | `%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits. | |
49 | | `%l` | `12` | Same as `%I` but space-padded. Same as `%_I`. | |
50 | | | | | |
51 | | `%P` | `am` | `am` or `pm` in 12-hour clocks. | |
52 | | `%p` | `AM` | `AM` or `PM` in 12-hour clocks. | |
53 | | | | | |
54 | | `%M` | `34` | Minute number (00--59), zero-padded to 2 digits. | |
55 | | `%S` | `60` | Second number (00--60), zero-padded to 2 digits. [^4] | |
56 | | `%f` | `026490000` | The fractional seconds (in nanoseconds) since last whole second. [^7] | |
57 | | `%.f` | `.026490`| Similar to `.%f` but left-aligned. These all consume the leading dot. [^7] | |
58 | | `%.3f`| `.026` | Similar to `.%f` but left-aligned but fixed to a length of 3. [^7] | |
59 | | `%.6f`| `.026490` | Similar to `.%f` but left-aligned but fixed to a length of 6. [^7] | |
60 | | `%.9f`| `.026490000` | Similar to `.%f` but left-aligned but fixed to a length of 9. [^7] | |
61 | | `%3f` | `026` | Similar to `%.3f` but without the leading dot. [^7] | |
62 | | `%6f` | `026490` | Similar to `%.6f` but without the leading dot. [^7] | |
63 | | `%9f` | `026490000` | Similar to `%.9f` but without the leading dot. [^7] | |
64 | | | | | |
65 | | `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. | |
66 | | `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. | |
67 | | `%X` | `00:34:60` | Locale's time representation (e.g., 23:13:48). | |
68 | | `%r` | `12:34:60 AM` | Hour-minute-second format in 12-hour clocks. Same as `%I:%M:%S %p`. | |
69 | | | | | |
70 | | | | **TIME ZONE SPECIFIERS:** | |
71 | | `%Z` | `ACST` | Local time zone name. Skips all non-whitespace characters during parsing. Identical to `%:z` when formatting. [^8] | |
72 | | `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). | |
73 | | `%:z` | `+09:30` | Same as `%z` but with a colon. | |
74 | |`%::z`|`+09:30:00`| Offset from the local time to UTC with seconds. | |
75 | |`%:::z`| `+09` | Offset from the local time to UTC without minutes. | |
76 | | `%#z` | `+09` | *Parsing only:* Same as `%z` but allows minutes to be missing or present. | |
77 | | | | | |
78 | | | | **DATE & TIME SPECIFIERS:** | |
79 | |`%c`|`Sun Jul 8 00:34:60 2001`|Locale's date and time (e.g., Thu Mar 3 23:05:25 2005). | |
80 | | `%+` | `2001-07-08T00:34:60.026490+09:30` | ISO 8601 / RFC 3339 date & time format. [^5] | |
81 | | | | | |
82 | | `%s` | `994518299` | UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC. [^6]| |
83 | | | | | |
84 | | | | **SPECIAL SPECIFIERS:** | |
85 | | `%t` | | Literal tab (`\t`). | |
86 | | `%n` | | Literal newline (`\n`). | |
87 | | `%%` | | Literal percent sign. | |
88 | |
89 | It is possible to override the default padding behavior of numeric specifiers `%?`. |
90 | This is not allowed for other specifiers and will result in the `BAD_FORMAT` error. |
91 | |
92 | Modifier | Description |
93 | -------- | ----------- |
94 | `%-?` | Suppresses any padding including spaces and zeroes. (e.g. `%j` = `012`, `%-j` = `12`) |
95 | `%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`) |
96 | `%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`) |
97 | |
98 | Notes: |
99 | |
100 | [^1]: `%C`, `%y`: |
101 | This is floor division, so 100 BCE (year number -99) will print `-1` and `99` respectively. |
102 | |
103 | [^2]: `%U`: |
104 | Week 1 starts with the first Sunday in that year. |
105 | It is possible to have week 0 for days before the first Sunday. |
106 | |
107 | [^3]: `%G`, `%g`, `%V`: |
108 | Week 1 is the first week with at least 4 days in that year. |
109 | Week 0 does not exist, so this should be used with `%G` or `%g`. |
110 | |
111 | [^4]: `%S`: |
112 | It accounts for leap seconds, so `60` is possible. |
113 | |
114 | [^5]: `%+`: Same as `%Y-%m-%dT%H:%M:%S%.f%:z`, i.e. 0, 3, 6 or 9 fractional |
115 | digits for seconds and colons in the time zone offset. |
116 | <br> |
117 | <br> |
118 | This format also supports having a `Z` or `UTC` in place of `%:z`. They |
119 | are equivalent to `+00:00`. |
120 | <br> |
121 | <br> |
122 | Note that all `T`, `Z`, and `UTC` are parsed case-insensitively. |
123 | <br> |
124 | <br> |
125 | The typical `strftime` implementations have different (and locale-dependent) |
126 | formats for this specifier. While Chrono's format for `%+` is far more |
127 | stable, it is best to avoid this specifier if you want to control the exact |
128 | output. |
129 | |
130 | [^6]: `%s`: |
131 | This is not padded and can be negative. |
132 | For the purpose of Chrono, it only accounts for non-leap seconds |
133 | so it slightly differs from ISO C `strftime` behavior. |
134 | |
135 | [^7]: `%f`, `%.f`, `%.3f`, `%.6f`, `%.9f`, `%3f`, `%6f`, `%9f`: |
136 | <br> |
137 | The default `%f` is right-aligned and always zero-padded to 9 digits |
138 | for the compatibility with glibc and others, |
139 | so it always counts the number of nanoseconds since the last whole second. |
140 | E.g. 7ms after the last second will print `007000000`, |
141 | and parsing `7000000` will yield the same. |
142 | <br> |
143 | <br> |
144 | The variant `%.f` is left-aligned and print 0, 3, 6 or 9 fractional digits |
145 | according to the precision. |
146 | E.g. 70ms after the last second under `%.f` will print `.070` (note: not `.07`), |
147 | and parsing `.07`, `.070000` etc. will yield the same. |
148 | Note that they can print or read nothing if the fractional part is zero or |
149 | the next character is not `.`. |
150 | <br> |
151 | <br> |
152 | The variant `%.3f`, `%.6f` and `%.9f` are left-aligned and print 3, 6 or 9 fractional digits |
153 | according to the number preceding `f`. |
154 | E.g. 70ms after the last second under `%.3f` will print `.070` (note: not `.07`), |
155 | and parsing `.07`, `.070000` etc. will yield the same. |
156 | Note that they can read nothing if the fractional part is zero or |
157 | the next character is not `.` however will print with the specified length. |
158 | <br> |
159 | <br> |
160 | The variant `%3f`, `%6f` and `%9f` are left-aligned and print 3, 6 or 9 fractional digits |
161 | according to the number preceding `f`, but without the leading dot. |
162 | E.g. 70ms after the last second under `%3f` will print `070` (note: not `07`), |
163 | and parsing `07`, `070000` etc. will yield the same. |
164 | Note that they can read nothing if the fractional part is zero. |
165 | |
166 | [^8]: `%Z`: |
167 | Since `chrono` is not aware of timezones beyond their offsets, this specifier |
168 | **only prints the offset** when used for formatting. The timezone abbreviation |
169 | will NOT be printed. See [this issue](https://github.com/chronotope/chrono/issues/960) |
170 | for more information. |
171 | <br> |
172 | <br> |
173 | Offset will not be populated from the parsed data, nor will it be validated. |
174 | Timezone is completely ignored. Similar to the glibc `strptime` treatment of |
175 | this format code. |
176 | <br> |
177 | <br> |
178 | It is not possible to reliably convert from an abbreviation to an offset, |
179 | for example CDT can mean either Central Daylight Time (North America) or |
180 | China Daylight Time. |
181 | */ |
182 | |
183 | #[cfg (feature = "unstable-locales" )] |
184 | extern crate alloc; |
185 | |
186 | #[cfg (feature = "unstable-locales" )] |
187 | use alloc::vec::Vec; |
188 | |
189 | #[cfg (feature = "unstable-locales" )] |
190 | use super::{locales, Locale}; |
191 | use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad}; |
192 | |
193 | #[cfg (feature = "unstable-locales" )] |
194 | type Fmt<'a> = Vec<Item<'a>>; |
195 | #[cfg (not(feature = "unstable-locales" ))] |
196 | type Fmt<'a> = &'static [Item<'static>]; |
197 | |
198 | static D_FMT: &[Item<'static>] = |
199 | &[num0!(Month), lit!("/" ), num0!(Day), lit!("/" ), num0!(YearMod100)]; |
200 | static D_T_FMT: &[Item<'static>] = &[ |
201 | fix!(ShortWeekdayName), |
202 | sp!(" " ), |
203 | fix!(ShortMonthName), |
204 | sp!(" " ), |
205 | nums!(Day), |
206 | sp!(" " ), |
207 | num0!(Hour), |
208 | lit!(":" ), |
209 | num0!(Minute), |
210 | lit!(":" ), |
211 | num0!(Second), |
212 | sp!(" " ), |
213 | num0!(Year), |
214 | ]; |
215 | static T_FMT: &[Item<'static>] = &[num0!(Hour), lit!(":" ), num0!(Minute), lit!(":" ), num0!(Second)]; |
216 | |
217 | /// Parsing iterator for `strftime`-like format strings. |
218 | #[derive (Clone, Debug)] |
219 | pub struct StrftimeItems<'a> { |
220 | /// Remaining portion of the string. |
221 | remainder: &'a str, |
222 | /// If the current specifier is composed of multiple formatting items (e.g. `%+`), |
223 | /// parser refers to the statically reconstructed slice of them. |
224 | /// If `recons` is not empty they have to be returned earlier than the `remainder`. |
225 | recons: Fmt<'a>, |
226 | /// Date format |
227 | d_fmt: Fmt<'a>, |
228 | /// Date and time format |
229 | d_t_fmt: Fmt<'a>, |
230 | /// Time format |
231 | t_fmt: Fmt<'a>, |
232 | } |
233 | |
234 | impl<'a> StrftimeItems<'a> { |
235 | /// Creates a new parsing iterator from the `strftime`-like format string. |
236 | #[must_use ] |
237 | pub fn new(s: &'a str) -> StrftimeItems<'a> { |
238 | Self::with_remainer(s) |
239 | } |
240 | |
241 | /// Creates a new parsing iterator from the `strftime`-like format string. |
242 | #[cfg (feature = "unstable-locales" )] |
243 | #[cfg_attr (docsrs, doc(cfg(feature = "unstable-locales" )))] |
244 | #[must_use ] |
245 | pub fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> { |
246 | let d_fmt = StrftimeItems::new(locales::d_fmt(locale)).collect(); |
247 | let d_t_fmt = StrftimeItems::new(locales::d_t_fmt(locale)).collect(); |
248 | let t_fmt = StrftimeItems::new(locales::t_fmt(locale)).collect(); |
249 | |
250 | StrftimeItems { remainder: s, recons: Vec::new(), d_fmt, d_t_fmt, t_fmt } |
251 | } |
252 | |
253 | #[cfg (not(feature = "unstable-locales" ))] |
254 | fn with_remainer(s: &'a str) -> StrftimeItems<'a> { |
255 | static FMT_NONE: &[Item<'static>; 0] = &[]; |
256 | |
257 | StrftimeItems { |
258 | remainder: s, |
259 | recons: FMT_NONE, |
260 | d_fmt: D_FMT, |
261 | d_t_fmt: D_T_FMT, |
262 | t_fmt: T_FMT, |
263 | } |
264 | } |
265 | |
266 | #[cfg (feature = "unstable-locales" )] |
267 | fn with_remainer(s: &'a str) -> StrftimeItems<'a> { |
268 | StrftimeItems { |
269 | remainder: s, |
270 | recons: Vec::new(), |
271 | d_fmt: D_FMT.to_vec(), |
272 | d_t_fmt: D_T_FMT.to_vec(), |
273 | t_fmt: T_FMT.to_vec(), |
274 | } |
275 | } |
276 | } |
277 | |
278 | const HAVE_ALTERNATES: &str = "z" ; |
279 | |
280 | impl<'a> Iterator for StrftimeItems<'a> { |
281 | type Item = Item<'a>; |
282 | |
283 | fn next(&mut self) -> Option<Item<'a>> { |
284 | // we have some reconstructed items to return |
285 | if !self.recons.is_empty() { |
286 | let item; |
287 | #[cfg (feature = "unstable-locales" )] |
288 | { |
289 | item = self.recons.remove(0); |
290 | } |
291 | #[cfg (not(feature = "unstable-locales" ))] |
292 | { |
293 | item = self.recons[0].clone(); |
294 | self.recons = &self.recons[1..]; |
295 | } |
296 | return Some(item); |
297 | } |
298 | |
299 | match self.remainder.chars().next() { |
300 | // we are done |
301 | None => None, |
302 | |
303 | // the next item is a specifier |
304 | Some('%' ) => { |
305 | self.remainder = &self.remainder[1..]; |
306 | |
307 | macro_rules! next { |
308 | () => { |
309 | match self.remainder.chars().next() { |
310 | Some(x) => { |
311 | self.remainder = &self.remainder[x.len_utf8()..]; |
312 | x |
313 | } |
314 | None => return Some(Item::Error), // premature end of string |
315 | } |
316 | }; |
317 | } |
318 | |
319 | let spec = next!(); |
320 | let pad_override = match spec { |
321 | '-' => Some(Pad::None), |
322 | '0' => Some(Pad::Zero), |
323 | '_' => Some(Pad::Space), |
324 | _ => None, |
325 | }; |
326 | let is_alternate = spec == '#' ; |
327 | let spec = if pad_override.is_some() || is_alternate { next!() } else { spec }; |
328 | if is_alternate && !HAVE_ALTERNATES.contains(spec) { |
329 | return Some(Item::Error); |
330 | } |
331 | |
332 | macro_rules! recons { |
333 | [$head:expr, $($tail:expr),+ $(,)*] => ({ |
334 | #[cfg(feature = "unstable-locales" )] |
335 | { |
336 | self.recons.clear(); |
337 | $(self.recons.push($tail);)+ |
338 | } |
339 | #[cfg(not(feature = "unstable-locales" ))] |
340 | { |
341 | const RECONS: &'static [Item<'static>] = &[$($tail),+]; |
342 | self.recons = RECONS; |
343 | } |
344 | $head |
345 | }) |
346 | } |
347 | |
348 | macro_rules! recons_from_slice { |
349 | ($slice:expr) => {{ |
350 | #[cfg(feature = "unstable-locales" )] |
351 | { |
352 | self.recons.clear(); |
353 | self.recons.extend_from_slice(&$slice[1..]); |
354 | } |
355 | #[cfg(not(feature = "unstable-locales" ))] |
356 | { |
357 | self.recons = &$slice[1..]; |
358 | } |
359 | $slice[0].clone() |
360 | }}; |
361 | } |
362 | |
363 | let item = match spec { |
364 | 'A' => fix!(LongWeekdayName), |
365 | 'B' => fix!(LongMonthName), |
366 | 'C' => num0!(YearDiv100), |
367 | 'D' => { |
368 | recons![num0!(Month), lit!("/" ), num0!(Day), lit!("/" ), num0!(YearMod100)] |
369 | } |
370 | 'F' => recons![num0!(Year), lit!("-" ), num0!(Month), lit!("-" ), num0!(Day)], |
371 | 'G' => num0!(IsoYear), |
372 | 'H' => num0!(Hour), |
373 | 'I' => num0!(Hour12), |
374 | 'M' => num0!(Minute), |
375 | 'P' => fix!(LowerAmPm), |
376 | 'R' => recons![num0!(Hour), lit!(":" ), num0!(Minute)], |
377 | 'S' => num0!(Second), |
378 | 'T' => recons![num0!(Hour), lit!(":" ), num0!(Minute), lit!(":" ), num0!(Second)], |
379 | 'U' => num0!(WeekFromSun), |
380 | 'V' => num0!(IsoWeek), |
381 | 'W' => num0!(WeekFromMon), |
382 | 'X' => recons_from_slice!(self.t_fmt), |
383 | 'Y' => num0!(Year), |
384 | 'Z' => fix!(TimezoneName), |
385 | 'a' => fix!(ShortWeekdayName), |
386 | 'b' | 'h' => fix!(ShortMonthName), |
387 | 'c' => recons_from_slice!(self.d_t_fmt), |
388 | 'd' => num0!(Day), |
389 | 'e' => nums!(Day), |
390 | 'f' => num0!(Nanosecond), |
391 | 'g' => num0!(IsoYearMod100), |
392 | 'j' => num0!(Ordinal), |
393 | 'k' => nums!(Hour), |
394 | 'l' => nums!(Hour12), |
395 | 'm' => num0!(Month), |
396 | 'n' => sp!(" \n" ), |
397 | 'p' => fix!(UpperAmPm), |
398 | 'r' => recons![ |
399 | num0!(Hour12), |
400 | lit!(":" ), |
401 | num0!(Minute), |
402 | lit!(":" ), |
403 | num0!(Second), |
404 | sp!(" " ), |
405 | fix!(UpperAmPm) |
406 | ], |
407 | 's' => num!(Timestamp), |
408 | 't' => sp!(" \t" ), |
409 | 'u' => num!(WeekdayFromMon), |
410 | 'v' => { |
411 | recons![nums!(Day), lit!("-" ), fix!(ShortMonthName), lit!("-" ), num0!(Year)] |
412 | } |
413 | 'w' => num!(NumDaysFromSun), |
414 | 'x' => recons_from_slice!(self.d_fmt), |
415 | 'y' => num0!(YearMod100), |
416 | 'z' => { |
417 | if is_alternate { |
418 | internal_fix!(TimezoneOffsetPermissive) |
419 | } else { |
420 | fix!(TimezoneOffset) |
421 | } |
422 | } |
423 | '+' => fix!(RFC3339), |
424 | ':' => { |
425 | if self.remainder.starts_with("::z" ) { |
426 | self.remainder = &self.remainder[3..]; |
427 | fix!(TimezoneOffsetTripleColon) |
428 | } else if self.remainder.starts_with(":z" ) { |
429 | self.remainder = &self.remainder[2..]; |
430 | fix!(TimezoneOffsetDoubleColon) |
431 | } else if self.remainder.starts_with('z' ) { |
432 | self.remainder = &self.remainder[1..]; |
433 | fix!(TimezoneOffsetColon) |
434 | } else { |
435 | Item::Error |
436 | } |
437 | } |
438 | '.' => match next!() { |
439 | '3' => match next!() { |
440 | 'f' => fix!(Nanosecond3), |
441 | _ => Item::Error, |
442 | }, |
443 | '6' => match next!() { |
444 | 'f' => fix!(Nanosecond6), |
445 | _ => Item::Error, |
446 | }, |
447 | '9' => match next!() { |
448 | 'f' => fix!(Nanosecond9), |
449 | _ => Item::Error, |
450 | }, |
451 | 'f' => fix!(Nanosecond), |
452 | _ => Item::Error, |
453 | }, |
454 | '3' => match next!() { |
455 | 'f' => internal_fix!(Nanosecond3NoDot), |
456 | _ => Item::Error, |
457 | }, |
458 | '6' => match next!() { |
459 | 'f' => internal_fix!(Nanosecond6NoDot), |
460 | _ => Item::Error, |
461 | }, |
462 | '9' => match next!() { |
463 | 'f' => internal_fix!(Nanosecond9NoDot), |
464 | _ => Item::Error, |
465 | }, |
466 | '%' => lit!("%" ), |
467 | _ => Item::Error, // no such specifier |
468 | }; |
469 | |
470 | // adjust `item` if we have any padding modifier |
471 | if let Some(new_pad) = pad_override { |
472 | match item { |
473 | Item::Numeric(ref kind, _pad) if self.recons.is_empty() => { |
474 | Some(Item::Numeric(kind.clone(), new_pad)) |
475 | } |
476 | _ => Some(Item::Error), // no reconstructed or non-numeric item allowed |
477 | } |
478 | } else { |
479 | Some(item) |
480 | } |
481 | } |
482 | |
483 | // the next item is space |
484 | Some(c) if c.is_whitespace() => { |
485 | // `%` is not a whitespace, so `c != '%'` is redundant |
486 | let nextspec = self |
487 | .remainder |
488 | .find(|c: char| !c.is_whitespace()) |
489 | .unwrap_or(self.remainder.len()); |
490 | assert!(nextspec > 0); |
491 | let item = sp!(&self.remainder[..nextspec]); |
492 | self.remainder = &self.remainder[nextspec..]; |
493 | Some(item) |
494 | } |
495 | |
496 | // the next item is literal |
497 | _ => { |
498 | let nextspec = self |
499 | .remainder |
500 | .find(|c: char| c.is_whitespace() || c == '%' ) |
501 | .unwrap_or(self.remainder.len()); |
502 | assert!(nextspec > 0); |
503 | let item = lit!(&self.remainder[..nextspec]); |
504 | self.remainder = &self.remainder[nextspec..]; |
505 | Some(item) |
506 | } |
507 | } |
508 | } |
509 | } |
510 | |
511 | #[cfg (test)] |
512 | #[test ] |
513 | fn test_strftime_items() { |
514 | fn parse_and_collect(s: &str) -> Vec<Item<'_>> { |
515 | // map any error into `[Item::Error]`. useful for easy testing. |
516 | let items = StrftimeItems::new(s); |
517 | let items = items.map(|spec| if spec == Item::Error { None } else { Some(spec) }); |
518 | items.collect::<Option<Vec<_>>>().unwrap_or_else(|| vec![Item::Error]) |
519 | } |
520 | |
521 | assert_eq!(parse_and_collect("" ), []); |
522 | assert_eq!(parse_and_collect(" \t\n\r " ), [sp!(" \t\n\r " )]); |
523 | assert_eq!(parse_and_collect("hello?" ), [lit!("hello?" )]); |
524 | assert_eq!( |
525 | parse_and_collect("a b \t\nc" ), |
526 | [lit!("a" ), sp!(" " ), lit!("b" ), sp!(" \t\n" ), lit!("c" )] |
527 | ); |
528 | assert_eq!(parse_and_collect("100%%" ), [lit!("100" ), lit!("%" )]); |
529 | assert_eq!(parse_and_collect("100%% ok" ), [lit!("100" ), lit!("%" ), sp!(" " ), lit!("ok" )]); |
530 | assert_eq!(parse_and_collect("%%PDF-1.0" ), [lit!("%" ), lit!("PDF-1.0" )]); |
531 | assert_eq!( |
532 | parse_and_collect("%Y-%m-%d" ), |
533 | [num0!(Year), lit!("-" ), num0!(Month), lit!("-" ), num0!(Day)] |
534 | ); |
535 | assert_eq!(parse_and_collect("[%F]" ), parse_and_collect("[%Y-%m-%d]" )); |
536 | assert_eq!(parse_and_collect("%m %d" ), [num0!(Month), sp!(" " ), num0!(Day)]); |
537 | assert_eq!(parse_and_collect("%" ), [Item::Error]); |
538 | assert_eq!(parse_and_collect("%%" ), [lit!("%" )]); |
539 | assert_eq!(parse_and_collect("%%%" ), [Item::Error]); |
540 | assert_eq!(parse_and_collect("%%%%" ), [lit!("%" ), lit!("%" )]); |
541 | assert_eq!(parse_and_collect("foo%?" ), [Item::Error]); |
542 | assert_eq!(parse_and_collect("bar%42" ), [Item::Error]); |
543 | assert_eq!(parse_and_collect("quux% +" ), [Item::Error]); |
544 | assert_eq!(parse_and_collect("%.Z" ), [Item::Error]); |
545 | assert_eq!(parse_and_collect("%:Z" ), [Item::Error]); |
546 | assert_eq!(parse_and_collect("%-Z" ), [Item::Error]); |
547 | assert_eq!(parse_and_collect("%0Z" ), [Item::Error]); |
548 | assert_eq!(parse_and_collect("%_Z" ), [Item::Error]); |
549 | assert_eq!(parse_and_collect("%.j" ), [Item::Error]); |
550 | assert_eq!(parse_and_collect("%:j" ), [Item::Error]); |
551 | assert_eq!(parse_and_collect("%-j" ), [num!(Ordinal)]); |
552 | assert_eq!(parse_and_collect("%0j" ), [num0!(Ordinal)]); |
553 | assert_eq!(parse_and_collect("%_j" ), [nums!(Ordinal)]); |
554 | assert_eq!(parse_and_collect("%.e" ), [Item::Error]); |
555 | assert_eq!(parse_and_collect("%:e" ), [Item::Error]); |
556 | assert_eq!(parse_and_collect("%-e" ), [num!(Day)]); |
557 | assert_eq!(parse_and_collect("%0e" ), [num0!(Day)]); |
558 | assert_eq!(parse_and_collect("%_e" ), [nums!(Day)]); |
559 | assert_eq!(parse_and_collect("%z" ), [fix!(TimezoneOffset)]); |
560 | assert_eq!(parse_and_collect("%#z" ), [internal_fix!(TimezoneOffsetPermissive)]); |
561 | assert_eq!(parse_and_collect("%#m" ), [Item::Error]); |
562 | } |
563 | |
564 | #[cfg (test)] |
565 | #[test ] |
566 | fn test_strftime_docs() { |
567 | use crate::NaiveDate; |
568 | use crate::{DateTime, FixedOffset, TimeZone, Timelike, Utc}; |
569 | |
570 | let dt = FixedOffset::east_opt(34200) |
571 | .unwrap() |
572 | .from_local_datetime( |
573 | &NaiveDate::from_ymd_opt(2001, 7, 8) |
574 | .unwrap() |
575 | .and_hms_nano_opt(0, 34, 59, 1_026_490_708) |
576 | .unwrap(), |
577 | ) |
578 | .unwrap(); |
579 | |
580 | // date specifiers |
581 | assert_eq!(dt.format("%Y" ).to_string(), "2001" ); |
582 | assert_eq!(dt.format("%C" ).to_string(), "20" ); |
583 | assert_eq!(dt.format("%y" ).to_string(), "01" ); |
584 | assert_eq!(dt.format("%m" ).to_string(), "07" ); |
585 | assert_eq!(dt.format("%b" ).to_string(), "Jul" ); |
586 | assert_eq!(dt.format("%B" ).to_string(), "July" ); |
587 | assert_eq!(dt.format("%h" ).to_string(), "Jul" ); |
588 | assert_eq!(dt.format("%d" ).to_string(), "08" ); |
589 | assert_eq!(dt.format("%e" ).to_string(), " 8" ); |
590 | assert_eq!(dt.format("%e" ).to_string(), dt.format("%_d" ).to_string()); |
591 | assert_eq!(dt.format("%a" ).to_string(), "Sun" ); |
592 | assert_eq!(dt.format("%A" ).to_string(), "Sunday" ); |
593 | assert_eq!(dt.format("%w" ).to_string(), "0" ); |
594 | assert_eq!(dt.format("%u" ).to_string(), "7" ); |
595 | assert_eq!(dt.format("%U" ).to_string(), "27" ); |
596 | assert_eq!(dt.format("%W" ).to_string(), "27" ); |
597 | assert_eq!(dt.format("%G" ).to_string(), "2001" ); |
598 | assert_eq!(dt.format("%g" ).to_string(), "01" ); |
599 | assert_eq!(dt.format("%V" ).to_string(), "27" ); |
600 | assert_eq!(dt.format("%j" ).to_string(), "189" ); |
601 | assert_eq!(dt.format("%D" ).to_string(), "07/08/01" ); |
602 | assert_eq!(dt.format("%x" ).to_string(), "07/08/01" ); |
603 | assert_eq!(dt.format("%F" ).to_string(), "2001-07-08" ); |
604 | assert_eq!(dt.format("%v" ).to_string(), " 8-Jul-2001" ); |
605 | |
606 | // time specifiers |
607 | assert_eq!(dt.format("%H" ).to_string(), "00" ); |
608 | assert_eq!(dt.format("%k" ).to_string(), " 0" ); |
609 | assert_eq!(dt.format("%k" ).to_string(), dt.format("%_H" ).to_string()); |
610 | assert_eq!(dt.format("%I" ).to_string(), "12" ); |
611 | assert_eq!(dt.format("%l" ).to_string(), "12" ); |
612 | assert_eq!(dt.format("%l" ).to_string(), dt.format("%_I" ).to_string()); |
613 | assert_eq!(dt.format("%P" ).to_string(), "am" ); |
614 | assert_eq!(dt.format("%p" ).to_string(), "AM" ); |
615 | assert_eq!(dt.format("%M" ).to_string(), "34" ); |
616 | assert_eq!(dt.format("%S" ).to_string(), "60" ); |
617 | assert_eq!(dt.format("%f" ).to_string(), "026490708" ); |
618 | assert_eq!(dt.format("%.f" ).to_string(), ".026490708" ); |
619 | assert_eq!(dt.with_nanosecond(1_026_490_000).unwrap().format("%.f" ).to_string(), ".026490" ); |
620 | assert_eq!(dt.format("%.3f" ).to_string(), ".026" ); |
621 | assert_eq!(dt.format("%.6f" ).to_string(), ".026490" ); |
622 | assert_eq!(dt.format("%.9f" ).to_string(), ".026490708" ); |
623 | assert_eq!(dt.format("%3f" ).to_string(), "026" ); |
624 | assert_eq!(dt.format("%6f" ).to_string(), "026490" ); |
625 | assert_eq!(dt.format("%9f" ).to_string(), "026490708" ); |
626 | assert_eq!(dt.format("%R" ).to_string(), "00:34" ); |
627 | assert_eq!(dt.format("%T" ).to_string(), "00:34:60" ); |
628 | assert_eq!(dt.format("%X" ).to_string(), "00:34:60" ); |
629 | assert_eq!(dt.format("%r" ).to_string(), "12:34:60 AM" ); |
630 | |
631 | // time zone specifiers |
632 | //assert_eq!(dt.format("%Z").to_string(), "ACST"); |
633 | assert_eq!(dt.format("%z" ).to_string(), "+0930" ); |
634 | assert_eq!(dt.format("%:z" ).to_string(), "+09:30" ); |
635 | assert_eq!(dt.format("%::z" ).to_string(), "+09:30:00" ); |
636 | assert_eq!(dt.format("%:::z" ).to_string(), "+09" ); |
637 | |
638 | // date & time specifiers |
639 | assert_eq!(dt.format("%c" ).to_string(), "Sun Jul 8 00:34:60 2001" ); |
640 | assert_eq!(dt.format("%+" ).to_string(), "2001-07-08T00:34:60.026490708+09:30" ); |
641 | |
642 | assert_eq!( |
643 | dt.with_timezone(&Utc).format("%+" ).to_string(), |
644 | "2001-07-07T15:04:60.026490708+00:00" |
645 | ); |
646 | assert_eq!( |
647 | dt.with_timezone(&Utc), |
648 | DateTime::parse_from_str("2001-07-07T15:04:60.026490708Z" , "%+" ).unwrap() |
649 | ); |
650 | assert_eq!( |
651 | dt.with_timezone(&Utc), |
652 | DateTime::parse_from_str("2001-07-07T15:04:60.026490708UTC" , "%+" ).unwrap() |
653 | ); |
654 | assert_eq!( |
655 | dt.with_timezone(&Utc), |
656 | DateTime::parse_from_str("2001-07-07t15:04:60.026490708utc" , "%+" ).unwrap() |
657 | ); |
658 | |
659 | assert_eq!( |
660 | dt.with_nanosecond(1_026_490_000).unwrap().format("%+" ).to_string(), |
661 | "2001-07-08T00:34:60.026490+09:30" |
662 | ); |
663 | assert_eq!(dt.format("%s" ).to_string(), "994518299" ); |
664 | |
665 | // special specifiers |
666 | assert_eq!(dt.format("%t" ).to_string(), " \t" ); |
667 | assert_eq!(dt.format("%n" ).to_string(), " \n" ); |
668 | assert_eq!(dt.format("%%" ).to_string(), "%" ); |
669 | } |
670 | |
671 | #[cfg (feature = "unstable-locales" )] |
672 | #[test ] |
673 | fn test_strftime_docs_localized() { |
674 | use crate::{FixedOffset, NaiveDate, TimeZone}; |
675 | |
676 | let dt = FixedOffset::east_opt(34200).unwrap().ymd_opt(2001, 7, 8).unwrap().and_hms_nano( |
677 | 0, |
678 | 34, |
679 | 59, |
680 | 1_026_490_708, |
681 | ); |
682 | |
683 | // date specifiers |
684 | assert_eq!(dt.format_localized("%b" , Locale::fr_BE).to_string(), "jui" ); |
685 | assert_eq!(dt.format_localized("%B" , Locale::fr_BE).to_string(), "juillet" ); |
686 | assert_eq!(dt.format_localized("%h" , Locale::fr_BE).to_string(), "jui" ); |
687 | assert_eq!(dt.format_localized("%a" , Locale::fr_BE).to_string(), "dim" ); |
688 | assert_eq!(dt.format_localized("%A" , Locale::fr_BE).to_string(), "dimanche" ); |
689 | assert_eq!(dt.format_localized("%D" , Locale::fr_BE).to_string(), "07/08/01" ); |
690 | assert_eq!(dt.format_localized("%x" , Locale::fr_BE).to_string(), "08/07/01" ); |
691 | assert_eq!(dt.format_localized("%F" , Locale::fr_BE).to_string(), "2001-07-08" ); |
692 | assert_eq!(dt.format_localized("%v" , Locale::fr_BE).to_string(), " 8-jui-2001" ); |
693 | |
694 | // time specifiers |
695 | assert_eq!(dt.format_localized("%P" , Locale::fr_BE).to_string(), "" ); |
696 | assert_eq!(dt.format_localized("%p" , Locale::fr_BE).to_string(), "" ); |
697 | assert_eq!(dt.format_localized("%R" , Locale::fr_BE).to_string(), "00:34" ); |
698 | assert_eq!(dt.format_localized("%T" , Locale::fr_BE).to_string(), "00:34:60" ); |
699 | assert_eq!(dt.format_localized("%X" , Locale::fr_BE).to_string(), "00:34:60" ); |
700 | assert_eq!(dt.format_localized("%r" , Locale::fr_BE).to_string(), "12:34:60 " ); |
701 | |
702 | // date & time specifiers |
703 | assert_eq!( |
704 | dt.format_localized("%c" , Locale::fr_BE).to_string(), |
705 | "dim 08 jui 2001 00:34:60 +09:30" |
706 | ); |
707 | |
708 | let nd = NaiveDate::from_ymd_opt(2001, 7, 8).unwrap(); |
709 | |
710 | // date specifiers |
711 | assert_eq!(nd.format_localized("%b" , Locale::de_DE).to_string(), "Jul" ); |
712 | assert_eq!(nd.format_localized("%B" , Locale::de_DE).to_string(), "Juli" ); |
713 | assert_eq!(nd.format_localized("%h" , Locale::de_DE).to_string(), "Jul" ); |
714 | assert_eq!(nd.format_localized("%a" , Locale::de_DE).to_string(), "So" ); |
715 | assert_eq!(nd.format_localized("%A" , Locale::de_DE).to_string(), "Sonntag" ); |
716 | assert_eq!(nd.format_localized("%D" , Locale::de_DE).to_string(), "07/08/01" ); |
717 | assert_eq!(nd.format_localized("%x" , Locale::de_DE).to_string(), "08.07.2001" ); |
718 | assert_eq!(nd.format_localized("%F" , Locale::de_DE).to_string(), "2001-07-08" ); |
719 | assert_eq!(nd.format_localized("%v" , Locale::de_DE).to_string(), " 8-Jul-2001" ); |
720 | } |
721 | |