Skip to main content

time/format_description/parse/
strftime.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::iter;
4
5use crate::error::InvalidFormatDescription;
6use crate::format_description::parse::{
7    Error, ErrorInner, Location, Spanned, SpannedValue, Unused, attach_location, unused,
8};
9use crate::format_description::{self, BorrowedFormatItem, Component, modifier};
10
11/// Parse a sequence of items from the [`strftime` format description][strftime docs].
12///
13/// The only heap allocation required is for the `Vec` itself. All components are bound to the
14/// lifetime of the input.
15///
16/// [strftime docs]: https://man7.org/linux/man-pages/man3/strftime.3.html
17#[doc(alias = "parse_strptime_borrowed")]
18#[inline]
19pub fn parse_strftime_borrowed(
20    s: &str,
21) -> Result<Vec<BorrowedFormatItem<'_>>, InvalidFormatDescription> {
22    let tokens = lex(s.as_bytes());
23    let items = into_items(tokens).collect::<Result<_, _>>()?;
24    Ok(items)
25}
26
27/// Parse a sequence of items from the [`strftime` format description][strftime docs].
28///
29/// This requires heap allocation for some owned items.
30///
31/// [strftime docs]: https://man7.org/linux/man-pages/man3/strftime.3.html
32#[doc(alias = "parse_strptime_owned")]
33#[inline]
34pub fn parse_strftime_owned(
35    s: &str,
36) -> Result<format_description::OwnedFormatItem, InvalidFormatDescription> {
37    parse_strftime_borrowed(s).map(Into::into)
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
41enum Padding {
42    /// The default padding for a numeric component. Indicated by no character.
43    Default,
44    /// Pad a numeric component with spaces. Indicated by an underscore.
45    Spaces,
46    /// Do not pad a numeric component. Indicated by a hyphen.
47    None,
48    /// Pad a numeric component with zeroes. Indicated by a zero.
49    Zeroes,
50}
51
52enum Token<'a> {
53    Literal(Spanned<&'a [u8]>),
54    Component {
55        _percent: Unused<Location>,
56        padding: Spanned<Padding>,
57        component: Spanned<u8>,
58    },
59}
60
61#[inline]
62fn lex(mut input: &[u8]) -> iter::Peekable<impl Iterator<Item = Result<Token<'_>, Error>>> {
63    let mut iter = attach_location(input.iter()).peekable();
64
65    iter::from_fn(move || {
66        Some(Ok(match iter.next()? {
67            (b'%', percent_loc) => match iter.next() {
68                Some((padding @ (b'_' | b'-' | b'0'), padding_loc)) => {
69                    let padding = match padding {
70                        b'_' => Padding::Spaces,
71                        b'-' => Padding::None,
72                        b'0' => Padding::Zeroes,
73                        _ => unreachable!(),
74                    };
75                    let (&component, component_loc) = iter.next()?;
76                    input = &input[3..];
77                    Token::Component {
78                        _percent: unused(percent_loc),
79                        padding: padding.spanned(padding_loc.to_self()),
80                        component: component.spanned(component_loc.to_self()),
81                    }
82                }
83                Some((&component, component_loc)) => {
84                    input = &input[2..];
85                    let span = component_loc.to_self();
86                    Token::Component {
87                        _percent: unused(percent_loc),
88                        padding: Padding::Default.spanned(span),
89                        component: component.spanned(span),
90                    }
91                }
92                None => {
93                    return Some(Err(Error {
94                        _inner: unused(percent_loc.error("unexpected end of input")),
95                        public: InvalidFormatDescription::Expected {
96                            what: "valid escape sequence",
97                            index: percent_loc.byte as usize,
98                        },
99                    }));
100                }
101            },
102            (_, start_location) => {
103                let mut bytes = 1;
104                let mut end_location = start_location;
105
106                while let Some((_, location)) = iter.next_if(|&(&byte, _)| byte != b'%') {
107                    end_location = location;
108                    bytes += 1;
109                }
110
111                let value = &input[..bytes];
112                input = &input[bytes..];
113
114                Token::Literal(value.spanned(start_location.to(end_location)))
115            }
116        }))
117    })
118    .peekable()
119}
120
121#[inline]
122fn into_items<'iter, 'token, I>(
123    mut tokens: iter::Peekable<I>,
124) -> impl Iterator<Item = Result<BorrowedFormatItem<'token>, Error>> + use<'token, I>
125where
126    'token: 'iter,
127    I: Iterator<Item = Result<Token<'token>, Error>> + 'iter,
128{
129    iter::from_fn(move || {
130        let next = match tokens.next()? {
131            Ok(token) => token,
132            Err(err) => return Some(Err(err)),
133        };
134
135        Some(match next {
136            // Safety: `parse_strftime` functions only accept strings, so UTF-8 validation is
137            // unnecessary
138            Token::Literal(spanned) => Ok(BorrowedFormatItem::StringLiteral(unsafe {
139                str::from_utf8_unchecked(*spanned)
140            })),
141            Token::Component {
142                _percent,
143                padding,
144                component,
145            } => parse_component(padding, component),
146        })
147    })
148}
149
150fn parse_component(
151    padding: Spanned<Padding>,
152    component: Spanned<u8>,
153) -> Result<BorrowedFormatItem<'static>, Error> {
154    let padding_or_default = |padding: Padding, default| match padding {
155        Padding::Default => default,
156        Padding::Spaces => modifier::Padding::Space,
157        Padding::None => modifier::Padding::None,
158        Padding::Zeroes => modifier::Padding::Zero,
159    };
160
161    /// Helper macro to create a component.
162    macro_rules! component {
163        ($name:ident { $($inner:tt)* }) => {
164            BorrowedFormatItem::Component(Component::$name(modifier::$name {
165                $($inner)*
166            }))
167        }
168    }
169
170    Ok(match *component {
171        b'%' => BorrowedFormatItem::StringLiteral("%"),
172        b'a' => component!(WeekdayShort {
173            case_sensitive: true
174        }),
175        b'A' => component!(WeekdayLong {
176            case_sensitive: true,
177        }),
178        b'b' | b'h' => component!(MonthShort {
179            case_sensitive: true,
180        }),
181        b'B' => component!(MonthLong {
182            case_sensitive: true,
183        }),
184        b'c' => BorrowedFormatItem::Compound(&[
185            component!(WeekdayShort {
186                case_sensitive: true,
187            }),
188            BorrowedFormatItem::StringLiteral(" "),
189            component!(MonthShort {
190                case_sensitive: true,
191            }),
192            BorrowedFormatItem::StringLiteral(" "),
193            component!(Day {
194                padding: modifier::Padding::Space
195            }),
196            BorrowedFormatItem::StringLiteral(" "),
197            component!(Hour24 {
198                padding: modifier::Padding::Zero,
199            }),
200            BorrowedFormatItem::StringLiteral(":"),
201            component!(Minute {
202                padding: modifier::Padding::Zero,
203            }),
204            BorrowedFormatItem::StringLiteral(":"),
205            component!(Second {
206                padding: modifier::Padding::Zero,
207            }),
208            BorrowedFormatItem::StringLiteral(" "),
209            #[cfg(feature = "large-dates")]
210            component!(CalendarYearFullExtendedRange {
211                padding: modifier::Padding::Zero,
212                sign_is_mandatory: false,
213            }),
214            #[cfg(not(feature = "large-dates"))]
215            component!(CalendarYearFullStandardRange {
216                padding: modifier::Padding::Zero,
217                sign_is_mandatory: false,
218            }),
219        ]),
220        #[cfg(feature = "large-dates")]
221        b'C' => component!(CalendarYearCenturyExtendedRange {
222            padding: padding_or_default(*padding, modifier::Padding::Zero),
223            sign_is_mandatory: false,
224        }),
225        #[cfg(not(feature = "large-dates"))]
226        b'C' => component!(CalendarYearCenturyStandardRange {
227            padding: padding_or_default(*padding, modifier::Padding::Zero),
228            sign_is_mandatory: false,
229        }),
230        b'd' => component!(Day {
231            padding: padding_or_default(*padding, modifier::Padding::Zero),
232        }),
233        b'D' => BorrowedFormatItem::Compound(&[
234            component!(MonthNumerical {
235                padding: modifier::Padding::Zero,
236            }),
237            BorrowedFormatItem::StringLiteral("/"),
238            component!(Day {
239                padding: modifier::Padding::Zero,
240            }),
241            BorrowedFormatItem::StringLiteral("/"),
242            component!(CalendarYearLastTwo {
243                padding: modifier::Padding::Zero,
244            }),
245        ]),
246        b'e' => component!(Day {
247            padding: padding_or_default(*padding, modifier::Padding::Space),
248        }),
249        b'F' => BorrowedFormatItem::Compound(&[
250            #[cfg(feature = "large-dates")]
251            component!(CalendarYearFullExtendedRange {
252                padding: modifier::Padding::Zero,
253                sign_is_mandatory: false,
254            }),
255            #[cfg(not(feature = "large-dates"))]
256            component!(CalendarYearFullStandardRange {
257                padding: modifier::Padding::Zero,
258                sign_is_mandatory: false,
259            }),
260            BorrowedFormatItem::StringLiteral("-"),
261            component!(MonthNumerical {
262                padding: modifier::Padding::Zero,
263            }),
264            BorrowedFormatItem::StringLiteral("-"),
265            component!(Day {
266                padding: modifier::Padding::Zero,
267            }),
268        ]),
269        b'g' => component!(IsoYearLastTwo {
270            padding: padding_or_default(*padding, modifier::Padding::Zero),
271        }),
272        #[cfg(feature = "large-dates")]
273        b'G' => component!(IsoYearFullExtendedRange {
274            padding: modifier::Padding::Zero,
275            sign_is_mandatory: false,
276        }),
277        #[cfg(not(feature = "large-dates"))]
278        b'G' => component!(IsoYearFullStandardRange {
279            padding: modifier::Padding::Zero,
280            sign_is_mandatory: false,
281        }),
282        b'H' => component!(Hour24 {
283            padding: padding_or_default(*padding, modifier::Padding::Zero),
284        }),
285        b'I' => component!(Hour12 {
286            padding: padding_or_default(*padding, modifier::Padding::Zero),
287        }),
288        b'j' => component!(Ordinal {
289            padding: padding_or_default(*padding, modifier::Padding::Zero),
290        }),
291        b'k' => component!(Hour24 {
292            padding: padding_or_default(*padding, modifier::Padding::Space),
293        }),
294        b'l' => component!(Hour12 {
295            padding: padding_or_default(*padding, modifier::Padding::Space),
296        }),
297        b'm' => component!(MonthNumerical {
298            padding: padding_or_default(*padding, modifier::Padding::Zero),
299        }),
300        b'M' => component!(Minute {
301            padding: padding_or_default(*padding, modifier::Padding::Zero),
302        }),
303        b'n' => BorrowedFormatItem::StringLiteral("\n"),
304        b'O' => {
305            return Err(Error {
306                _inner: unused(ErrorInner {
307                    _message: "unsupported modifier",
308                    _span: component.span,
309                }),
310                public: InvalidFormatDescription::NotSupported {
311                    what: "modifier",
312                    context: "",
313                    index: component.span.start.byte as usize,
314                },
315            });
316        }
317        b'p' => component!(Period {
318            is_uppercase: true,
319            case_sensitive: true
320        }),
321        b'P' => component!(Period {
322            is_uppercase: false,
323            case_sensitive: true
324        }),
325        b'r' => BorrowedFormatItem::Compound(&[
326            component!(Hour12 {
327                padding: modifier::Padding::Zero,
328            }),
329            BorrowedFormatItem::StringLiteral(":"),
330            component!(Minute {
331                padding: modifier::Padding::Zero,
332            }),
333            BorrowedFormatItem::StringLiteral(":"),
334            component!(Second {
335                padding: modifier::Padding::Zero,
336            }),
337            BorrowedFormatItem::StringLiteral(" "),
338            component!(Period {
339                is_uppercase: true,
340                case_sensitive: true,
341            }),
342        ]),
343        b'R' => BorrowedFormatItem::Compound(&[
344            component!(Hour24 {
345                padding: modifier::Padding::Zero,
346            }),
347            BorrowedFormatItem::StringLiteral(":"),
348            component!(Minute {
349                padding: modifier::Padding::Zero,
350            }),
351        ]),
352        b's' => component!(UnixTimestampSecond {
353            sign_is_mandatory: false,
354        }),
355        b'S' => component!(Second {
356            padding: padding_or_default(*padding, modifier::Padding::Zero),
357        }),
358        b't' => BorrowedFormatItem::StringLiteral("\t"),
359        b'T' => BorrowedFormatItem::Compound(&[
360            component!(Hour24 {
361                padding: modifier::Padding::Zero,
362            }),
363            BorrowedFormatItem::StringLiteral(":"),
364            component!(Minute {
365                padding: modifier::Padding::Zero,
366            }),
367            BorrowedFormatItem::StringLiteral(":"),
368            component!(Second {
369                padding: modifier::Padding::Zero,
370            }),
371        ]),
372        b'u' => component!(WeekdayMonday { one_indexed: true }),
373        b'U' => component!(WeekNumberSunday {
374            padding: padding_or_default(*padding, modifier::Padding::Zero),
375        }),
376        b'V' => component!(WeekNumberIso {
377            padding: padding_or_default(*padding, modifier::Padding::Zero),
378        }),
379        b'w' => component!(WeekdaySunday { one_indexed: true }),
380        b'W' => component!(WeekNumberMonday {
381            padding: padding_or_default(*padding, modifier::Padding::Zero),
382        }),
383        b'x' => BorrowedFormatItem::Compound(&[
384            component!(MonthNumerical {
385                padding: modifier::Padding::Zero,
386            }),
387            BorrowedFormatItem::StringLiteral("/"),
388            component!(Day {
389                padding: modifier::Padding::Zero
390            }),
391            BorrowedFormatItem::StringLiteral("/"),
392            component!(CalendarYearLastTwo {
393                padding: modifier::Padding::Zero,
394            }),
395        ]),
396        b'X' => BorrowedFormatItem::Compound(&[
397            component!(Hour24 {
398                padding: modifier::Padding::Zero,
399            }),
400            BorrowedFormatItem::StringLiteral(":"),
401            component!(Minute {
402                padding: modifier::Padding::Zero,
403            }),
404            BorrowedFormatItem::StringLiteral(":"),
405            component!(Second {
406                padding: modifier::Padding::Zero,
407            }),
408        ]),
409        b'y' => component!(CalendarYearLastTwo {
410            padding: padding_or_default(*padding, modifier::Padding::Zero),
411        }),
412        #[cfg(feature = "large-dates")]
413        b'Y' => component!(CalendarYearFullExtendedRange {
414            padding: modifier::Padding::Zero,
415            sign_is_mandatory: false,
416        }),
417        #[cfg(not(feature = "large-dates"))]
418        b'Y' => component!(CalendarYearFullStandardRange {
419            padding: modifier::Padding::Zero,
420            sign_is_mandatory: false,
421        }),
422        b'z' => BorrowedFormatItem::Compound(&[
423            component!(OffsetHour {
424                sign_is_mandatory: true,
425                padding: modifier::Padding::Zero,
426            }),
427            component!(OffsetMinute {
428                padding: modifier::Padding::Zero,
429            }),
430        ]),
431        b'Z' => {
432            return Err(Error {
433                _inner: unused(ErrorInner {
434                    _message: "unsupported component",
435                    _span: component.span,
436                }),
437                public: InvalidFormatDescription::NotSupported {
438                    what: "component",
439                    context: "",
440                    index: component.span.start.byte as usize,
441                },
442            });
443        }
444        _ => {
445            return Err(Error {
446                _inner: unused(ErrorInner {
447                    _message: "invalid component",
448                    _span: component.span,
449                }),
450                public: InvalidFormatDescription::InvalidComponentName {
451                    name: String::from_utf8_lossy(&[*component]).into_owned(),
452                    index: component.span.start.byte as usize,
453                },
454            });
455        }
456    })
457}