chrono/format/
strftime.rs

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`  | `26490000`    | Number of nanoseconds since last whole second. [^7]                   |
57| `%.f` | `.026490`| Decimal fraction of a second. Consumes the leading dot. [^7]               |
58| `%.3f`| `.026`        | Decimal fraction of a second with a fixed length of 3.                |
59| `%.6f`| `.026490`     | Decimal fraction of a second with a fixed length of 6.                |
60| `%.9f`| `.026490000`  | Decimal fraction of a second with a fixed length of 9.                |
61| `%3f` | `026`         | Decimal fraction of a second like `%.3f` but without the leading dot. |
62| `%6f` | `026490`      | Decimal fraction of a second like `%.6f` but without the leading dot. |
63| `%9f` | `026490000`   | Decimal fraction of a second like `%.9f` but without the leading dot. |
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` | Locale's 12 hour clock time. (e.g., 11:11:04 PM). Falls back to `%X` if the locale does not have a 12 hour clock format. |
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`:
136   <br>
137   `%f` and `%.f` are notably different formatting specifiers.<br>
138   `%f` counts the number of nanoseconds since the last whole second, while `%.f` is a fraction of a
139   second.<br>
140   Example: 7ΞΌs is formatted as `7000` with `%f`, and formatted as `.000007` with `%.f`.
141
142[^8]: `%Z`:
143   Since `chrono` is not aware of timezones beyond their offsets, this specifier
144   **only prints the offset** when used for formatting. The timezone abbreviation
145   will NOT be printed. See [this issue](https://github.com/chronotope/chrono/issues/960)
146   for more information.
147   <br>
148   <br>
149   Offset will not be populated from the parsed data, nor will it be validated.
150   Timezone is completely ignored. Similar to the glibc `strptime` treatment of
151   this format code.
152   <br>
153   <br>
154   It is not possible to reliably convert from an abbreviation to an offset,
155   for example CDT can mean either Central Daylight Time (North America) or
156   China Daylight Time.
157*/
158
159use super::{fixed, internal_fixed, num, num0, nums};
160#[cfg(feature = "unstable-locales")]
161use super::{locales, Locale};
162use super::{Fixed, InternalInternal, Item, Numeric, Pad};
163
164/// Parsing iterator for `strftime`-like format strings.
165#[derive(Clone, Debug)]
166pub struct StrftimeItems<'a> {
167    /// Remaining portion of the string.
168    remainder: &'a str,
169    /// If the current specifier is composed of multiple formatting items (e.g. `%+`),
170    /// `queue` stores a slice of `Item`s that have to be returned one by one.
171    queue: &'static [Item<'static>],
172    #[cfg(feature = "unstable-locales")]
173    locale_str: &'a str,
174    #[cfg(feature = "unstable-locales")]
175    locale: Option<Locale>,
176}
177
178impl<'a> StrftimeItems<'a> {
179    /// Creates a new parsing iterator from the `strftime`-like format string.
180    #[must_use]
181    pub const fn new(s: &'a str) -> StrftimeItems<'a> {
182        #[cfg(not(feature = "unstable-locales"))]
183        {
184            StrftimeItems { remainder: s, queue: &[] }
185        }
186        #[cfg(feature = "unstable-locales")]
187        {
188            StrftimeItems { remainder: s, queue: &[], locale_str: "", locale: None }
189        }
190    }
191
192    /// Creates a new parsing iterator from the `strftime`-like format string.
193    #[cfg(feature = "unstable-locales")]
194    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))]
195    #[must_use]
196    pub const fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> {
197        StrftimeItems { remainder: s, queue: &[], locale_str: "", locale: Some(locale) }
198    }
199}
200
201const HAVE_ALTERNATES: &str = "z";
202
203impl<'a> Iterator for StrftimeItems<'a> {
204    type Item = Item<'a>;
205
206    fn next(&mut self) -> Option<Item<'a>> {
207        // We have items queued to return from a specifier composed of multiple formatting items.
208        if let Some((item, remainder)) = self.queue.split_first() {
209            self.queue = remainder;
210            return Some(item.clone());
211        }
212
213        // We are in the middle of parsing the localized formatting string of a specifier.
214        #[cfg(feature = "unstable-locales")]
215        if !self.locale_str.is_empty() {
216            let (remainder, item) = self.parse_next_item(self.locale_str)?;
217            self.locale_str = remainder;
218            return Some(item);
219        }
220
221        // Normal: we are parsing the formatting string.
222        let (remainder, item) = self.parse_next_item(self.remainder)?;
223        self.remainder = remainder;
224        Some(item)
225    }
226}
227
228impl<'a> StrftimeItems<'a> {
229    fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> {
230        use InternalInternal::*;
231        use Item::{Literal, Space};
232        use Numeric::*;
233
234        static D_FMT: &[Item<'static>] =
235            &[num0(Month), Literal("/"), num0(Day), Literal("/"), num0(YearMod100)];
236        static D_T_FMT: &[Item<'static>] = &[
237            fixed(Fixed::ShortWeekdayName),
238            Space(" "),
239            fixed(Fixed::ShortMonthName),
240            Space(" "),
241            nums(Day),
242            Space(" "),
243            num0(Hour),
244            Literal(":"),
245            num0(Minute),
246            Literal(":"),
247            num0(Second),
248            Space(" "),
249            num0(Year),
250        ];
251        static T_FMT: &[Item<'static>] =
252            &[num0(Hour), Literal(":"), num0(Minute), Literal(":"), num0(Second)];
253        static T_FMT_AMPM: &[Item<'static>] = &[
254            num0(Hour12),
255            Literal(":"),
256            num0(Minute),
257            Literal(":"),
258            num0(Second),
259            Space(" "),
260            fixed(Fixed::UpperAmPm),
261        ];
262
263        match remainder.chars().next() {
264            // we are done
265            None => None,
266
267            // the next item is a specifier
268            Some('%') => {
269                remainder = &remainder[1..];
270
271                macro_rules! next {
272                    () => {
273                        match remainder.chars().next() {
274                            Some(x) => {
275                                remainder = &remainder[x.len_utf8()..];
276                                x
277                            }
278                            None => return Some((remainder, Item::Error)), // premature end of string
279                        }
280                    };
281                }
282
283                let spec = next!();
284                let pad_override = match spec {
285                    '-' => Some(Pad::None),
286                    '0' => Some(Pad::Zero),
287                    '_' => Some(Pad::Space),
288                    _ => None,
289                };
290                let is_alternate = spec == '#';
291                let spec = if pad_override.is_some() || is_alternate { next!() } else { spec };
292                if is_alternate && !HAVE_ALTERNATES.contains(spec) {
293                    return Some((remainder, Item::Error));
294                }
295
296                macro_rules! queue {
297                    [$head:expr, $($tail:expr),+ $(,)*] => ({
298                        const QUEUE: &'static [Item<'static>] = &[$($tail),+];
299                        self.queue = QUEUE;
300                        $head
301                    })
302                }
303                #[cfg(not(feature = "unstable-locales"))]
304                macro_rules! queue_from_slice {
305                    ($slice:expr) => {{
306                        self.queue = &$slice[1..];
307                        $slice[0].clone()
308                    }};
309                }
310
311                let item = match spec {
312                    'A' => fixed(Fixed::LongWeekdayName),
313                    'B' => fixed(Fixed::LongMonthName),
314                    'C' => num0(YearDiv100),
315                    'D' => {
316                        queue![num0(Month), Literal("/"), num0(Day), Literal("/"), num0(YearMod100)]
317                    }
318                    'F' => queue![num0(Year), Literal("-"), num0(Month), Literal("-"), num0(Day)],
319                    'G' => num0(IsoYear),
320                    'H' => num0(Hour),
321                    'I' => num0(Hour12),
322                    'M' => num0(Minute),
323                    'P' => fixed(Fixed::LowerAmPm),
324                    'R' => queue![num0(Hour), Literal(":"), num0(Minute)],
325                    'S' => num0(Second),
326                    'T' => {
327                        queue![num0(Hour), Literal(":"), num0(Minute), Literal(":"), num0(Second)]
328                    }
329                    'U' => num0(WeekFromSun),
330                    'V' => num0(IsoWeek),
331                    'W' => num0(WeekFromMon),
332                    #[cfg(not(feature = "unstable-locales"))]
333                    'X' => queue_from_slice!(T_FMT),
334                    #[cfg(feature = "unstable-locales")]
335                    'X' => self.switch_to_locale_str(locales::t_fmt, T_FMT),
336                    'Y' => num0(Year),
337                    'Z' => fixed(Fixed::TimezoneName),
338                    'a' => fixed(Fixed::ShortWeekdayName),
339                    'b' | 'h' => fixed(Fixed::ShortMonthName),
340                    #[cfg(not(feature = "unstable-locales"))]
341                    'c' => queue_from_slice!(D_T_FMT),
342                    #[cfg(feature = "unstable-locales")]
343                    'c' => self.switch_to_locale_str(locales::d_t_fmt, D_T_FMT),
344                    'd' => num0(Day),
345                    'e' => nums(Day),
346                    'f' => num0(Nanosecond),
347                    'g' => num0(IsoYearMod100),
348                    'j' => num0(Ordinal),
349                    'k' => nums(Hour),
350                    'l' => nums(Hour12),
351                    'm' => num0(Month),
352                    'n' => Space("\n"),
353                    'p' => fixed(Fixed::UpperAmPm),
354                    #[cfg(not(feature = "unstable-locales"))]
355                    'r' => queue_from_slice!(T_FMT_AMPM),
356                    #[cfg(feature = "unstable-locales")]
357                    'r' => {
358                        if self.locale.is_some()
359                            && locales::t_fmt_ampm(self.locale.unwrap()).is_empty()
360                        {
361                            // 12-hour clock not supported by this locale. Switch to 24-hour format.
362                            self.switch_to_locale_str(locales::t_fmt, T_FMT)
363                        } else {
364                            self.switch_to_locale_str(locales::t_fmt_ampm, T_FMT_AMPM)
365                        }
366                    }
367                    's' => num(Timestamp),
368                    't' => Space("\t"),
369                    'u' => num(WeekdayFromMon),
370                    'v' => {
371                        queue![
372                            nums(Day),
373                            Literal("-"),
374                            fixed(Fixed::ShortMonthName),
375                            Literal("-"),
376                            num0(Year)
377                        ]
378                    }
379                    'w' => num(NumDaysFromSun),
380                    #[cfg(not(feature = "unstable-locales"))]
381                    'x' => queue_from_slice!(D_FMT),
382                    #[cfg(feature = "unstable-locales")]
383                    'x' => self.switch_to_locale_str(locales::d_fmt, D_FMT),
384                    'y' => num0(YearMod100),
385                    'z' => {
386                        if is_alternate {
387                            internal_fixed(TimezoneOffsetPermissive)
388                        } else {
389                            fixed(Fixed::TimezoneOffset)
390                        }
391                    }
392                    '+' => fixed(Fixed::RFC3339),
393                    ':' => {
394                        if remainder.starts_with("::z") {
395                            remainder = &remainder[3..];
396                            fixed(Fixed::TimezoneOffsetTripleColon)
397                        } else if remainder.starts_with(":z") {
398                            remainder = &remainder[2..];
399                            fixed(Fixed::TimezoneOffsetDoubleColon)
400                        } else if remainder.starts_with('z') {
401                            remainder = &remainder[1..];
402                            fixed(Fixed::TimezoneOffsetColon)
403                        } else {
404                            Item::Error
405                        }
406                    }
407                    '.' => match next!() {
408                        '3' => match next!() {
409                            'f' => fixed(Fixed::Nanosecond3),
410                            _ => Item::Error,
411                        },
412                        '6' => match next!() {
413                            'f' => fixed(Fixed::Nanosecond6),
414                            _ => Item::Error,
415                        },
416                        '9' => match next!() {
417                            'f' => fixed(Fixed::Nanosecond9),
418                            _ => Item::Error,
419                        },
420                        'f' => fixed(Fixed::Nanosecond),
421                        _ => Item::Error,
422                    },
423                    '3' => match next!() {
424                        'f' => internal_fixed(Nanosecond3NoDot),
425                        _ => Item::Error,
426                    },
427                    '6' => match next!() {
428                        'f' => internal_fixed(Nanosecond6NoDot),
429                        _ => Item::Error,
430                    },
431                    '9' => match next!() {
432                        'f' => internal_fixed(Nanosecond9NoDot),
433                        _ => Item::Error,
434                    },
435                    '%' => Literal("%"),
436                    _ => Item::Error, // no such specifier
437                };
438
439                // Adjust `item` if we have any padding modifier.
440                // Not allowed on non-numeric items or on specifiers composed out of multiple
441                // formatting items.
442                if let Some(new_pad) = pad_override {
443                    match item {
444                        Item::Numeric(ref kind, _pad) if self.queue.is_empty() => {
445                            Some((remainder, Item::Numeric(kind.clone(), new_pad)))
446                        }
447                        _ => Some((remainder, Item::Error)),
448                    }
449                } else {
450                    Some((remainder, item))
451                }
452            }
453
454            // the next item is space
455            Some(c) if c.is_whitespace() => {
456                // `%` is not a whitespace, so `c != '%'` is redundant
457                let nextspec =
458                    remainder.find(|c: char| !c.is_whitespace()).unwrap_or(remainder.len());
459                assert!(nextspec > 0);
460                let item = Space(&remainder[..nextspec]);
461                remainder = &remainder[nextspec..];
462                Some((remainder, item))
463            }
464
465            // the next item is literal
466            _ => {
467                let nextspec = remainder
468                    .find(|c: char| c.is_whitespace() || c == '%')
469                    .unwrap_or(remainder.len());
470                assert!(nextspec > 0);
471                let item = Literal(&remainder[..nextspec]);
472                remainder = &remainder[nextspec..];
473                Some((remainder, item))
474            }
475        }
476    }
477
478    #[cfg(feature = "unstable-locales")]
479    fn switch_to_locale_str(
480        &mut self,
481        localized_fmt_str: impl Fn(Locale) -> &'static str,
482        fallback: &'static [Item<'static>],
483    ) -> Item<'a> {
484        if let Some(locale) = self.locale {
485            assert!(self.locale_str.is_empty());
486            let (fmt_str, item) = self.parse_next_item(localized_fmt_str(locale)).unwrap();
487            self.locale_str = fmt_str;
488            item
489        } else {
490            self.queue = &fallback[1..];
491            fallback[0].clone()
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::StrftimeItems;
499    use crate::format::Item::{self, Literal, Space};
500    #[cfg(feature = "unstable-locales")]
501    use crate::format::Locale;
502    use crate::format::{fixed, internal_fixed, num, num0, nums};
503    use crate::format::{Fixed, InternalInternal, Numeric::*};
504    #[cfg(any(feature = "alloc", feature = "std"))]
505    use crate::{DateTime, FixedOffset, NaiveDate, TimeZone, Timelike, Utc};
506
507    #[test]
508    fn test_strftime_items() {
509        fn parse_and_collect(s: &str) -> Vec<Item<'_>> {
510            // map any error into `[Item::Error]`. useful for easy testing.
511            eprintln!("test_strftime_items: parse_and_collect({:?})", s);
512            let items = StrftimeItems::new(s);
513            let items = items.map(|spec| if spec == Item::Error { None } else { Some(spec) });
514            items.collect::<Option<Vec<_>>>().unwrap_or_else(|| vec![Item::Error])
515        }
516
517        assert_eq!(parse_and_collect(""), []);
518        assert_eq!(parse_and_collect(" "), [Space(" ")]);
519        assert_eq!(parse_and_collect("  "), [Space("  ")]);
520        // ne!
521        assert_ne!(parse_and_collect("  "), [Space(" "), Space(" ")]);
522        // eq!
523        assert_eq!(parse_and_collect("  "), [Space("  ")]);
524        assert_eq!(parse_and_collect("a"), [Literal("a")]);
525        assert_eq!(parse_and_collect("ab"), [Literal("ab")]);
526        assert_eq!(parse_and_collect("😽"), [Literal("😽")]);
527        assert_eq!(parse_and_collect("a😽"), [Literal("a😽")]);
528        assert_eq!(parse_and_collect("😽a"), [Literal("😽a")]);
529        assert_eq!(parse_and_collect(" 😽"), [Space(" "), Literal("😽")]);
530        assert_eq!(parse_and_collect("😽 "), [Literal("😽"), Space(" ")]);
531        // ne!
532        assert_ne!(parse_and_collect("😽😽"), [Literal("😽")]);
533        assert_ne!(parse_and_collect("😽"), [Literal("😽😽")]);
534        assert_ne!(parse_and_collect("😽😽"), [Literal("😽😽"), Literal("😽")]);
535        // eq!
536        assert_eq!(parse_and_collect("😽😽"), [Literal("😽😽")]);
537        assert_eq!(parse_and_collect(" \t\n\r "), [Space(" \t\n\r ")]);
538        assert_eq!(parse_and_collect("hello?"), [Literal("hello?")]);
539        assert_eq!(
540            parse_and_collect("a  b\t\nc"),
541            [Literal("a"), Space("  "), Literal("b"), Space("\t\n"), Literal("c")]
542        );
543        assert_eq!(parse_and_collect("100%%"), [Literal("100"), Literal("%")]);
544        assert_eq!(
545            parse_and_collect("100%% ok"),
546            [Literal("100"), Literal("%"), Space(" "), Literal("ok")]
547        );
548        assert_eq!(parse_and_collect("%%PDF-1.0"), [Literal("%"), Literal("PDF-1.0")]);
549        assert_eq!(
550            parse_and_collect("%Y-%m-%d"),
551            [num0(Year), Literal("-"), num0(Month), Literal("-"), num0(Day)]
552        );
553        assert_eq!(parse_and_collect("😽   "), [Literal("😽"), Space("   ")]);
554        assert_eq!(parse_and_collect("😽😽"), [Literal("😽😽")]);
555        assert_eq!(parse_and_collect("😽😽😽"), [Literal("😽😽😽")]);
556        assert_eq!(parse_and_collect("😽😽 😽"), [Literal("😽😽"), Space(" "), Literal("😽")]);
557        assert_eq!(parse_and_collect("😽😽a 😽"), [Literal("😽😽a"), Space(" "), Literal("😽")]);
558        assert_eq!(parse_and_collect("😽😽a b😽"), [Literal("😽😽a"), Space(" "), Literal("b😽")]);
559        assert_eq!(
560            parse_and_collect("😽😽a b😽c"),
561            [Literal("😽😽a"), Space(" "), Literal("b😽c")]
562        );
563        assert_eq!(parse_and_collect("😽😽   "), [Literal("😽😽"), Space("   ")]);
564        assert_eq!(parse_and_collect("😽😽   😽"), [Literal("😽😽"), Space("   "), Literal("😽")]);
565        assert_eq!(parse_and_collect("   😽"), [Space("   "), Literal("😽")]);
566        assert_eq!(parse_and_collect("   😽 "), [Space("   "), Literal("😽"), Space(" ")]);
567        assert_eq!(
568            parse_and_collect("   😽 😽"),
569            [Space("   "), Literal("😽"), Space(" "), Literal("😽")]
570        );
571        assert_eq!(
572            parse_and_collect("   😽 😽 "),
573            [Space("   "), Literal("😽"), Space(" "), Literal("😽"), Space(" ")]
574        );
575        assert_eq!(
576            parse_and_collect("   😽  😽 "),
577            [Space("   "), Literal("😽"), Space("  "), Literal("😽"), Space(" ")]
578        );
579        assert_eq!(
580            parse_and_collect("   😽  😽😽 "),
581            [Space("   "), Literal("😽"), Space("  "), Literal("😽😽"), Space(" ")]
582        );
583        assert_eq!(parse_and_collect("   😽😽"), [Space("   "), Literal("😽😽")]);
584        assert_eq!(parse_and_collect("   😽😽 "), [Space("   "), Literal("😽😽"), Space(" ")]);
585        assert_eq!(
586            parse_and_collect("   😽😽    "),
587            [Space("   "), Literal("😽😽"), Space("    ")]
588        );
589        assert_eq!(
590            parse_and_collect("   😽😽    "),
591            [Space("   "), Literal("😽😽"), Space("    ")]
592        );
593        assert_eq!(parse_and_collect(" 😽😽    "), [Space(" "), Literal("😽😽"), Space("    ")]);
594        assert_eq!(
595            parse_and_collect(" 😽 😽😽    "),
596            [Space(" "), Literal("😽"), Space(" "), Literal("😽😽"), Space("    ")]
597        );
598        assert_eq!(
599            parse_and_collect(" 😽 πŸ˜½γ―γ„πŸ˜½    ハンバーガー"),
600            [
601                Space(" "),
602                Literal("😽"),
603                Space(" "),
604                Literal("πŸ˜½γ―γ„πŸ˜½"),
605                Space("    "),
606                Literal("ハンバーガー")
607            ]
608        );
609        assert_eq!(
610            parse_and_collect("%%😽%%😽"),
611            [Literal("%"), Literal("😽"), Literal("%"), Literal("😽")]
612        );
613        assert_eq!(parse_and_collect("%Y--%m"), [num0(Year), Literal("--"), num0(Month)]);
614        assert_eq!(parse_and_collect("[%F]"), parse_and_collect("[%Y-%m-%d]"));
615        assert_eq!(parse_and_collect("100%%😽"), [Literal("100"), Literal("%"), Literal("😽")]);
616        assert_eq!(
617            parse_and_collect("100%%😽%%a"),
618            [Literal("100"), Literal("%"), Literal("😽"), Literal("%"), Literal("a")]
619        );
620        assert_eq!(parse_and_collect("😽100%%"), [Literal("😽100"), Literal("%")]);
621        assert_eq!(parse_and_collect("%m %d"), [num0(Month), Space(" "), num0(Day)]);
622        assert_eq!(parse_and_collect("%"), [Item::Error]);
623        assert_eq!(parse_and_collect("%%"), [Literal("%")]);
624        assert_eq!(parse_and_collect("%%%"), [Item::Error]);
625        assert_eq!(parse_and_collect("%a"), [fixed(Fixed::ShortWeekdayName)]);
626        assert_eq!(parse_and_collect("%aa"), [fixed(Fixed::ShortWeekdayName), Literal("a")]);
627        assert_eq!(parse_and_collect("%%a%"), [Item::Error]);
628        assert_eq!(parse_and_collect("%😽"), [Item::Error]);
629        assert_eq!(parse_and_collect("%😽😽"), [Item::Error]);
630        assert_eq!(parse_and_collect("%%%%"), [Literal("%"), Literal("%")]);
631        assert_eq!(
632            parse_and_collect("%%%%ハンバーガー"),
633            [Literal("%"), Literal("%"), Literal("ハンバーガー")]
634        );
635        assert_eq!(parse_and_collect("foo%?"), [Item::Error]);
636        assert_eq!(parse_and_collect("bar%42"), [Item::Error]);
637        assert_eq!(parse_and_collect("quux% +"), [Item::Error]);
638        assert_eq!(parse_and_collect("%.Z"), [Item::Error]);
639        assert_eq!(parse_and_collect("%:Z"), [Item::Error]);
640        assert_eq!(parse_and_collect("%-Z"), [Item::Error]);
641        assert_eq!(parse_and_collect("%0Z"), [Item::Error]);
642        assert_eq!(parse_and_collect("%_Z"), [Item::Error]);
643        assert_eq!(parse_and_collect("%.j"), [Item::Error]);
644        assert_eq!(parse_and_collect("%:j"), [Item::Error]);
645        assert_eq!(parse_and_collect("%-j"), [num(Ordinal)]);
646        assert_eq!(parse_and_collect("%0j"), [num0(Ordinal)]);
647        assert_eq!(parse_and_collect("%_j"), [nums(Ordinal)]);
648        assert_eq!(parse_and_collect("%.e"), [Item::Error]);
649        assert_eq!(parse_and_collect("%:e"), [Item::Error]);
650        assert_eq!(parse_and_collect("%-e"), [num(Day)]);
651        assert_eq!(parse_and_collect("%0e"), [num0(Day)]);
652        assert_eq!(parse_and_collect("%_e"), [nums(Day)]);
653        assert_eq!(parse_and_collect("%z"), [fixed(Fixed::TimezoneOffset)]);
654        assert_eq!(parse_and_collect("%:z"), [fixed(Fixed::TimezoneOffsetColon)]);
655        assert_eq!(parse_and_collect("%Z"), [fixed(Fixed::TimezoneName)]);
656        assert_eq!(parse_and_collect("%ZZZZ"), [fixed(Fixed::TimezoneName), Literal("ZZZ")]);
657        assert_eq!(parse_and_collect("%Z😽"), [fixed(Fixed::TimezoneName), Literal("😽")]);
658        assert_eq!(
659            parse_and_collect("%#z"),
660            [internal_fixed(InternalInternal::TimezoneOffsetPermissive)]
661        );
662        assert_eq!(parse_and_collect("%#m"), [Item::Error]);
663    }
664
665    #[test]
666    #[cfg(any(feature = "alloc", feature = "std"))]
667    fn test_strftime_docs() {
668        let dt = FixedOffset::east_opt(34200)
669            .unwrap()
670            .from_local_datetime(
671                &NaiveDate::from_ymd_opt(2001, 7, 8)
672                    .unwrap()
673                    .and_hms_nano_opt(0, 34, 59, 1_026_490_708)
674                    .unwrap(),
675            )
676            .unwrap();
677
678        // date specifiers
679        assert_eq!(dt.format("%Y").to_string(), "2001");
680        assert_eq!(dt.format("%C").to_string(), "20");
681        assert_eq!(dt.format("%y").to_string(), "01");
682        assert_eq!(dt.format("%m").to_string(), "07");
683        assert_eq!(dt.format("%b").to_string(), "Jul");
684        assert_eq!(dt.format("%B").to_string(), "July");
685        assert_eq!(dt.format("%h").to_string(), "Jul");
686        assert_eq!(dt.format("%d").to_string(), "08");
687        assert_eq!(dt.format("%e").to_string(), " 8");
688        assert_eq!(dt.format("%e").to_string(), dt.format("%_d").to_string());
689        assert_eq!(dt.format("%a").to_string(), "Sun");
690        assert_eq!(dt.format("%A").to_string(), "Sunday");
691        assert_eq!(dt.format("%w").to_string(), "0");
692        assert_eq!(dt.format("%u").to_string(), "7");
693        assert_eq!(dt.format("%U").to_string(), "27");
694        assert_eq!(dt.format("%W").to_string(), "27");
695        assert_eq!(dt.format("%G").to_string(), "2001");
696        assert_eq!(dt.format("%g").to_string(), "01");
697        assert_eq!(dt.format("%V").to_string(), "27");
698        assert_eq!(dt.format("%j").to_string(), "189");
699        assert_eq!(dt.format("%D").to_string(), "07/08/01");
700        assert_eq!(dt.format("%x").to_string(), "07/08/01");
701        assert_eq!(dt.format("%F").to_string(), "2001-07-08");
702        assert_eq!(dt.format("%v").to_string(), " 8-Jul-2001");
703
704        // time specifiers
705        assert_eq!(dt.format("%H").to_string(), "00");
706        assert_eq!(dt.format("%k").to_string(), " 0");
707        assert_eq!(dt.format("%k").to_string(), dt.format("%_H").to_string());
708        assert_eq!(dt.format("%I").to_string(), "12");
709        assert_eq!(dt.format("%l").to_string(), "12");
710        assert_eq!(dt.format("%l").to_string(), dt.format("%_I").to_string());
711        assert_eq!(dt.format("%P").to_string(), "am");
712        assert_eq!(dt.format("%p").to_string(), "AM");
713        assert_eq!(dt.format("%M").to_string(), "34");
714        assert_eq!(dt.format("%S").to_string(), "60");
715        assert_eq!(dt.format("%f").to_string(), "026490708");
716        assert_eq!(dt.format("%.f").to_string(), ".026490708");
717        assert_eq!(dt.with_nanosecond(1_026_490_000).unwrap().format("%.f").to_string(), ".026490");
718        assert_eq!(dt.format("%.3f").to_string(), ".026");
719        assert_eq!(dt.format("%.6f").to_string(), ".026490");
720        assert_eq!(dt.format("%.9f").to_string(), ".026490708");
721        assert_eq!(dt.format("%3f").to_string(), "026");
722        assert_eq!(dt.format("%6f").to_string(), "026490");
723        assert_eq!(dt.format("%9f").to_string(), "026490708");
724        assert_eq!(dt.format("%R").to_string(), "00:34");
725        assert_eq!(dt.format("%T").to_string(), "00:34:60");
726        assert_eq!(dt.format("%X").to_string(), "00:34:60");
727        assert_eq!(dt.format("%r").to_string(), "12:34:60 AM");
728
729        // time zone specifiers
730        //assert_eq!(dt.format("%Z").to_string(), "ACST");
731        assert_eq!(dt.format("%z").to_string(), "+0930");
732        assert_eq!(dt.format("%:z").to_string(), "+09:30");
733        assert_eq!(dt.format("%::z").to_string(), "+09:30:00");
734        assert_eq!(dt.format("%:::z").to_string(), "+09");
735
736        // date & time specifiers
737        assert_eq!(dt.format("%c").to_string(), "Sun Jul  8 00:34:60 2001");
738        assert_eq!(dt.format("%+").to_string(), "2001-07-08T00:34:60.026490708+09:30");
739
740        assert_eq!(
741            dt.with_timezone(&Utc).format("%+").to_string(),
742            "2001-07-07T15:04:60.026490708+00:00"
743        );
744        assert_eq!(
745            dt.with_timezone(&Utc),
746            DateTime::parse_from_str("2001-07-07T15:04:60.026490708Z", "%+").unwrap()
747        );
748        assert_eq!(
749            dt.with_timezone(&Utc),
750            DateTime::parse_from_str("2001-07-07T15:04:60.026490708UTC", "%+").unwrap()
751        );
752        assert_eq!(
753            dt.with_timezone(&Utc),
754            DateTime::parse_from_str("2001-07-07t15:04:60.026490708utc", "%+").unwrap()
755        );
756
757        assert_eq!(
758            dt.with_nanosecond(1_026_490_000).unwrap().format("%+").to_string(),
759            "2001-07-08T00:34:60.026490+09:30"
760        );
761        assert_eq!(dt.format("%s").to_string(), "994518299");
762
763        // special specifiers
764        assert_eq!(dt.format("%t").to_string(), "\t");
765        assert_eq!(dt.format("%n").to_string(), "\n");
766        assert_eq!(dt.format("%%").to_string(), "%");
767
768        // complex format specifiers
769        assert_eq!(dt.format("  %Y%d%m%%%%%t%H%M%S\t").to_string(), "  20010807%%\t003460\t");
770        assert_eq!(
771            dt.format("  %Y%d%m%%%%%t%H:%P:%M%S%:::z\t").to_string(),
772            "  20010807%%\t00:am:3460+09\t"
773        );
774    }
775
776    #[test]
777    #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))]
778    fn test_strftime_docs_localized() {
779        let dt = FixedOffset::east_opt(34200)
780            .unwrap()
781            .with_ymd_and_hms(2001, 7, 8, 0, 34, 59)
782            .unwrap()
783            .with_nanosecond(1_026_490_708)
784            .unwrap();
785
786        // date specifiers
787        assert_eq!(dt.format_localized("%b", Locale::fr_BE).to_string(), "jui");
788        assert_eq!(dt.format_localized("%B", Locale::fr_BE).to_string(), "juillet");
789        assert_eq!(dt.format_localized("%h", Locale::fr_BE).to_string(), "jui");
790        assert_eq!(dt.format_localized("%a", Locale::fr_BE).to_string(), "dim");
791        assert_eq!(dt.format_localized("%A", Locale::fr_BE).to_string(), "dimanche");
792        assert_eq!(dt.format_localized("%D", Locale::fr_BE).to_string(), "07/08/01");
793        assert_eq!(dt.format_localized("%x", Locale::fr_BE).to_string(), "08/07/01");
794        assert_eq!(dt.format_localized("%F", Locale::fr_BE).to_string(), "2001-07-08");
795        assert_eq!(dt.format_localized("%v", Locale::fr_BE).to_string(), " 8-jui-2001");
796
797        // time specifiers
798        assert_eq!(dt.format_localized("%P", Locale::fr_BE).to_string(), "");
799        assert_eq!(dt.format_localized("%p", Locale::fr_BE).to_string(), "");
800        assert_eq!(dt.format_localized("%R", Locale::fr_BE).to_string(), "00:34");
801        assert_eq!(dt.format_localized("%T", Locale::fr_BE).to_string(), "00:34:60");
802        assert_eq!(dt.format_localized("%X", Locale::fr_BE).to_string(), "00:34:60");
803        assert_eq!(dt.format_localized("%r", Locale::fr_BE).to_string(), "00:34:60");
804
805        // date & time specifiers
806        assert_eq!(
807            dt.format_localized("%c", Locale::fr_BE).to_string(),
808            "dim 08 jui 2001 00:34:60 +09:30"
809        );
810
811        let nd = NaiveDate::from_ymd_opt(2001, 7, 8).unwrap();
812
813        // date specifiers
814        assert_eq!(nd.format_localized("%b", Locale::de_DE).to_string(), "Jul");
815        assert_eq!(nd.format_localized("%B", Locale::de_DE).to_string(), "Juli");
816        assert_eq!(nd.format_localized("%h", Locale::de_DE).to_string(), "Jul");
817        assert_eq!(nd.format_localized("%a", Locale::de_DE).to_string(), "So");
818        assert_eq!(nd.format_localized("%A", Locale::de_DE).to_string(), "Sonntag");
819        assert_eq!(nd.format_localized("%D", Locale::de_DE).to_string(), "07/08/01");
820        assert_eq!(nd.format_localized("%x", Locale::de_DE).to_string(), "08.07.2001");
821        assert_eq!(nd.format_localized("%F", Locale::de_DE).to_string(), "2001-07-08");
822        assert_eq!(nd.format_localized("%v", Locale::de_DE).to_string(), " 8-Jul-2001");
823    }
824
825    /// Ensure parsing a timestamp with the parse-only stftime formatter "%#z" does
826    /// not cause a panic.
827    ///
828    /// See <https://github.com/chronotope/chrono/issues/1139>.
829    #[test]
830    #[cfg(any(feature = "alloc", feature = "std"))]
831    fn test_parse_only_timezone_offset_permissive_no_panic() {
832        use crate::NaiveDate;
833        use crate::{FixedOffset, TimeZone};
834        use std::fmt::Write;
835
836        let dt = FixedOffset::east_opt(34200)
837            .unwrap()
838            .from_local_datetime(
839                &NaiveDate::from_ymd_opt(2001, 7, 8)
840                    .unwrap()
841                    .and_hms_nano_opt(0, 34, 59, 1_026_490_708)
842                    .unwrap(),
843            )
844            .unwrap();
845
846        let mut buf = String::new();
847        let _ = write!(buf, "{}", dt.format("%#z")).expect_err("parse-only formatter should fail");
848    }
849
850    #[test]
851    #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))]
852    fn test_strftime_localized_korean() {
853        let dt = FixedOffset::east_opt(34200)
854            .unwrap()
855            .with_ymd_and_hms(2001, 7, 8, 0, 34, 59)
856            .unwrap()
857            .with_nanosecond(1_026_490_708)
858            .unwrap();
859
860        // date specifiers
861        assert_eq!(dt.format_localized("%b", Locale::ko_KR).to_string(), " 7μ›”");
862        assert_eq!(dt.format_localized("%B", Locale::ko_KR).to_string(), "7μ›”");
863        assert_eq!(dt.format_localized("%h", Locale::ko_KR).to_string(), " 7μ›”");
864        assert_eq!(dt.format_localized("%a", Locale::ko_KR).to_string(), "일");
865        assert_eq!(dt.format_localized("%A", Locale::ko_KR).to_string(), "μΌμš”μΌ");
866        assert_eq!(dt.format_localized("%D", Locale::ko_KR).to_string(), "07/08/01");
867        assert_eq!(dt.format_localized("%x", Locale::ko_KR).to_string(), "2001λ…„ 07μ›” 08일");
868        assert_eq!(dt.format_localized("%F", Locale::ko_KR).to_string(), "2001-07-08");
869        assert_eq!(dt.format_localized("%v", Locale::ko_KR).to_string(), " 8- 7μ›”-2001");
870        assert_eq!(dt.format_localized("%r", Locale::ko_KR).to_string(), "μ˜€μ „ 12μ‹œ 34λΆ„ 60초");
871
872        // date & time specifiers
873        assert_eq!(
874            dt.format_localized("%c", Locale::ko_KR).to_string(),
875            "2001λ…„ 07μ›” 08일 (일) μ˜€μ „ 12μ‹œ 34λΆ„ 60초"
876        );
877    }
878
879    #[test]
880    #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))]
881    fn test_strftime_localized_japanese() {
882        let dt = FixedOffset::east_opt(34200)
883            .unwrap()
884            .with_ymd_and_hms(2001, 7, 8, 0, 34, 59)
885            .unwrap()
886            .with_nanosecond(1_026_490_708)
887            .unwrap();
888
889        // date specifiers
890        assert_eq!(dt.format_localized("%b", Locale::ja_JP).to_string(), " 7月");
891        assert_eq!(dt.format_localized("%B", Locale::ja_JP).to_string(), "7月");
892        assert_eq!(dt.format_localized("%h", Locale::ja_JP).to_string(), " 7月");
893        assert_eq!(dt.format_localized("%a", Locale::ja_JP).to_string(), "ζ—₯");
894        assert_eq!(dt.format_localized("%A", Locale::ja_JP).to_string(), "ζ—₯ζ›œζ—₯");
895        assert_eq!(dt.format_localized("%D", Locale::ja_JP).to_string(), "07/08/01");
896        assert_eq!(dt.format_localized("%x", Locale::ja_JP).to_string(), "2001εΉ΄07月08ζ—₯");
897        assert_eq!(dt.format_localized("%F", Locale::ja_JP).to_string(), "2001-07-08");
898        assert_eq!(dt.format_localized("%v", Locale::ja_JP).to_string(), " 8- 7月-2001");
899        assert_eq!(dt.format_localized("%r", Locale::ja_JP).to_string(), "εˆε‰12ζ™‚34εˆ†60η§’");
900
901        // date & time specifiers
902        assert_eq!(
903            dt.format_localized("%c", Locale::ja_JP).to_string(),
904            "2001εΉ΄07月08ζ—₯ 00ζ™‚34εˆ†60η§’"
905        );
906    }
907
908    #[test]
909    #[cfg(all(feature = "unstable-locales", target_pointer_width = "64"))]
910    fn test_type_sizes() {
911        use core::mem::size_of;
912        assert_eq!(size_of::<Item>(), 24);
913        assert_eq!(size_of::<StrftimeItems>(), 56);
914        assert_eq!(size_of::<Locale>(), 2);
915    }
916
917    #[test]
918    #[cfg(all(feature = "unstable-locales", target_pointer_width = "32"))]
919    fn test_type_sizes() {
920        use core::mem::size_of;
921        assert_eq!(size_of::<Item>(), 12);
922        assert_eq!(size_of::<StrftimeItems>(), 28);
923        assert_eq!(size_of::<Locale>(), 2);
924    }
925}