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            Token::Literal(spanned) => Ok(BorrowedFormatItem::Literal(*spanned)),
137            Token::Component {
138                _percent,
139                padding,
140                component,
141            } => parse_component(padding, component),
142        })
143    })
144}
145
146fn parse_component(
147    padding: Spanned<Padding>,
148    component: Spanned<u8>,
149) -> Result<BorrowedFormatItem<'static>, Error> {
150    let padding_or_default = |padding: Padding, default| match padding {
151        Padding::Default => default,
152        Padding::Spaces => modifier::Padding::Space,
153        Padding::None => modifier::Padding::None,
154        Padding::Zeroes => modifier::Padding::Zero,
155    };
156
157    /// Helper macro to create a component.
158    macro_rules! component {
159        ($name:ident { $($inner:tt)* }) => {
160            BorrowedFormatItem::Component(Component::$name(modifier::$name {
161                $($inner)*
162            }))
163        }
164    }
165
166    Ok(match *component {
167        b'%' => BorrowedFormatItem::Literal(b"%"),
168        b'a' => component!(Weekday {
169            repr: modifier::WeekdayRepr::Short,
170            one_indexed: true,
171            case_sensitive: true,
172        }),
173        b'A' => component!(Weekday {
174            repr: modifier::WeekdayRepr::Long,
175            one_indexed: true,
176            case_sensitive: true,
177        }),
178        b'b' | b'h' => component!(Month {
179            repr: modifier::MonthRepr::Short,
180            padding: modifier::Padding::Zero,
181            case_sensitive: true,
182        }),
183        b'B' => component!(Month {
184            repr: modifier::MonthRepr::Long,
185            padding: modifier::Padding::Zero,
186            case_sensitive: true,
187        }),
188        b'c' => BorrowedFormatItem::Compound(&[
189            component!(Weekday {
190                repr: modifier::WeekdayRepr::Short,
191                one_indexed: true,
192                case_sensitive: true,
193            }),
194            BorrowedFormatItem::Literal(b" "),
195            component!(Month {
196                repr: modifier::MonthRepr::Short,
197                padding: modifier::Padding::Zero,
198                case_sensitive: true,
199            }),
200            BorrowedFormatItem::Literal(b" "),
201            component!(Day {
202                padding: modifier::Padding::Space
203            }),
204            BorrowedFormatItem::Literal(b" "),
205            component!(Hour {
206                padding: modifier::Padding::Zero,
207                is_12_hour_clock: false,
208            }),
209            BorrowedFormatItem::Literal(b":"),
210            component!(Minute {
211                padding: modifier::Padding::Zero,
212            }),
213            BorrowedFormatItem::Literal(b":"),
214            component!(Second {
215                padding: modifier::Padding::Zero,
216            }),
217            BorrowedFormatItem::Literal(b" "),
218            component!(Year {
219                padding: modifier::Padding::Zero,
220                repr: modifier::YearRepr::Full,
221                range: modifier::YearRange::Extended,
222                iso_week_based: false,
223                sign_is_mandatory: false,
224            }),
225        ]),
226        b'C' => component!(Year {
227            padding: padding_or_default(*padding, modifier::Padding::Zero),
228            repr: modifier::YearRepr::Century,
229            range: modifier::YearRange::Extended,
230            iso_week_based: false,
231            sign_is_mandatory: false,
232        }),
233        b'd' => component!(Day {
234            padding: padding_or_default(*padding, modifier::Padding::Zero),
235        }),
236        b'D' => BorrowedFormatItem::Compound(&[
237            component!(Month {
238                repr: modifier::MonthRepr::Numerical,
239                padding: modifier::Padding::Zero,
240                case_sensitive: true,
241            }),
242            BorrowedFormatItem::Literal(b"/"),
243            component!(Day {
244                padding: modifier::Padding::Zero,
245            }),
246            BorrowedFormatItem::Literal(b"/"),
247            component!(Year {
248                padding: modifier::Padding::Zero,
249                repr: modifier::YearRepr::LastTwo,
250                range: modifier::YearRange::Extended,
251                iso_week_based: false,
252                sign_is_mandatory: false,
253            }),
254        ]),
255        b'e' => component!(Day {
256            padding: padding_or_default(*padding, modifier::Padding::Space),
257        }),
258        b'F' => BorrowedFormatItem::Compound(&[
259            component!(Year {
260                padding: modifier::Padding::Zero,
261                repr: modifier::YearRepr::Full,
262                range: modifier::YearRange::Extended,
263                iso_week_based: false,
264                sign_is_mandatory: false,
265            }),
266            BorrowedFormatItem::Literal(b"-"),
267            component!(Month {
268                padding: modifier::Padding::Zero,
269                repr: modifier::MonthRepr::Numerical,
270                case_sensitive: true,
271            }),
272            BorrowedFormatItem::Literal(b"-"),
273            component!(Day {
274                padding: modifier::Padding::Zero,
275            }),
276        ]),
277        b'g' => component!(Year {
278            padding: padding_or_default(*padding, modifier::Padding::Zero),
279            repr: modifier::YearRepr::LastTwo,
280            range: modifier::YearRange::Extended,
281            iso_week_based: true,
282            sign_is_mandatory: false,
283        }),
284        b'G' => component!(Year {
285            padding: modifier::Padding::Zero,
286            repr: modifier::YearRepr::Full,
287            range: modifier::YearRange::Extended,
288            iso_week_based: true,
289            sign_is_mandatory: false,
290        }),
291        b'H' => component!(Hour {
292            padding: padding_or_default(*padding, modifier::Padding::Zero),
293            is_12_hour_clock: false,
294        }),
295        b'I' => component!(Hour {
296            padding: padding_or_default(*padding, modifier::Padding::Zero),
297            is_12_hour_clock: true,
298        }),
299        b'j' => component!(Ordinal {
300            padding: padding_or_default(*padding, modifier::Padding::Zero),
301        }),
302        b'k' => component!(Hour {
303            padding: padding_or_default(*padding, modifier::Padding::Space),
304            is_12_hour_clock: false,
305        }),
306        b'l' => component!(Hour {
307            padding: padding_or_default(*padding, modifier::Padding::Space),
308            is_12_hour_clock: true,
309        }),
310        b'm' => component!(Month {
311            padding: padding_or_default(*padding, modifier::Padding::Zero),
312            repr: modifier::MonthRepr::Numerical,
313            case_sensitive: true,
314        }),
315        b'M' => component!(Minute {
316            padding: padding_or_default(*padding, modifier::Padding::Zero),
317        }),
318        b'n' => BorrowedFormatItem::Literal(b"\n"),
319        b'O' => {
320            return Err(Error {
321                _inner: unused(ErrorInner {
322                    _message: "unsupported modifier",
323                    _span: component.span,
324                }),
325                public: InvalidFormatDescription::NotSupported {
326                    what: "modifier",
327                    context: "",
328                    index: component.span.start.byte as usize,
329                },
330            });
331        }
332        b'p' => component!(Period {
333            is_uppercase: true,
334            case_sensitive: true
335        }),
336        b'P' => component!(Period {
337            is_uppercase: false,
338            case_sensitive: true
339        }),
340        b'r' => BorrowedFormatItem::Compound(&[
341            component!(Hour {
342                padding: modifier::Padding::Zero,
343                is_12_hour_clock: true,
344            }),
345            BorrowedFormatItem::Literal(b":"),
346            component!(Minute {
347                padding: modifier::Padding::Zero,
348            }),
349            BorrowedFormatItem::Literal(b":"),
350            component!(Second {
351                padding: modifier::Padding::Zero,
352            }),
353            BorrowedFormatItem::Literal(b" "),
354            component!(Period {
355                is_uppercase: true,
356                case_sensitive: true,
357            }),
358        ]),
359        b'R' => BorrowedFormatItem::Compound(&[
360            component!(Hour {
361                padding: modifier::Padding::Zero,
362                is_12_hour_clock: false,
363            }),
364            BorrowedFormatItem::Literal(b":"),
365            component!(Minute {
366                padding: modifier::Padding::Zero,
367            }),
368        ]),
369        b's' => component!(UnixTimestamp {
370            precision: modifier::UnixTimestampPrecision::Second,
371            sign_is_mandatory: false,
372        }),
373        b'S' => component!(Second {
374            padding: padding_or_default(*padding, modifier::Padding::Zero),
375        }),
376        b't' => BorrowedFormatItem::Literal(b"\t"),
377        b'T' => BorrowedFormatItem::Compound(&[
378            component!(Hour {
379                padding: modifier::Padding::Zero,
380                is_12_hour_clock: false,
381            }),
382            BorrowedFormatItem::Literal(b":"),
383            component!(Minute {
384                padding: modifier::Padding::Zero,
385            }),
386            BorrowedFormatItem::Literal(b":"),
387            component!(Second {
388                padding: modifier::Padding::Zero,
389            }),
390        ]),
391        b'u' => component!(Weekday {
392            repr: modifier::WeekdayRepr::Monday,
393            one_indexed: true,
394            case_sensitive: true,
395        }),
396        b'U' => component!(WeekNumber {
397            padding: padding_or_default(*padding, modifier::Padding::Zero),
398            repr: modifier::WeekNumberRepr::Sunday,
399        }),
400        b'V' => component!(WeekNumber {
401            padding: padding_or_default(*padding, modifier::Padding::Zero),
402            repr: modifier::WeekNumberRepr::Iso,
403        }),
404        b'w' => component!(Weekday {
405            repr: modifier::WeekdayRepr::Sunday,
406            one_indexed: true,
407            case_sensitive: true,
408        }),
409        b'W' => component!(WeekNumber {
410            padding: padding_or_default(*padding, modifier::Padding::Zero),
411            repr: modifier::WeekNumberRepr::Monday,
412        }),
413        b'x' => BorrowedFormatItem::Compound(&[
414            component!(Month {
415                repr: modifier::MonthRepr::Numerical,
416                padding: modifier::Padding::Zero,
417                case_sensitive: true,
418            }),
419            BorrowedFormatItem::Literal(b"/"),
420            component!(Day {
421                padding: modifier::Padding::Zero
422            }),
423            BorrowedFormatItem::Literal(b"/"),
424            component!(Year {
425                padding: modifier::Padding::Zero,
426                repr: modifier::YearRepr::LastTwo,
427                range: modifier::YearRange::Extended,
428                iso_week_based: false,
429                sign_is_mandatory: false,
430            }),
431        ]),
432        b'X' => BorrowedFormatItem::Compound(&[
433            component!(Hour {
434                padding: modifier::Padding::Zero,
435                is_12_hour_clock: false,
436            }),
437            BorrowedFormatItem::Literal(b":"),
438            component!(Minute {
439                padding: modifier::Padding::Zero,
440            }),
441            BorrowedFormatItem::Literal(b":"),
442            component!(Second {
443                padding: modifier::Padding::Zero,
444            }),
445        ]),
446        b'y' => component!(Year {
447            padding: padding_or_default(*padding, modifier::Padding::Zero),
448            repr: modifier::YearRepr::LastTwo,
449            range: modifier::YearRange::Extended,
450            iso_week_based: false,
451            sign_is_mandatory: false,
452        }),
453        b'Y' => component!(Year {
454            padding: modifier::Padding::Zero,
455            repr: modifier::YearRepr::Full,
456            range: modifier::YearRange::Extended,
457            iso_week_based: false,
458            sign_is_mandatory: false,
459        }),
460        b'z' => BorrowedFormatItem::Compound(&[
461            component!(OffsetHour {
462                sign_is_mandatory: true,
463                padding: modifier::Padding::Zero,
464            }),
465            component!(OffsetMinute {
466                padding: modifier::Padding::Zero,
467            }),
468        ]),
469        b'Z' => {
470            return Err(Error {
471                _inner: unused(ErrorInner {
472                    _message: "unsupported component",
473                    _span: component.span,
474                }),
475                public: InvalidFormatDescription::NotSupported {
476                    what: "component",
477                    context: "",
478                    index: component.span.start.byte as usize,
479                },
480            });
481        }
482        _ => {
483            return Err(Error {
484                _inner: unused(ErrorInner {
485                    _message: "invalid component",
486                    _span: component.span,
487                }),
488                public: InvalidFormatDescription::InvalidComponentName {
489                    name: String::from_utf8_lossy(&[*component]).into_owned(),
490                    index: component.span.start.byte as usize,
491                },
492            });
493        }
494    })
495}