Skip to main content

time/format_description/parse/
strftime.rs

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