1use alloc::string::String;
2use alloc::vec::Vec;
3
4use crate::error::InvalidFormatDescription;
5use crate::format_description::modifier::Padding;
6use crate::format_description::parse::{
7 Error, ErrorInner, Location, Spanned, SpannedValue, unused,
8};
9use crate::format_description::{BorrowedFormatItem, Component, OwnedFormatItem, modifier};
10use crate::internal_macros::try_likely_ok;
11
12#[doc(alias = "parse_strptime_borrowed")]
19#[inline]
20pub fn parse_strftime_borrowed(
21 s: &str,
22) -> Result<Vec<BorrowedFormatItem<'_>>, InvalidFormatDescription> {
23 let mut items = Vec::with_capacity(s.bytes().filter(|&b| b == b'%').count().saturating_add(2));
24 for item in Tokenizer::new(s.as_bytes()) {
25 items.push(try_likely_ok!(item));
26 }
27 Ok(items)
28}
29
30#[doc(alias = "parse_strptime_owned")]
36#[inline]
37pub fn parse_strftime_owned(s: &str) -> Result<OwnedFormatItem, InvalidFormatDescription> {
38 parse_strftime_borrowed(s).map(Into::into)
39}
40
41struct Tokenizer<'input> {
42 input: &'input [u8],
43 byte_pos: u32,
44}
45
46impl Tokenizer<'_> {
47 #[inline]
48 const fn new(input: &[u8]) -> Tokenizer<'_> {
49 Tokenizer { input, byte_pos: 0 }
50 }
51}
52
53impl<'input> Iterator for Tokenizer<'input> {
54 type Item = Result<BorrowedFormatItem<'input>, Error>;
55
56 #[inline]
57 fn next(&mut self) -> Option<Self::Item> {
58 if self.input.is_empty() {
59 return None;
60 }
61
62 if self.input[0] != b'%' {
63 let bytes = self
64 .input
65 .iter()
66 .position(|&b| b == b'%')
67 .unwrap_or(self.input.len()) as u32;
68
69 let value = unsafe { str::from_utf8_unchecked(&self.input[..bytes as usize]) };
72 self.input = &self.input[bytes as usize..];
73 self.byte_pos += bytes;
74
75 return Some(Ok(BorrowedFormatItem::StringLiteral(value)));
76 }
77
78 let padding = match self.input.get(1) {
79 Some(&b'_') => Some(Padding::Space),
80 Some(&b'-') => Some(Padding::None),
81 Some(&b'0') => Some(Padding::Zero),
82 Some(_) => None,
83 None => {
84 return Some(Err(error_expected_end(Location {
85 byte: self.byte_pos,
86 })));
87 }
88 };
89
90 let (component, advance) = match (padding, self.input.get(2)) {
91 (Some(_), Some(&component)) => (component, 3),
92 (Some(_), None) => {
93 return Some(Err(error_expected_end(Location {
94 byte: self.byte_pos + 2,
95 })));
96 }
97 (None, _) => (self.input[1], 2),
98 };
99
100 let component_loc = Location {
101 byte: self.byte_pos + (advance - 1) as u32,
102 };
103 self.input = &self.input[advance..];
104 self.byte_pos += advance as u32;
105 Some(parse_component(
106 padding,
107 component.spanned(component_loc.to_self()),
108 ))
109 }
110}
111
112#[cold]
113fn error_expected_end(location: Location) -> Error {
114 Error {
115 _inner: unused(location.error("unexpected end of input")),
116 public: InvalidFormatDescription::Expected {
117 what: "valid escape sequence",
118 index: location.byte as usize,
119 },
120 }
121}
122
123#[cold]
124fn error_unsupported_modifier(component: Spanned<u8>) -> Error {
125 Error {
126 _inner: unused(ErrorInner {
127 _message: "unsupported modifier",
128 _span: component.span,
129 }),
130 public: InvalidFormatDescription::NotSupported {
131 what: "modifier",
132 context: "",
133 index: component.span.start.byte as usize,
134 },
135 }
136}
137
138#[cold]
139fn error_unsupported_component(component: Spanned<u8>) -> Error {
140 Error {
141 _inner: unused(ErrorInner {
142 _message: "unsupported component",
143 _span: component.span,
144 }),
145 public: InvalidFormatDescription::NotSupported {
146 what: "component",
147 context: "",
148 index: component.span.start.byte as usize,
149 },
150 }
151}
152
153#[cold]
154fn error_invalid_component(component: Spanned<u8>) -> Error {
155 let name = if component.is_ascii() {
156 unsafe { String::from_utf8_unchecked(Vec::from([*component])) }
159 } else {
160 String::from(char::REPLACEMENT_CHARACTER)
161 };
162
163 Error {
164 _inner: unused(ErrorInner {
165 _message: "invalid component",
166 _span: component.span,
167 }),
168 public: InvalidFormatDescription::InvalidComponentName {
169 name,
170 index: component.span.start.byte as usize,
171 },
172 }
173}
174
175#[inline]
176fn parse_component(
177 padding: Option<Padding>,
178 component: Spanned<u8>,
179) -> Result<BorrowedFormatItem<'static>, Error> {
180 macro_rules! component {
182 ($name:ident { $($inner:tt)* }) => {
183 BorrowedFormatItem::Component(Component::$name(modifier::$name {
184 $($inner)*
185 }))
186 }
187 }
188
189 Ok(match *component {
190 b'%' => BorrowedFormatItem::StringLiteral("%"),
191 b'a' => component!(WeekdayShort {
192 case_sensitive: true
193 }),
194 b'A' => component!(WeekdayLong {
195 case_sensitive: true,
196 }),
197 b'b' | b'h' => component!(MonthShort {
198 case_sensitive: true,
199 }),
200 b'B' => component!(MonthLong {
201 case_sensitive: true,
202 }),
203 b'c' => BorrowedFormatItem::Compound(&[
204 component!(WeekdayShort {
205 case_sensitive: true,
206 }),
207 BorrowedFormatItem::StringLiteral(" "),
208 component!(MonthShort {
209 case_sensitive: true,
210 }),
211 BorrowedFormatItem::StringLiteral(" "),
212 component!(Day {
213 padding: Padding::Space
214 }),
215 BorrowedFormatItem::StringLiteral(" "),
216 component!(Hour24 {
217 padding: Padding::Zero,
218 }),
219 BorrowedFormatItem::StringLiteral(":"),
220 component!(Minute {
221 padding: Padding::Zero,
222 }),
223 BorrowedFormatItem::StringLiteral(":"),
224 component!(Second {
225 padding: Padding::Zero,
226 }),
227 BorrowedFormatItem::StringLiteral(" "),
228 #[cfg(feature = "large-dates")]
229 component!(CalendarYearFullExtendedRange {
230 padding: Padding::Zero,
231 sign_is_mandatory: false,
232 }),
233 #[cfg(not(feature = "large-dates"))]
234 component!(CalendarYearFullStandardRange {
235 padding: Padding::Zero,
236 sign_is_mandatory: false,
237 }),
238 ]),
239 #[cfg(feature = "large-dates")]
240 b'C' => component!(CalendarYearCenturyExtendedRange {
241 padding: padding.unwrap_or(Padding::Zero),
242 sign_is_mandatory: false,
243 }),
244 #[cfg(not(feature = "large-dates"))]
245 b'C' => component!(CalendarYearCenturyStandardRange {
246 padding: padding.unwrap_or(Padding::Zero),
247 sign_is_mandatory: false,
248 }),
249 b'd' => component!(Day {
250 padding: padding.unwrap_or(Padding::Zero),
251 }),
252 b'D' => BorrowedFormatItem::Compound(&[
253 component!(MonthNumerical {
254 padding: Padding::Zero,
255 }),
256 BorrowedFormatItem::StringLiteral("/"),
257 component!(Day {
258 padding: Padding::Zero,
259 }),
260 BorrowedFormatItem::StringLiteral("/"),
261 component!(CalendarYearLastTwo {
262 padding: Padding::Zero,
263 }),
264 ]),
265 b'e' => component!(Day {
266 padding: padding.unwrap_or(Padding::Space),
267 }),
268 b'F' => BorrowedFormatItem::Compound(&[
269 #[cfg(feature = "large-dates")]
270 component!(CalendarYearFullExtendedRange {
271 padding: Padding::Zero,
272 sign_is_mandatory: false,
273 }),
274 #[cfg(not(feature = "large-dates"))]
275 component!(CalendarYearFullStandardRange {
276 padding: Padding::Zero,
277 sign_is_mandatory: false,
278 }),
279 BorrowedFormatItem::StringLiteral("-"),
280 component!(MonthNumerical {
281 padding: Padding::Zero,
282 }),
283 BorrowedFormatItem::StringLiteral("-"),
284 component!(Day {
285 padding: Padding::Zero,
286 }),
287 ]),
288 b'g' => component!(IsoYearLastTwo {
289 padding: padding.unwrap_or(Padding::Zero),
290 }),
291 #[cfg(feature = "large-dates")]
292 b'G' => component!(IsoYearFullExtendedRange {
293 padding: Padding::Zero,
294 sign_is_mandatory: false,
295 }),
296 #[cfg(not(feature = "large-dates"))]
297 b'G' => component!(IsoYearFullStandardRange {
298 padding: Padding::Zero,
299 sign_is_mandatory: false,
300 }),
301 b'H' => component!(Hour24 {
302 padding: padding.unwrap_or(Padding::Zero),
303 }),
304 b'I' => component!(Hour12 {
305 padding: padding.unwrap_or(Padding::Zero),
306 }),
307 b'j' => component!(Ordinal {
308 padding: padding.unwrap_or(Padding::Zero),
309 }),
310 b'k' => component!(Hour24 {
311 padding: padding.unwrap_or(Padding::Space),
312 }),
313 b'l' => component!(Hour12 {
314 padding: padding.unwrap_or(Padding::Space),
315 }),
316 b'm' => component!(MonthNumerical {
317 padding: padding.unwrap_or(Padding::Zero),
318 }),
319 b'M' => component!(Minute {
320 padding: padding.unwrap_or(Padding::Zero),
321 }),
322 b'n' => BorrowedFormatItem::StringLiteral("\n"),
323 b'O' => return Err(error_unsupported_modifier(component)),
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!(Hour12 {
334 padding: Padding::Zero,
335 }),
336 BorrowedFormatItem::StringLiteral(":"),
337 component!(Minute {
338 padding: Padding::Zero,
339 }),
340 BorrowedFormatItem::StringLiteral(":"),
341 component!(Second {
342 padding: Padding::Zero,
343 }),
344 BorrowedFormatItem::StringLiteral(" "),
345 component!(Period {
346 is_uppercase: true,
347 case_sensitive: true,
348 }),
349 ]),
350 b'R' => BorrowedFormatItem::Compound(&[
351 component!(Hour24 {
352 padding: Padding::Zero,
353 }),
354 BorrowedFormatItem::StringLiteral(":"),
355 component!(Minute {
356 padding: Padding::Zero,
357 }),
358 ]),
359 b's' => component!(UnixTimestampSecond {
360 sign_is_mandatory: false,
361 }),
362 b'S' => component!(Second {
363 padding: padding.unwrap_or(Padding::Zero),
364 }),
365 b't' => BorrowedFormatItem::StringLiteral("\t"),
366 b'T' => BorrowedFormatItem::Compound(&[
367 component!(Hour24 {
368 padding: Padding::Zero,
369 }),
370 BorrowedFormatItem::StringLiteral(":"),
371 component!(Minute {
372 padding: Padding::Zero,
373 }),
374 BorrowedFormatItem::StringLiteral(":"),
375 component!(Second {
376 padding: Padding::Zero,
377 }),
378 ]),
379 b'u' => component!(WeekdayMonday { one_indexed: true }),
380 b'U' => component!(WeekNumberSunday {
381 padding: padding.unwrap_or(Padding::Zero),
382 }),
383 b'V' => component!(WeekNumberIso {
384 padding: padding.unwrap_or(Padding::Zero),
385 }),
386 b'w' => component!(WeekdaySunday { one_indexed: true }),
387 b'W' => component!(WeekNumberMonday {
388 padding: padding.unwrap_or(Padding::Zero),
389 }),
390 b'x' => BorrowedFormatItem::Compound(&[
391 component!(MonthNumerical {
392 padding: Padding::Zero,
393 }),
394 BorrowedFormatItem::StringLiteral("/"),
395 component!(Day {
396 padding: Padding::Zero
397 }),
398 BorrowedFormatItem::StringLiteral("/"),
399 component!(CalendarYearLastTwo {
400 padding: Padding::Zero,
401 }),
402 ]),
403 b'X' => BorrowedFormatItem::Compound(&[
404 component!(Hour24 {
405 padding: Padding::Zero,
406 }),
407 BorrowedFormatItem::StringLiteral(":"),
408 component!(Minute {
409 padding: Padding::Zero,
410 }),
411 BorrowedFormatItem::StringLiteral(":"),
412 component!(Second {
413 padding: Padding::Zero,
414 }),
415 ]),
416 b'y' => component!(CalendarYearLastTwo {
417 padding: padding.unwrap_or(Padding::Zero),
418 }),
419 #[cfg(feature = "large-dates")]
420 b'Y' => component!(CalendarYearFullExtendedRange {
421 padding: Padding::Zero,
422 sign_is_mandatory: false,
423 }),
424 #[cfg(not(feature = "large-dates"))]
425 b'Y' => component!(CalendarYearFullStandardRange {
426 padding: Padding::Zero,
427 sign_is_mandatory: false,
428 }),
429 b'z' => BorrowedFormatItem::Compound(&[
430 component!(OffsetHour {
431 sign_is_mandatory: true,
432 padding: Padding::Zero,
433 }),
434 component!(OffsetMinute {
435 padding: Padding::Zero,
436 }),
437 ]),
438 b'Z' => return Err(error_unsupported_component(component)),
439 _ => return Err(error_invalid_component(component)),
440 })
441}