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