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, component, advance) = match self.input.get(1) {
79 Some(&b'_') => (Some(Padding::Space), self.input[2], 3),
80 Some(&b'-') => (Some(Padding::None), self.input[2], 3),
81 Some(&b'0') => (Some(Padding::Zero), self.input[2], 3),
82 Some(_) => (None, self.input[1], 2),
83 _ => {
84 return Some(Err(error_expected_end(Location {
85 byte: self.byte_pos,
86 })));
87 }
88 };
89
90 let component_loc = Location {
91 byte: self.byte_pos + (advance - 1) as u32,
92 };
93 self.input = &self.input[advance..];
94 self.byte_pos += advance as u32;
95 Some(parse_component(
96 padding,
97 component.spanned(component_loc.to_self()),
98 ))
99 }
100}
101
102#[cold]
103fn error_expected_end(location: Location) -> Error {
104 Error {
105 _inner: unused(location.error("unexpected end of input")),
106 public: InvalidFormatDescription::Expected {
107 what: "valid escape sequence",
108 index: location.byte as usize,
109 },
110 }
111}
112
113#[cold]
114fn error_unsupported_modifier(component: Spanned<u8>) -> Error {
115 Error {
116 _inner: unused(ErrorInner {
117 _message: "unsupported modifier",
118 _span: component.span,
119 }),
120 public: InvalidFormatDescription::NotSupported {
121 what: "modifier",
122 context: "",
123 index: component.span.start.byte as usize,
124 },
125 }
126}
127
128#[cold]
129fn error_unsupported_component(component: Spanned<u8>) -> Error {
130 Error {
131 _inner: unused(ErrorInner {
132 _message: "unsupported component",
133 _span: component.span,
134 }),
135 public: InvalidFormatDescription::NotSupported {
136 what: "component",
137 context: "",
138 index: component.span.start.byte as usize,
139 },
140 }
141}
142
143#[cold]
144fn error_invalid_component(component: Spanned<u8>) -> Error {
145 let name = if component.is_ascii() {
146 unsafe { String::from_utf8_unchecked(Vec::from([*component])) }
149 } else {
150 String::from(char::REPLACEMENT_CHARACTER)
151 };
152
153 Error {
154 _inner: unused(ErrorInner {
155 _message: "invalid component",
156 _span: component.span,
157 }),
158 public: InvalidFormatDescription::InvalidComponentName {
159 name,
160 index: component.span.start.byte as usize,
161 },
162 }
163}
164
165#[inline]
166fn parse_component(
167 padding: Option<Padding>,
168 component: Spanned<u8>,
169) -> Result<BorrowedFormatItem<'static>, Error> {
170 macro_rules! component {
172 ($name:ident { $($inner:tt)* }) => {
173 BorrowedFormatItem::Component(Component::$name(modifier::$name {
174 $($inner)*
175 }))
176 }
177 }
178
179 Ok(match *component {
180 b'%' => BorrowedFormatItem::StringLiteral("%"),
181 b'a' => component!(WeekdayShort {
182 case_sensitive: true
183 }),
184 b'A' => component!(WeekdayLong {
185 case_sensitive: true,
186 }),
187 b'b' | b'h' => component!(MonthShort {
188 case_sensitive: true,
189 }),
190 b'B' => component!(MonthLong {
191 case_sensitive: true,
192 }),
193 b'c' => BorrowedFormatItem::Compound(&[
194 component!(WeekdayShort {
195 case_sensitive: true,
196 }),
197 BorrowedFormatItem::StringLiteral(" "),
198 component!(MonthShort {
199 case_sensitive: true,
200 }),
201 BorrowedFormatItem::StringLiteral(" "),
202 component!(Day {
203 padding: Padding::Space
204 }),
205 BorrowedFormatItem::StringLiteral(" "),
206 component!(Hour24 {
207 padding: Padding::Zero,
208 }),
209 BorrowedFormatItem::StringLiteral(":"),
210 component!(Minute {
211 padding: Padding::Zero,
212 }),
213 BorrowedFormatItem::StringLiteral(":"),
214 component!(Second {
215 padding: Padding::Zero,
216 }),
217 BorrowedFormatItem::StringLiteral(" "),
218 #[cfg(feature = "large-dates")]
219 component!(CalendarYearFullExtendedRange {
220 padding: Padding::Zero,
221 sign_is_mandatory: false,
222 }),
223 #[cfg(not(feature = "large-dates"))]
224 component!(CalendarYearFullStandardRange {
225 padding: Padding::Zero,
226 sign_is_mandatory: false,
227 }),
228 ]),
229 #[cfg(feature = "large-dates")]
230 b'C' => component!(CalendarYearCenturyExtendedRange {
231 padding: padding.unwrap_or(Padding::Zero),
232 sign_is_mandatory: false,
233 }),
234 #[cfg(not(feature = "large-dates"))]
235 b'C' => component!(CalendarYearCenturyStandardRange {
236 padding: padding.unwrap_or(Padding::Zero),
237 sign_is_mandatory: false,
238 }),
239 b'd' => component!(Day {
240 padding: padding.unwrap_or(Padding::Zero),
241 }),
242 b'D' => BorrowedFormatItem::Compound(&[
243 component!(MonthNumerical {
244 padding: Padding::Zero,
245 }),
246 BorrowedFormatItem::StringLiteral("/"),
247 component!(Day {
248 padding: Padding::Zero,
249 }),
250 BorrowedFormatItem::StringLiteral("/"),
251 component!(CalendarYearLastTwo {
252 padding: Padding::Zero,
253 }),
254 ]),
255 b'e' => component!(Day {
256 padding: padding.unwrap_or(Padding::Space),
257 }),
258 b'F' => BorrowedFormatItem::Compound(&[
259 #[cfg(feature = "large-dates")]
260 component!(CalendarYearFullExtendedRange {
261 padding: Padding::Zero,
262 sign_is_mandatory: false,
263 }),
264 #[cfg(not(feature = "large-dates"))]
265 component!(CalendarYearFullStandardRange {
266 padding: Padding::Zero,
267 sign_is_mandatory: false,
268 }),
269 BorrowedFormatItem::StringLiteral("-"),
270 component!(MonthNumerical {
271 padding: Padding::Zero,
272 }),
273 BorrowedFormatItem::StringLiteral("-"),
274 component!(Day {
275 padding: Padding::Zero,
276 }),
277 ]),
278 b'g' => component!(IsoYearLastTwo {
279 padding: padding.unwrap_or(Padding::Zero),
280 }),
281 #[cfg(feature = "large-dates")]
282 b'G' => component!(IsoYearFullExtendedRange {
283 padding: Padding::Zero,
284 sign_is_mandatory: false,
285 }),
286 #[cfg(not(feature = "large-dates"))]
287 b'G' => component!(IsoYearFullStandardRange {
288 padding: Padding::Zero,
289 sign_is_mandatory: false,
290 }),
291 b'H' => component!(Hour24 {
292 padding: padding.unwrap_or(Padding::Zero),
293 }),
294 b'I' => component!(Hour12 {
295 padding: padding.unwrap_or(Padding::Zero),
296 }),
297 b'j' => component!(Ordinal {
298 padding: padding.unwrap_or(Padding::Zero),
299 }),
300 b'k' => component!(Hour24 {
301 padding: padding.unwrap_or(Padding::Space),
302 }),
303 b'l' => component!(Hour12 {
304 padding: padding.unwrap_or(Padding::Space),
305 }),
306 b'm' => component!(MonthNumerical {
307 padding: padding.unwrap_or(Padding::Zero),
308 }),
309 b'M' => component!(Minute {
310 padding: padding.unwrap_or(Padding::Zero),
311 }),
312 b'n' => BorrowedFormatItem::StringLiteral("\n"),
313 b'O' => return Err(error_unsupported_modifier(component)),
314 b'p' => component!(Period {
315 is_uppercase: true,
316 case_sensitive: true
317 }),
318 b'P' => component!(Period {
319 is_uppercase: false,
320 case_sensitive: true
321 }),
322 b'r' => BorrowedFormatItem::Compound(&[
323 component!(Hour12 {
324 padding: Padding::Zero,
325 }),
326 BorrowedFormatItem::StringLiteral(":"),
327 component!(Minute {
328 padding: Padding::Zero,
329 }),
330 BorrowedFormatItem::StringLiteral(":"),
331 component!(Second {
332 padding: Padding::Zero,
333 }),
334 BorrowedFormatItem::StringLiteral(" "),
335 component!(Period {
336 is_uppercase: true,
337 case_sensitive: true,
338 }),
339 ]),
340 b'R' => BorrowedFormatItem::Compound(&[
341 component!(Hour24 {
342 padding: Padding::Zero,
343 }),
344 BorrowedFormatItem::StringLiteral(":"),
345 component!(Minute {
346 padding: Padding::Zero,
347 }),
348 ]),
349 b's' => component!(UnixTimestampSecond {
350 sign_is_mandatory: false,
351 }),
352 b'S' => component!(Second {
353 padding: padding.unwrap_or(Padding::Zero),
354 }),
355 b't' => BorrowedFormatItem::StringLiteral("\t"),
356 b'T' => BorrowedFormatItem::Compound(&[
357 component!(Hour24 {
358 padding: Padding::Zero,
359 }),
360 BorrowedFormatItem::StringLiteral(":"),
361 component!(Minute {
362 padding: Padding::Zero,
363 }),
364 BorrowedFormatItem::StringLiteral(":"),
365 component!(Second {
366 padding: Padding::Zero,
367 }),
368 ]),
369 b'u' => component!(WeekdayMonday { one_indexed: true }),
370 b'U' => component!(WeekNumberSunday {
371 padding: padding.unwrap_or(Padding::Zero),
372 }),
373 b'V' => component!(WeekNumberIso {
374 padding: padding.unwrap_or(Padding::Zero),
375 }),
376 b'w' => component!(WeekdaySunday { one_indexed: true }),
377 b'W' => component!(WeekNumberMonday {
378 padding: padding.unwrap_or(Padding::Zero),
379 }),
380 b'x' => BorrowedFormatItem::Compound(&[
381 component!(MonthNumerical {
382 padding: Padding::Zero,
383 }),
384 BorrowedFormatItem::StringLiteral("/"),
385 component!(Day {
386 padding: Padding::Zero
387 }),
388 BorrowedFormatItem::StringLiteral("/"),
389 component!(CalendarYearLastTwo {
390 padding: Padding::Zero,
391 }),
392 ]),
393 b'X' => BorrowedFormatItem::Compound(&[
394 component!(Hour24 {
395 padding: Padding::Zero,
396 }),
397 BorrowedFormatItem::StringLiteral(":"),
398 component!(Minute {
399 padding: Padding::Zero,
400 }),
401 BorrowedFormatItem::StringLiteral(":"),
402 component!(Second {
403 padding: Padding::Zero,
404 }),
405 ]),
406 b'y' => component!(CalendarYearLastTwo {
407 padding: padding.unwrap_or(Padding::Zero),
408 }),
409 #[cfg(feature = "large-dates")]
410 b'Y' => component!(CalendarYearFullExtendedRange {
411 padding: Padding::Zero,
412 sign_is_mandatory: false,
413 }),
414 #[cfg(not(feature = "large-dates"))]
415 b'Y' => component!(CalendarYearFullStandardRange {
416 padding: Padding::Zero,
417 sign_is_mandatory: false,
418 }),
419 b'z' => BorrowedFormatItem::Compound(&[
420 component!(OffsetHour {
421 sign_is_mandatory: true,
422 padding: Padding::Zero,
423 }),
424 component!(OffsetMinute {
425 padding: Padding::Zero,
426 }),
427 ]),
428 b'Z' => return Err(error_unsupported_component(component)),
429 _ => return Err(error_invalid_component(component)),
430 })
431}