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::StringLiteral(unsafe {
139 str::from_utf8_unchecked(*spanned)
140 })),
141 Token::Component {
142 _percent,
143 padding,
144 component,
145 } => parse_component(padding, component),
146 })
147 })
148}
149
150fn parse_component(
151 padding: Spanned<Padding>,
152 component: Spanned<u8>,
153) -> Result<BorrowedFormatItem<'static>, Error> {
154 let padding_or_default = |padding: Padding, default| match padding {
155 Padding::Default => default,
156 Padding::Spaces => modifier::Padding::Space,
157 Padding::None => modifier::Padding::None,
158 Padding::Zeroes => modifier::Padding::Zero,
159 };
160
161 macro_rules! component {
163 ($name:ident { $($inner:tt)* }) => {
164 BorrowedFormatItem::Component(Component::$name(modifier::$name {
165 $($inner)*
166 }))
167 }
168 }
169
170 Ok(match *component {
171 b'%' => BorrowedFormatItem::StringLiteral("%"),
172 b'a' => component!(WeekdayShort {
173 case_sensitive: true
174 }),
175 b'A' => component!(WeekdayLong {
176 case_sensitive: true,
177 }),
178 b'b' | b'h' => component!(MonthShort {
179 case_sensitive: true,
180 }),
181 b'B' => component!(MonthLong {
182 case_sensitive: true,
183 }),
184 b'c' => BorrowedFormatItem::Compound(&[
185 component!(WeekdayShort {
186 case_sensitive: true,
187 }),
188 BorrowedFormatItem::StringLiteral(" "),
189 component!(MonthShort {
190 case_sensitive: true,
191 }),
192 BorrowedFormatItem::StringLiteral(" "),
193 component!(Day {
194 padding: modifier::Padding::Space
195 }),
196 BorrowedFormatItem::StringLiteral(" "),
197 component!(Hour24 {
198 padding: modifier::Padding::Zero,
199 }),
200 BorrowedFormatItem::StringLiteral(":"),
201 component!(Minute {
202 padding: modifier::Padding::Zero,
203 }),
204 BorrowedFormatItem::StringLiteral(":"),
205 component!(Second {
206 padding: modifier::Padding::Zero,
207 }),
208 BorrowedFormatItem::StringLiteral(" "),
209 #[cfg(feature = "large-dates")]
210 component!(CalendarYearFullExtendedRange {
211 padding: modifier::Padding::Zero,
212 sign_is_mandatory: false,
213 }),
214 #[cfg(not(feature = "large-dates"))]
215 component!(CalendarYearFullStandardRange {
216 padding: modifier::Padding::Zero,
217 sign_is_mandatory: false,
218 }),
219 ]),
220 #[cfg(feature = "large-dates")]
221 b'C' => component!(CalendarYearCenturyExtendedRange {
222 padding: padding_or_default(*padding, modifier::Padding::Zero),
223 sign_is_mandatory: false,
224 }),
225 #[cfg(not(feature = "large-dates"))]
226 b'C' => component!(CalendarYearCenturyStandardRange {
227 padding: padding_or_default(*padding, modifier::Padding::Zero),
228 sign_is_mandatory: false,
229 }),
230 b'd' => component!(Day {
231 padding: padding_or_default(*padding, modifier::Padding::Zero),
232 }),
233 b'D' => BorrowedFormatItem::Compound(&[
234 component!(MonthNumerical {
235 padding: modifier::Padding::Zero,
236 }),
237 BorrowedFormatItem::StringLiteral("/"),
238 component!(Day {
239 padding: modifier::Padding::Zero,
240 }),
241 BorrowedFormatItem::StringLiteral("/"),
242 component!(CalendarYearLastTwo {
243 padding: modifier::Padding::Zero,
244 }),
245 ]),
246 b'e' => component!(Day {
247 padding: padding_or_default(*padding, modifier::Padding::Space),
248 }),
249 b'F' => BorrowedFormatItem::Compound(&[
250 #[cfg(feature = "large-dates")]
251 component!(CalendarYearFullExtendedRange {
252 padding: modifier::Padding::Zero,
253 sign_is_mandatory: false,
254 }),
255 #[cfg(not(feature = "large-dates"))]
256 component!(CalendarYearFullStandardRange {
257 padding: modifier::Padding::Zero,
258 sign_is_mandatory: false,
259 }),
260 BorrowedFormatItem::StringLiteral("-"),
261 component!(MonthNumerical {
262 padding: modifier::Padding::Zero,
263 }),
264 BorrowedFormatItem::StringLiteral("-"),
265 component!(Day {
266 padding: modifier::Padding::Zero,
267 }),
268 ]),
269 b'g' => component!(IsoYearLastTwo {
270 padding: padding_or_default(*padding, modifier::Padding::Zero),
271 }),
272 #[cfg(feature = "large-dates")]
273 b'G' => component!(IsoYearFullExtendedRange {
274 padding: modifier::Padding::Zero,
275 sign_is_mandatory: false,
276 }),
277 #[cfg(not(feature = "large-dates"))]
278 b'G' => component!(IsoYearFullStandardRange {
279 padding: modifier::Padding::Zero,
280 sign_is_mandatory: false,
281 }),
282 b'H' => component!(Hour24 {
283 padding: padding_or_default(*padding, modifier::Padding::Zero),
284 }),
285 b'I' => component!(Hour12 {
286 padding: padding_or_default(*padding, modifier::Padding::Zero),
287 }),
288 b'j' => component!(Ordinal {
289 padding: padding_or_default(*padding, modifier::Padding::Zero),
290 }),
291 b'k' => component!(Hour24 {
292 padding: padding_or_default(*padding, modifier::Padding::Space),
293 }),
294 b'l' => component!(Hour12 {
295 padding: padding_or_default(*padding, modifier::Padding::Space),
296 }),
297 b'm' => component!(MonthNumerical {
298 padding: padding_or_default(*padding, modifier::Padding::Zero),
299 }),
300 b'M' => component!(Minute {
301 padding: padding_or_default(*padding, modifier::Padding::Zero),
302 }),
303 b'n' => BorrowedFormatItem::StringLiteral("\n"),
304 b'O' => {
305 return Err(Error {
306 _inner: unused(ErrorInner {
307 _message: "unsupported modifier",
308 _span: component.span,
309 }),
310 public: InvalidFormatDescription::NotSupported {
311 what: "modifier",
312 context: "",
313 index: component.span.start.byte as usize,
314 },
315 });
316 }
317 b'p' => component!(Period {
318 is_uppercase: true,
319 case_sensitive: true
320 }),
321 b'P' => component!(Period {
322 is_uppercase: false,
323 case_sensitive: true
324 }),
325 b'r' => BorrowedFormatItem::Compound(&[
326 component!(Hour12 {
327 padding: modifier::Padding::Zero,
328 }),
329 BorrowedFormatItem::StringLiteral(":"),
330 component!(Minute {
331 padding: modifier::Padding::Zero,
332 }),
333 BorrowedFormatItem::StringLiteral(":"),
334 component!(Second {
335 padding: modifier::Padding::Zero,
336 }),
337 BorrowedFormatItem::StringLiteral(" "),
338 component!(Period {
339 is_uppercase: true,
340 case_sensitive: true,
341 }),
342 ]),
343 b'R' => BorrowedFormatItem::Compound(&[
344 component!(Hour24 {
345 padding: modifier::Padding::Zero,
346 }),
347 BorrowedFormatItem::StringLiteral(":"),
348 component!(Minute {
349 padding: modifier::Padding::Zero,
350 }),
351 ]),
352 b's' => component!(UnixTimestampSecond {
353 sign_is_mandatory: false,
354 }),
355 b'S' => component!(Second {
356 padding: padding_or_default(*padding, modifier::Padding::Zero),
357 }),
358 b't' => BorrowedFormatItem::StringLiteral("\t"),
359 b'T' => BorrowedFormatItem::Compound(&[
360 component!(Hour24 {
361 padding: modifier::Padding::Zero,
362 }),
363 BorrowedFormatItem::StringLiteral(":"),
364 component!(Minute {
365 padding: modifier::Padding::Zero,
366 }),
367 BorrowedFormatItem::StringLiteral(":"),
368 component!(Second {
369 padding: modifier::Padding::Zero,
370 }),
371 ]),
372 b'u' => component!(WeekdayMonday { one_indexed: true }),
373 b'U' => component!(WeekNumberSunday {
374 padding: padding_or_default(*padding, modifier::Padding::Zero),
375 }),
376 b'V' => component!(WeekNumberIso {
377 padding: padding_or_default(*padding, modifier::Padding::Zero),
378 }),
379 b'w' => component!(WeekdaySunday { one_indexed: true }),
380 b'W' => component!(WeekNumberMonday {
381 padding: padding_or_default(*padding, modifier::Padding::Zero),
382 }),
383 b'x' => BorrowedFormatItem::Compound(&[
384 component!(MonthNumerical {
385 padding: modifier::Padding::Zero,
386 }),
387 BorrowedFormatItem::StringLiteral("/"),
388 component!(Day {
389 padding: modifier::Padding::Zero
390 }),
391 BorrowedFormatItem::StringLiteral("/"),
392 component!(CalendarYearLastTwo {
393 padding: modifier::Padding::Zero,
394 }),
395 ]),
396 b'X' => BorrowedFormatItem::Compound(&[
397 component!(Hour24 {
398 padding: modifier::Padding::Zero,
399 }),
400 BorrowedFormatItem::StringLiteral(":"),
401 component!(Minute {
402 padding: modifier::Padding::Zero,
403 }),
404 BorrowedFormatItem::StringLiteral(":"),
405 component!(Second {
406 padding: modifier::Padding::Zero,
407 }),
408 ]),
409 b'y' => component!(CalendarYearLastTwo {
410 padding: padding_or_default(*padding, modifier::Padding::Zero),
411 }),
412 #[cfg(feature = "large-dates")]
413 b'Y' => component!(CalendarYearFullExtendedRange {
414 padding: modifier::Padding::Zero,
415 sign_is_mandatory: false,
416 }),
417 #[cfg(not(feature = "large-dates"))]
418 b'Y' => component!(CalendarYearFullStandardRange {
419 padding: modifier::Padding::Zero,
420 sign_is_mandatory: false,
421 }),
422 b'z' => BorrowedFormatItem::Compound(&[
423 component!(OffsetHour {
424 sign_is_mandatory: true,
425 padding: modifier::Padding::Zero,
426 }),
427 component!(OffsetMinute {
428 padding: modifier::Padding::Zero,
429 }),
430 ]),
431 b'Z' => {
432 return Err(Error {
433 _inner: unused(ErrorInner {
434 _message: "unsupported component",
435 _span: component.span,
436 }),
437 public: InvalidFormatDescription::NotSupported {
438 what: "component",
439 context: "",
440 index: component.span.start.byte as usize,
441 },
442 });
443 }
444 _ => {
445 return Err(Error {
446 _inner: unused(ErrorInner {
447 _message: "invalid component",
448 _span: component.span,
449 }),
450 public: InvalidFormatDescription::InvalidComponentName {
451 name: String::from_utf8_lossy(&[*component]).into_owned(),
452 index: component.span.start.byte as usize,
453 },
454 });
455 }
456 })
457}