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
9The 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
89It is possible to override the default padding behavior of numeric specifiers `%?`.
90This is not allowed for other specifiers and will result in the `BAD_FORMAT` error.
91
92Modifier | 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
98Notes:
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")]
184extern crate alloc;
185
186#[cfg(feature = "unstable-locales")]
187use alloc::vec::Vec;
188
189#[cfg(feature = "unstable-locales")]
190use super::{locales, Locale};
191use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad};
192
193#[cfg(feature = "unstable-locales")]
194type Fmt<'a> = Vec<Item<'a>>;
195#[cfg(not(feature = "unstable-locales"))]
196type Fmt<'a> = &'static [Item<'static>];
197
198static D_FMT: &[Item<'static>] =
199 &[num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)];
200static 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];
215static 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)]
219pub 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
234impl<'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
278const HAVE_ALTERNATES: &str = "z";
279
280impl<'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]
513fn 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]
566fn 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]
673fn 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