Skip to content

Commit e2bd1d1

Browse files
authored
Add support for lenient format strings (#1693)
1 parent 2c95b0a commit e2bd1d1

File tree

1 file changed

+131
-24
lines changed

1 file changed

+131
-24
lines changed

src/format/strftime.rs

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ pub struct StrftimeItems<'a> {
195195
/// If the current specifier is composed of multiple formatting items (e.g. `%+`),
196196
/// `queue` stores a slice of `Item`s that have to be returned one by one.
197197
queue: &'static [Item<'static>],
198+
lenient: bool,
198199
#[cfg(feature = "unstable-locales")]
199200
locale_str: &'a str,
200201
#[cfg(feature = "unstable-locales")]
@@ -227,15 +228,47 @@ impl<'a> StrftimeItems<'a> {
227228
/// ```
228229
#[must_use]
229230
pub const fn new(s: &'a str) -> StrftimeItems<'a> {
230-
{
231-
StrftimeItems {
232-
remainder: s,
233-
queue: &[],
234-
#[cfg(feature = "unstable-locales")]
235-
locale_str: "",
236-
#[cfg(feature = "unstable-locales")]
237-
locale: None,
238-
}
231+
StrftimeItems {
232+
remainder: s,
233+
queue: &[],
234+
lenient: false,
235+
#[cfg(feature = "unstable-locales")]
236+
locale_str: "",
237+
#[cfg(feature = "unstable-locales")]
238+
locale: None,
239+
}
240+
}
241+
242+
/// The same as [`StrftimeItems::new`], but returns [`Item::Literal`] instead of [`Item::Error`].
243+
///
244+
/// Useful for formatting according to potentially invalid format strings.
245+
///
246+
/// # Example
247+
///
248+
/// ```
249+
/// use chrono::format::*;
250+
///
251+
/// let strftime_parser = StrftimeItems::new_lenient("%Y-%Q"); // %Y: year, %Q: invalid
252+
///
253+
/// const ITEMS: &[Item<'static>] = &[
254+
/// Item::Numeric(Numeric::Year, Pad::Zero),
255+
/// Item::Literal("-"),
256+
/// Item::Literal("%"),
257+
/// Item::Literal("Q"),
258+
/// ];
259+
/// println!("{:?}", strftime_parser.clone().collect::<Vec<_>>());
260+
/// assert!(strftime_parser.eq(ITEMS.iter().cloned()));
261+
/// ```
262+
#[must_use]
263+
pub const fn new_lenient(s: &'a str) -> StrftimeItems<'a> {
264+
StrftimeItems {
265+
remainder: s,
266+
queue: &[],
267+
lenient: true,
268+
#[cfg(feature = "unstable-locales")]
269+
locale_str: "",
270+
#[cfg(feature = "unstable-locales")]
271+
locale: None,
239272
}
240273
}
241274

@@ -288,7 +321,13 @@ impl<'a> StrftimeItems<'a> {
288321
#[cfg(feature = "unstable-locales")]
289322
#[must_use]
290323
pub const fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> {
291-
StrftimeItems { remainder: s, queue: &[], locale_str: "", locale: Some(locale) }
324+
StrftimeItems {
325+
remainder: s,
326+
queue: &[],
327+
lenient: false,
328+
locale_str: "",
329+
locale: Some(locale),
330+
}
292331
}
293332

294333
/// Parse format string into a `Vec` of formatting [`Item`]'s.
@@ -310,7 +349,7 @@ impl<'a> StrftimeItems<'a> {
310349
/// # Errors
311350
///
312351
/// Returns an error if the format string contains an invalid or unrecognized formatting
313-
/// specifier.
352+
/// specifier and the [`StrftimeItems`] wasn't constructed with [`new_lenient`][Self::new_lenient].
314353
///
315354
/// # Example
316355
///
@@ -354,7 +393,7 @@ impl<'a> StrftimeItems<'a> {
354393
/// # Errors
355394
///
356395
/// Returns an error if the format string contains an invalid or unrecognized formatting
357-
/// specifier.
396+
/// specifier and the [`StrftimeItems`] wasn't constructed with [`new_lenient`][Self::new_lenient].
358397
///
359398
/// # Example
360399
///
@@ -416,6 +455,22 @@ impl<'a> Iterator for StrftimeItems<'a> {
416455
}
417456

418457
impl<'a> StrftimeItems<'a> {
458+
fn error<'b>(
459+
&mut self,
460+
original: &'b str,
461+
error_len: &mut usize,
462+
ch: Option<char>,
463+
) -> (&'b str, Item<'b>) {
464+
if !self.lenient {
465+
return (&original[*error_len..], Item::Error);
466+
}
467+
468+
if let Some(c) = ch {
469+
*error_len -= c.len_utf8();
470+
}
471+
(&original[*error_len..], Item::Literal(&original[..*error_len]))
472+
}
473+
419474
fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> {
420475
use InternalInternal::*;
421476
use Item::{Literal, Space};
@@ -456,16 +511,24 @@ impl<'a> StrftimeItems<'a> {
456511

457512
// the next item is a specifier
458513
Some('%') => {
514+
let original = remainder;
459515
remainder = &remainder[1..];
516+
let mut error_len = 0;
517+
if self.lenient {
518+
error_len += 1;
519+
}
460520

461521
macro_rules! next {
462522
() => {
463523
match remainder.chars().next() {
464524
Some(x) => {
465525
remainder = &remainder[x.len_utf8()..];
526+
if self.lenient {
527+
error_len += x.len_utf8();
528+
}
466529
x
467530
}
468-
None => return Some((remainder, Item::Error)), // premature end of string
531+
None => return Some(self.error(original, &mut error_len, None)), // premature end of string
469532
}
470533
};
471534
}
@@ -480,7 +543,7 @@ impl<'a> StrftimeItems<'a> {
480543
let is_alternate = spec == '#';
481544
let spec = if pad_override.is_some() || is_alternate { next!() } else { spec };
482545
if is_alternate && !HAVE_ALTERNATES.contains(spec) {
483-
return Some((remainder, Item::Error));
546+
return Some(self.error(original, &mut error_len, Some(spec)));
484547
}
485548

486549
macro_rules! queue {
@@ -592,39 +655,71 @@ impl<'a> StrftimeItems<'a> {
592655
remainder = &remainder[1..];
593656
fixed(Fixed::TimezoneOffsetColon)
594657
} else {
595-
Item::Error
658+
self.error(original, &mut error_len, None).1
596659
}
597660
}
598661
'.' => match next!() {
599662
'3' => match next!() {
600663
'f' => fixed(Fixed::Nanosecond3),
601-
_ => Item::Error,
664+
c => {
665+
let res = self.error(original, &mut error_len, Some(c));
666+
remainder = res.0;
667+
res.1
668+
}
602669
},
603670
'6' => match next!() {
604671
'f' => fixed(Fixed::Nanosecond6),
605-
_ => Item::Error,
672+
c => {
673+
let res = self.error(original, &mut error_len, Some(c));
674+
remainder = res.0;
675+
res.1
676+
}
606677
},
607678
'9' => match next!() {
608679
'f' => fixed(Fixed::Nanosecond9),
609-
_ => Item::Error,
680+
c => {
681+
let res = self.error(original, &mut error_len, Some(c));
682+
remainder = res.0;
683+
res.1
684+
}
610685
},
611686
'f' => fixed(Fixed::Nanosecond),
612-
_ => Item::Error,
687+
c => {
688+
let res = self.error(original, &mut error_len, Some(c));
689+
remainder = res.0;
690+
res.1
691+
}
613692
},
614693
'3' => match next!() {
615694
'f' => internal_fixed(Nanosecond3NoDot),
616-
_ => Item::Error,
695+
c => {
696+
let res = self.error(original, &mut error_len, Some(c));
697+
remainder = res.0;
698+
res.1
699+
}
617700
},
618701
'6' => match next!() {
619702
'f' => internal_fixed(Nanosecond6NoDot),
620-
_ => Item::Error,
703+
c => {
704+
let res = self.error(original, &mut error_len, Some(c));
705+
remainder = res.0;
706+
res.1
707+
}
621708
},
622709
'9' => match next!() {
623710
'f' => internal_fixed(Nanosecond9NoDot),
624-
_ => Item::Error,
711+
c => {
712+
let res = self.error(original, &mut error_len, Some(c));
713+
remainder = res.0;
714+
res.1
715+
}
625716
},
626717
'%' => Literal("%"),
627-
_ => Item::Error, // no such specifier
718+
c => {
719+
let res = self.error(original, &mut error_len, Some(c));
720+
remainder = res.0;
721+
res.1
722+
}
628723
};
629724

630725
// Adjust `item` if we have any padding modifier.
@@ -635,7 +730,7 @@ impl<'a> StrftimeItems<'a> {
635730
Item::Numeric(ref kind, _pad) if self.queue.is_empty() => {
636731
Some((remainder, Item::Numeric(kind.clone(), new_pad)))
637732
}
638-
_ => Some((remainder, Item::Error)),
733+
_ => Some(self.error(original, &mut error_len, None)),
639734
}
640735
} else {
641736
Some((remainder, item))
@@ -1139,4 +1234,16 @@ mod tests {
11391234
let dt = Utc.with_ymd_and_hms(2014, 5, 7, 12, 34, 56).unwrap();
11401235
assert_eq!(&dt.format_with_items(fmt_items.iter()).to_string(), "2014-05-07T12:34:56+0000");
11411236
}
1237+
1238+
#[test]
1239+
#[cfg(any(feature = "alloc", feature = "std"))]
1240+
fn test_strftime_parse_lenient() {
1241+
let fmt_str = StrftimeItems::new_lenient("%Y-%m-%dT%H:%M:%S%z%Q%.2f%%%");
1242+
let fmt_items = fmt_str.parse().unwrap();
1243+
let dt = Utc.with_ymd_and_hms(2014, 5, 7, 12, 34, 56).unwrap();
1244+
assert_eq!(
1245+
&dt.format_with_items(fmt_items.iter()).to_string(),
1246+
"2014-05-07T12:34:56+0000%Q%.2f%%"
1247+
);
1248+
}
11421249
}

0 commit comments

Comments
 (0)