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