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#[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, 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 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}