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 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}