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                iso_week_based: false,
214                sign_is_mandatory: false,
215            }),
216        ]),
217        b'C' => component!(Year {
218            padding: padding_or_default(*padding, modifier::Padding::Zero),
219            repr: modifier::YearRepr::Century,
220            iso_week_based: false,
221            sign_is_mandatory: false,
222        }),
223        b'd' => component!(Day {
224            padding: padding_or_default(*padding, modifier::Padding::Zero),
225        }),
226        b'D' => BorrowedFormatItem::Compound(&[
227            component!(Month {
228                repr: modifier::MonthRepr::Numerical,
229                padding: modifier::Padding::Zero,
230                case_sensitive: true,
231            }),
232            BorrowedFormatItem::Literal(b"/"),
233            component!(Day {
234                padding: modifier::Padding::Zero,
235            }),
236            BorrowedFormatItem::Literal(b"/"),
237            component!(Year {
238                padding: modifier::Padding::Zero,
239                repr: modifier::YearRepr::LastTwo,
240                iso_week_based: false,
241                sign_is_mandatory: false,
242            }),
243        ]),
244        b'e' => component!(Day {
245            padding: padding_or_default(*padding, modifier::Padding::Space),
246        }),
247        b'F' => BorrowedFormatItem::Compound(&[
248            component!(Year {
249                padding: modifier::Padding::Zero,
250                repr: modifier::YearRepr::Full,
251                iso_week_based: false,
252                sign_is_mandatory: false,
253            }),
254            BorrowedFormatItem::Literal(b"-"),
255            component!(Month {
256                padding: modifier::Padding::Zero,
257                repr: modifier::MonthRepr::Numerical,
258                case_sensitive: true,
259            }),
260            BorrowedFormatItem::Literal(b"-"),
261            component!(Day {
262                padding: modifier::Padding::Zero,
263            }),
264        ]),
265        b'g' => component!(Year {
266            padding: padding_or_default(*padding, modifier::Padding::Zero),
267            repr: modifier::YearRepr::LastTwo,
268            iso_week_based: true,
269            sign_is_mandatory: false,
270        }),
271        b'G' => component!(Year {
272            padding: modifier::Padding::Zero,
273            repr: modifier::YearRepr::Full,
274            iso_week_based: true,
275            sign_is_mandatory: false,
276        }),
277        b'H' => component!(Hour {
278            padding: padding_or_default(*padding, modifier::Padding::Zero),
279            is_12_hour_clock: false,
280        }),
281        b'I' => component!(Hour {
282            padding: padding_or_default(*padding, modifier::Padding::Zero),
283            is_12_hour_clock: true,
284        }),
285        b'j' => component!(Ordinal {
286            padding: padding_or_default(*padding, modifier::Padding::Zero),
287        }),
288        b'k' => component!(Hour {
289            padding: padding_or_default(*padding, modifier::Padding::Space),
290            is_12_hour_clock: false,
291        }),
292        b'l' => component!(Hour {
293            padding: padding_or_default(*padding, modifier::Padding::Space),
294            is_12_hour_clock: true,
295        }),
296        b'm' => component!(Month {
297            padding: padding_or_default(*padding, modifier::Padding::Zero),
298            repr: modifier::MonthRepr::Numerical,
299            case_sensitive: true,
300        }),
301        b'M' => component!(Minute {
302            padding: padding_or_default(*padding, modifier::Padding::Zero),
303        }),
304        b'n' => BorrowedFormatItem::Literal(b"\n"),
305        b'O' => {
306            return Err(Error {
307                _inner: unused(ErrorInner {
308                    _message: "unsupported modifier",
309                    _span: component.span,
310                }),
311                public: InvalidFormatDescription::NotSupported {
312                    what: "modifier",
313                    context: "",
314                    index: component.span.start.byte as _,
315                },
316            })
317        }
318        b'p' => component!(Period {
319            is_uppercase: true,
320            case_sensitive: true
321        }),
322        b'P' => component!(Period {
323            is_uppercase: false,
324            case_sensitive: true
325        }),
326        b'r' => BorrowedFormatItem::Compound(&[
327            component!(Hour {
328                padding: modifier::Padding::Zero,
329                is_12_hour_clock: true,
330            }),
331            BorrowedFormatItem::Literal(b":"),
332            component!(Minute {
333                padding: modifier::Padding::Zero,
334            }),
335            BorrowedFormatItem::Literal(b":"),
336            component!(Second {
337                padding: modifier::Padding::Zero,
338            }),
339            BorrowedFormatItem::Literal(b" "),
340            component!(Period {
341                is_uppercase: true,
342                case_sensitive: true,
343            }),
344        ]),
345        b'R' => BorrowedFormatItem::Compound(&[
346            component!(Hour {
347                padding: modifier::Padding::Zero,
348                is_12_hour_clock: false,
349            }),
350            BorrowedFormatItem::Literal(b":"),
351            component!(Minute {
352                padding: modifier::Padding::Zero,
353            }),
354        ]),
355        b's' => component!(UnixTimestamp {
356            precision: modifier::UnixTimestampPrecision::Second,
357            sign_is_mandatory: false,
358        }),
359        b'S' => component!(Second {
360            padding: padding_or_default(*padding, modifier::Padding::Zero),
361        }),
362        b't' => BorrowedFormatItem::Literal(b"\t"),
363        b'T' => BorrowedFormatItem::Compound(&[
364            component!(Hour {
365                padding: modifier::Padding::Zero,
366                is_12_hour_clock: false,
367            }),
368            BorrowedFormatItem::Literal(b":"),
369            component!(Minute {
370                padding: modifier::Padding::Zero,
371            }),
372            BorrowedFormatItem::Literal(b":"),
373            component!(Second {
374                padding: modifier::Padding::Zero,
375            }),
376        ]),
377        b'u' => component!(Weekday {
378            repr: modifier::WeekdayRepr::Monday,
379            one_indexed: true,
380            case_sensitive: true,
381        }),
382        b'U' => component!(WeekNumber {
383            padding: padding_or_default(*padding, modifier::Padding::Zero),
384            repr: modifier::WeekNumberRepr::Sunday,
385        }),
386        b'V' => component!(WeekNumber {
387            padding: padding_or_default(*padding, modifier::Padding::Zero),
388            repr: modifier::WeekNumberRepr::Iso,
389        }),
390        b'w' => component!(Weekday {
391            repr: modifier::WeekdayRepr::Sunday,
392            one_indexed: true,
393            case_sensitive: true,
394        }),
395        b'W' => component!(WeekNumber {
396            padding: padding_or_default(*padding, modifier::Padding::Zero),
397            repr: modifier::WeekNumberRepr::Monday,
398        }),
399        b'x' => BorrowedFormatItem::Compound(&[
400            component!(Month {
401                repr: modifier::MonthRepr::Numerical,
402                padding: modifier::Padding::Zero,
403                case_sensitive: true,
404            }),
405            BorrowedFormatItem::Literal(b"/"),
406            component!(Day {
407                padding: modifier::Padding::Zero
408            }),
409            BorrowedFormatItem::Literal(b"/"),
410            component!(Year {
411                padding: modifier::Padding::Zero,
412                repr: modifier::YearRepr::LastTwo,
413                iso_week_based: false,
414                sign_is_mandatory: false,
415            }),
416        ]),
417        b'X' => BorrowedFormatItem::Compound(&[
418            component!(Hour {
419                padding: modifier::Padding::Zero,
420                is_12_hour_clock: false,
421            }),
422            BorrowedFormatItem::Literal(b":"),
423            component!(Minute {
424                padding: modifier::Padding::Zero,
425            }),
426            BorrowedFormatItem::Literal(b":"),
427            component!(Second {
428                padding: modifier::Padding::Zero,
429            }),
430        ]),
431        b'y' => component!(Year {
432            padding: padding_or_default(*padding, modifier::Padding::Zero),
433            repr: modifier::YearRepr::LastTwo,
434            iso_week_based: false,
435            sign_is_mandatory: false,
436        }),
437        b'Y' => component!(Year {
438            padding: modifier::Padding::Zero,
439            repr: modifier::YearRepr::Full,
440            iso_week_based: false,
441            sign_is_mandatory: false,
442        }),
443        b'z' => BorrowedFormatItem::Compound(&[
444            component!(OffsetHour {
445                sign_is_mandatory: true,
446                padding: modifier::Padding::Zero,
447            }),
448            component!(OffsetMinute {
449                padding: modifier::Padding::Zero,
450            }),
451        ]),
452        b'Z' => {
453            return Err(Error {
454                _inner: unused(ErrorInner {
455                    _message: "unsupported component",
456                    _span: component.span,
457                }),
458                public: InvalidFormatDescription::NotSupported {
459                    what: "component",
460                    context: "",
461                    index: component.span.start.byte as _,
462                },
463            })
464        }
465        _ => {
466            return Err(Error {
467                _inner: unused(ErrorInner {
468                    _message: "invalid component",
469                    _span: component.span,
470                }),
471                public: InvalidFormatDescription::InvalidComponentName {
472                    name: String::from_utf8_lossy(&[*component]).into_owned(),
473                    index: component.span.start.byte as _,
474                },
475            })
476        }
477    })
478}