#[cfg(feature = "unstable-locales")]
use super::{locales, Locale};
use super::{BAD_FORMAT, Fixed, InternalInternal, Item, Numeric, Pad, ParseError};
use super::{fixed, internal_fixed, num, num0, nums};
use std::vec::Vec;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct StrftimeItems<'a> {
remainder: &'a str,
queue: &'static [Item<'static>],
lenient: bool,
#[cfg(feature = "unstable-locales")]
locale_str: &'a str,
#[cfg(feature = "unstable-locales")]
locale: Option<Locale>,
}
impl<'a> StrftimeItems<'a> {
#[must_use]
pub const fn new(s: &'a str) -> StrftimeItems<'a> {
StrftimeItems {
remainder: s,
queue: &[],
lenient: false,
#[cfg(feature = "unstable-locales")]
locale_str: "",
#[cfg(feature = "unstable-locales")]
locale: None,
}
}
#[must_use]
pub const fn new_lenient(s: &'a str) -> StrftimeItems<'a> {
StrftimeItems {
remainder: s,
queue: &[],
lenient: true,
#[cfg(feature = "unstable-locales")]
locale_str: "",
#[cfg(feature = "unstable-locales")]
locale: None,
}
}
#[cfg(feature = "unstable-locales")]
#[must_use]
pub const fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> {
StrftimeItems { remainder: s, queue: &[], lenient: false, locale_str: "", locale: Some(locale) }
}
pub fn parse(self) -> Result<Vec<Item<'a>>, ParseError> {
self.into_iter()
.map(|item| match item == Item::Error {
false => Ok(item),
true => Err(BAD_FORMAT),
})
.collect()
}
pub fn parse_to_owned(self) -> Result<Vec<Item<'static>>, ParseError> {
self.into_iter()
.map(|item| match item == Item::Error {
false => Ok(item.to_owned()),
true => Err(BAD_FORMAT),
})
.collect()
}
fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> {
use InternalInternal::*;
use Item::{Literal, Space};
use Numeric::*;
let (original, mut remainder) = match remainder.chars().next()? {
'%' => (remainder, &remainder[1..]),
c if c.is_whitespace() => {
let nextspec =
remainder.find(|c: char| !c.is_whitespace()).unwrap_or(remainder.len());
assert!(nextspec > 0);
let item = Space(&remainder[..nextspec]);
remainder = &remainder[nextspec..];
return Some((remainder, item));
}
_ => {
let nextspec = remainder
.find(|c: char| c.is_whitespace() || c == '%')
.unwrap_or(remainder.len());
assert!(nextspec > 0);
let item = Literal(&remainder[..nextspec]);
remainder = &remainder[nextspec..];
return Some((remainder, item));
}
};
macro_rules! next {
() => {
match remainder.chars().next() {
Some(x) => {
remainder = &remainder[x.len_utf8()..];
x
}
None => return Some((remainder, self.error(original, remainder))),
}
};
}
let spec = next!();
let pad_override = match spec {
'-' => Some(Pad::None),
'0' => Some(Pad::Zero),
'_' => Some(Pad::Space),
_ => None,
};
let is_alternate = spec == '#';
let spec = if pad_override.is_some() || is_alternate { next!() } else { spec };
if is_alternate && !HAVE_ALTERNATES.contains(spec) {
return Some((remainder, self.error(original, remainder)));
}
macro_rules! queue {
[$head:expr, $($tail:expr),+ $(,)*] => ({
const QUEUE: &'static [Item<'static>] = &[$($tail),+];
self.queue = QUEUE;
$head
})
}
#[cfg(not(feature = "unstable-locales"))]
macro_rules! queue_from_slice {
($slice:expr) => {{
self.queue = &$slice[1..];
$slice[0].clone()
}};
}
let item = match spec {
'A' => fixed(Fixed::LongWeekdayName),
'B' => fixed(Fixed::LongMonthName),
'C' => num0(YearDiv100),
'D' => {
queue![num0(Month), Literal("/"), num0(Day), Literal("/"), num0(YearMod100)]
}
'F' => queue![num0(Year), Literal("-"), num0(Month), Literal("-"), num0(Day)],
'G' => num0(IsoYear),
'H' => num0(Hour),
'I' => num0(Hour12),
'M' => num0(Minute),
'P' => fixed(Fixed::LowerAmPm),
'R' => queue![num0(Hour), Literal(":"), num0(Minute)],
'S' => num0(Second),
'T' => {
queue![num0(Hour), Literal(":"), num0(Minute), Literal(":"), num0(Second)]
}
'U' => num0(WeekFromSun),
'V' => num0(IsoWeek),
'W' => num0(WeekFromMon),
#[cfg(not(feature = "unstable-locales"))]
'X' => queue_from_slice!(T_FMT),
#[cfg(feature = "unstable-locales")]
'X' => self.switch_to_locale_str(locales::t_fmt, T_FMT),
'Y' => num0(Year),
'Z' => fixed(Fixed::TimezoneName),
'a' => fixed(Fixed::ShortWeekdayName),
'b' | 'h' => fixed(Fixed::ShortMonthName),
#[cfg(not(feature = "unstable-locales"))]
'c' => queue_from_slice!(D_T_FMT),
#[cfg(feature = "unstable-locales")]
'c' => self.switch_to_locale_str(locales::d_t_fmt, D_T_FMT),
'd' => num0(Day),
'e' => nums(Day),
'f' => num0(Nanosecond),
'g' => num0(IsoYearMod100),
'j' => num0(Ordinal),
'k' => nums(Hour),
'l' => nums(Hour12),
'm' => num0(Month),
'n' => Space("\n"),
'p' => fixed(Fixed::UpperAmPm),
'q' => num(Quarter),
#[cfg(not(feature = "unstable-locales"))]
'r' => queue_from_slice!(T_FMT_AMPM),
#[cfg(feature = "unstable-locales")]
'r' => {
if self.locale.is_some() && locales::t_fmt_ampm(self.locale.unwrap()).is_empty() {
self.switch_to_locale_str(locales::t_fmt, T_FMT)
} else {
self.switch_to_locale_str(locales::t_fmt_ampm, T_FMT_AMPM)
}
}
's' => num(Timestamp),
't' => Space("\t"),
'u' => num(WeekdayFromMon),
'v' => {
queue![
nums(Day),
Literal("-"),
fixed(Fixed::ShortMonthName),
Literal("-"),
num0(Year)
]
}
'w' => num(NumDaysFromSun),
#[cfg(not(feature = "unstable-locales"))]
'x' => queue_from_slice!(D_FMT),
#[cfg(feature = "unstable-locales")]
'x' => self.switch_to_locale_str(locales::d_fmt, D_FMT),
'y' => num0(YearMod100),
'z' => {
if is_alternate {
internal_fixed(TimezoneOffsetPermissive)
} else {
fixed(Fixed::TimezoneOffset)
}
}
'+' => fixed(Fixed::RFC3339),
':' => {
if remainder.starts_with("::z") {
remainder = &remainder[3..];
fixed(Fixed::TimezoneOffsetTripleColon)
} else if remainder.starts_with(":z") {
remainder = &remainder[2..];
fixed(Fixed::TimezoneOffsetDoubleColon)
} else if remainder.starts_with('z') {
remainder = &remainder[1..];
fixed(Fixed::TimezoneOffsetColon)
} else {
self.error(original, remainder)
}
}
'.' => match next!() {
'3' => match next!() {
'f' => fixed(Fixed::Nanosecond3),
_ => self.error(original, remainder),
},
'6' => match next!() {
'f' => fixed(Fixed::Nanosecond6),
_ => self.error(original, remainder),
},
'9' => match next!() {
'f' => fixed(Fixed::Nanosecond9),
_ => self.error(original, remainder),
},
'f' => fixed(Fixed::Nanosecond),
_ => self.error(original, remainder),
},
'3' => match next!() {
'f' => internal_fixed(Nanosecond3NoDot),
_ => self.error(original, remainder),
},
'6' => match next!() {
'f' => internal_fixed(Nanosecond6NoDot),
_ => self.error(original, remainder),
},
'9' => match next!() {
'f' => internal_fixed(Nanosecond9NoDot),
_ => self.error(original, remainder),
},
'%' => Literal("%"),
_ => self.error(original, remainder),
};
if let Some(new_pad) = pad_override {
match item {
Item::Numeric(ref kind, _pad) if self.queue.is_empty() => {
Some((remainder, Item::Numeric(kind.clone(), new_pad)))
}
_ => Some((remainder, self.error(original, remainder))),
}
} else {
Some((remainder, item))
}
}
fn error<'b>(&mut self, original: &'b str, remainder: &'b str) -> Item<'b> {
match self.lenient {
false => Item::Error,
true => Item::Literal(&original[..original.len() - remainder.len()]),
}
}
#[cfg(feature = "unstable-locales")]
fn switch_to_locale_str(
&mut self,
localized_fmt_str: impl Fn(Locale) -> &'static str,
fallback: &'static [Item<'static>],
) -> Item<'a> {
if let Some(locale) = self.locale {
assert!(self.locale_str.is_empty());
let (fmt_str, item) = self.parse_next_item(localized_fmt_str(locale)).unwrap();
self.locale_str = fmt_str;
item
} else {
self.queue = &fallback[1..];
fallback[0].clone()
}
}
}
impl<'a> Iterator for StrftimeItems<'a> {
type Item = Item<'a>;
fn next(&mut self) -> Option<Item<'a>> {
if let Some((item, remainder)) = self.queue.split_first() {
self.queue = remainder;
return Some(item.clone());
}
#[cfg(feature = "unstable-locales")]
if !self.locale_str.is_empty() {
let (remainder, item) = self.parse_next_item(self.locale_str)?;
self.locale_str = remainder;
return Some(item);
}
let (remainder, item) = self.parse_next_item(self.remainder)?;
self.remainder = remainder;
Some(item)
}
}
static D_FMT: &[Item<'static>] = &[
num0(Numeric::Month),
Item::Literal("/"),
num0(Numeric::Day),
Item::Literal("/"),
num0(Numeric::YearMod100),
];
static D_T_FMT: &[Item<'static>] = &[
fixed(Fixed::ShortWeekdayName),
Item::Space(" "),
fixed(Fixed::ShortMonthName),
Item::Space(" "),
nums(Numeric::Day),
Item::Space(" "),
num0(Numeric::Hour),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second),
Item::Space(" "),
num0(Numeric::Year),
];
static T_FMT: &[Item<'static>] = &[
num0(Numeric::Hour),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second),
];
static T_FMT_AMPM: &[Item<'static>] = &[
num0(Numeric::Hour12),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second),
Item::Space(" "),
fixed(Fixed::UpperAmPm),
];
const HAVE_ALTERNATES: &str = "z";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal_and_space_segments_are_preserved() {
let items = StrftimeItems::new("Hello World").parse().unwrap();
assert_eq!(items, vec![Item::Literal("Hello"), Item::Space(" "), Item::Literal("World")]);
}
#[test]
fn simple_date_specifiers_map_to_expected_items() {
assert_eq!(StrftimeItems::new("%Y").parse().unwrap(), vec![num0(Numeric::Year)]);
assert_eq!(StrftimeItems::new("%m").parse().unwrap(), vec![num0(Numeric::Month)]);
assert_eq!(StrftimeItems::new("%d").parse().unwrap(), vec![num0(Numeric::Day)]);
assert_eq!(StrftimeItems::new("%e").parse().unwrap(), vec![nums(Numeric::Day)]);
assert_eq!(StrftimeItems::new("%q").parse().unwrap(), vec![num(Numeric::Quarter)]);
assert_eq!(StrftimeItems::new("%b").parse().unwrap(), vec![fixed(Fixed::ShortMonthName)]);
assert_eq!(StrftimeItems::new("%h").parse().unwrap(), vec![fixed(Fixed::ShortMonthName)]);
assert_eq!(StrftimeItems::new("%B").parse().unwrap(), vec![fixed(Fixed::LongMonthName)]);
assert_eq!(StrftimeItems::new("%A").parse().unwrap(), vec![fixed(Fixed::LongWeekdayName)]);
assert_eq!(StrftimeItems::new("%a").parse().unwrap(), vec![fixed(Fixed::ShortWeekdayName)]);
}
#[test]
fn weekday_number_specifiers() {
assert_eq!(StrftimeItems::new("%w").parse().unwrap(), vec![num(Numeric::NumDaysFromSun)]);
assert_eq!(StrftimeItems::new("%u").parse().unwrap(), vec![num(Numeric::WeekdayFromMon)]);
}
#[test]
fn composed_date_specifiers_expand_to_multiple_items() {
assert_eq!(
StrftimeItems::new("%F").parse().unwrap(),
vec![
num0(Numeric::Year),
Item::Literal("-"),
num0(Numeric::Month),
Item::Literal("-"),
num0(Numeric::Day)
]
);
assert_eq!(
StrftimeItems::new("%D").parse().unwrap(),
vec![
num0(Numeric::Month),
Item::Literal("/"),
num0(Numeric::Day),
Item::Literal("/"),
num0(Numeric::YearMod100)
]
);
assert_eq!(
StrftimeItems::new("%T").parse().unwrap(),
vec![
num0(Numeric::Hour),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second)
]
);
assert_eq!(
StrftimeItems::new("%R").parse().unwrap(),
vec![num0(Numeric::Hour), Item::Literal(":"), num0(Numeric::Minute)]
);
assert_eq!(
StrftimeItems::new("%v").parse().unwrap(),
vec![
nums(Numeric::Day),
Item::Literal("-"),
fixed(Fixed::ShortMonthName),
Item::Literal("-"),
num0(Numeric::Year)
]
);
}
#[test]
fn x_c_r_specifiers_use_english_fallback_forms() {
assert_eq!(
StrftimeItems::new("%X").parse().unwrap(),
vec![
num0(Numeric::Hour),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second)
]
);
assert_eq!(
StrftimeItems::new("%x").parse().unwrap(),
vec![
num0(Numeric::Month),
Item::Literal("/"),
num0(Numeric::Day),
Item::Literal("/"),
num0(Numeric::YearMod100)
]
);
assert_eq!(
StrftimeItems::new("%r").parse().unwrap(),
vec![
num0(Numeric::Hour12),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second),
Item::Space(" "),
fixed(Fixed::UpperAmPm)
]
);
assert_eq!(
StrftimeItems::new("%c").parse().unwrap(),
vec![
fixed(Fixed::ShortWeekdayName),
Item::Space(" "),
fixed(Fixed::ShortMonthName),
Item::Space(" "),
nums(Numeric::Day),
Item::Space(" "),
num0(Numeric::Hour),
Item::Literal(":"),
num0(Numeric::Minute),
Item::Literal(":"),
num0(Numeric::Second),
Item::Space(" "),
num0(Numeric::Year)
]
);
}
#[test]
fn padding_modifiers_override_the_default_pad() {
assert_eq!(
StrftimeItems::new("%-j").parse().unwrap(),
vec![Item::Numeric(Numeric::Ordinal, Pad::None)]
);
assert_eq!(
StrftimeItems::new("%_j").parse().unwrap(),
vec![Item::Numeric(Numeric::Ordinal, Pad::Space)]
);
assert_eq!(
StrftimeItems::new("%0e").parse().unwrap(),
vec![Item::Numeric(Numeric::Day, Pad::Zero)]
);
}
#[test]
fn padding_modifier_on_composed_specifier_is_an_error() {
assert_eq!(StrftimeItems::new("%-F").parse(), Err(BAD_FORMAT));
}
#[test]
fn padding_modifier_on_non_numeric_specifier_is_an_error() {
assert_eq!(StrftimeItems::new("%-A").parse(), Err(BAD_FORMAT));
}
#[test]
fn nanosecond_fraction_specifiers() {
assert_eq!(StrftimeItems::new("%.3f").parse().unwrap(), vec![fixed(Fixed::Nanosecond3)]);
assert_eq!(StrftimeItems::new("%.6f").parse().unwrap(), vec![fixed(Fixed::Nanosecond6)]);
assert_eq!(StrftimeItems::new("%.9f").parse().unwrap(), vec![fixed(Fixed::Nanosecond9)]);
assert_eq!(StrftimeItems::new("%.f").parse().unwrap(), vec![fixed(Fixed::Nanosecond)]);
assert_eq!(
StrftimeItems::new("%3f").parse().unwrap(),
vec![internal_fixed(InternalInternal::Nanosecond3NoDot)]
);
assert_eq!(
StrftimeItems::new("%6f").parse().unwrap(),
vec![internal_fixed(InternalInternal::Nanosecond6NoDot)]
);
assert_eq!(
StrftimeItems::new("%9f").parse().unwrap(),
vec![internal_fixed(InternalInternal::Nanosecond9NoDot)]
);
}
#[test]
fn nanosecond_fraction_specifier_rejects_bad_suffix() {
assert_eq!(StrftimeItems::new("%.4f").parse(), Err(BAD_FORMAT));
}
#[test]
fn timezone_specifiers() {
assert_eq!(StrftimeItems::new("%z").parse().unwrap(), vec![fixed(Fixed::TimezoneOffset)]);
assert_eq!(
StrftimeItems::new("%:z").parse().unwrap(),
vec![fixed(Fixed::TimezoneOffsetColon)]
);
assert_eq!(
StrftimeItems::new("%::z").parse().unwrap(),
vec![fixed(Fixed::TimezoneOffsetDoubleColon)]
);
assert_eq!(
StrftimeItems::new("%:::z").parse().unwrap(),
vec![fixed(Fixed::TimezoneOffsetTripleColon)]
);
assert_eq!(
StrftimeItems::new("%#z").parse().unwrap(),
vec![internal_fixed(InternalInternal::TimezoneOffsetPermissive)]
);
assert_eq!(StrftimeItems::new("%Z").parse().unwrap(), vec![fixed(Fixed::TimezoneName)]);
}
#[test]
fn alternate_flag_only_supported_for_z() {
assert_eq!(StrftimeItems::new("%#Y").parse(), Err(BAD_FORMAT));
}
#[test]
fn rfc3339_and_timestamp_specifiers() {
assert_eq!(StrftimeItems::new("%+").parse().unwrap(), vec![fixed(Fixed::RFC3339)]);
assert_eq!(StrftimeItems::new("%s").parse().unwrap(), vec![num(Numeric::Timestamp)]);
}
#[test]
fn special_specifiers() {
assert_eq!(StrftimeItems::new("%t").parse().unwrap(), vec![Item::Space("\t")]);
assert_eq!(StrftimeItems::new("%n").parse().unwrap(), vec![Item::Space("\n")]);
assert_eq!(StrftimeItems::new("%%").parse().unwrap(), vec![Item::Literal("%")]);
}
#[test]
fn unknown_specifier_is_a_bad_format_error() {
assert_eq!(StrftimeItems::new("%Q").parse(), Err(BAD_FORMAT));
}
#[test]
fn new_lenient_turns_errors_into_literals() {
let items: Vec<Item> = StrftimeItems::new_lenient("%Q").collect();
assert_eq!(items, vec![Item::Literal("%Q")]);
}
#[test]
fn trailing_percent_with_no_specifier_is_an_error() {
assert_eq!(StrftimeItems::new("abc%").parse(), Err(BAD_FORMAT));
}
#[test]
fn parse_to_owned_produces_owned_items() {
let owned = StrftimeItems::new("%Y-literal").parse_to_owned().unwrap();
assert_eq!(owned[0], num0(Numeric::Year).to_owned());
match &owned[1] {
Item::OwnedLiteral(s) => assert_eq!(&**s, "-literal"),
other => panic!("expected OwnedLiteral, got {other:?}"),
}
}
}