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#[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#[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 Default,
44 Spaces,
46 None,
48 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 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}