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