use alloc::string::String;
use alloc::vec::Vec;
use core::iter;
use crate::error::InvalidFormatDescription;
use crate::format_description::parse::{
attach_location, unused, Error, ErrorInner, Location, Spanned, SpannedValue, Unused,
};
use crate::format_description::{self, modifier, BorrowedFormatItem, Component};
#[doc(alias = "parse_strptime_borrowed")]
pub fn parse_strftime_borrowed(
s: &str,
) -> Result<Vec<BorrowedFormatItem<'_>>, InvalidFormatDescription> {
let tokens = lex(s.as_bytes());
let items = into_items(tokens).collect::<Result<_, _>>()?;
Ok(items)
}
#[doc(alias = "parse_strptime_owned")]
pub fn parse_strftime_owned(
s: &str,
) -> Result<format_description::OwnedFormatItem, InvalidFormatDescription> {
parse_strftime_borrowed(s).map(Into::into)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Padding {
Default,
Spaces,
None,
Zeroes,
}
enum Token<'a> {
Literal(Spanned<&'a [u8]>),
Component {
_percent: Unused<Location>,
padding: Spanned<Padding>,
component: Spanned<u8>,
},
}
fn lex(mut input: &[u8]) -> iter::Peekable<impl Iterator<Item = Result<Token<'_>, Error>>> {
let mut iter = attach_location(input.iter()).peekable();
iter::from_fn(move || {
Some(Ok(match iter.next()? {
(b'%', percent_loc) => match iter.next() {
Some((padding @ (b'_' | b'-' | b'0'), padding_loc)) => {
let padding = match padding {
b'_' => Padding::Spaces,
b'-' => Padding::None,
b'0' => Padding::Zeroes,
_ => unreachable!(),
};
let (&component, component_loc) = iter.next()?;
input = &input[3..];
Token::Component {
_percent: unused(percent_loc),
padding: padding.spanned(padding_loc.to_self()),
component: component.spanned(component_loc.to_self()),
}
}
Some((&component, component_loc)) => {
input = &input[2..];
let span = component_loc.to_self();
Token::Component {
_percent: unused(percent_loc),
padding: Padding::Default.spanned(span),
component: component.spanned(span),
}
}
None => {
return Some(Err(Error {
_inner: unused(percent_loc.error("unexpected end of input")),
public: InvalidFormatDescription::Expected {
what: "valid escape sequence",
index: percent_loc.byte as _,
},
}));
}
},
(_, start_location) => {
let mut bytes = 1;
let mut end_location = start_location;
while let Some((_, location)) = iter.next_if(|&(&byte, _)| byte != b'%') {
end_location = location;
bytes += 1;
}
let value = &input[..bytes];
input = &input[bytes..];
Token::Literal(value.spanned(start_location.to(end_location)))
}
}))
})
.peekable()
}
fn into_items<'iter, 'token: 'iter>(
mut tokens: iter::Peekable<impl Iterator<Item = Result<Token<'token>, Error>> + 'iter>,
) -> impl Iterator<Item = Result<BorrowedFormatItem<'token>, Error>> + 'iter {
iter::from_fn(move || {
let next = match tokens.next()? {
Ok(token) => token,
Err(err) => return Some(Err(err)),
};
Some(match next {
Token::Literal(spanned) => Ok(BorrowedFormatItem::Literal(*spanned)),
Token::Component {
_percent,
padding,
component,
} => parse_component(padding, component),
})
})
}
fn parse_component(
padding: Spanned<Padding>,
component: Spanned<u8>,
) -> Result<BorrowedFormatItem<'static>, Error> {
let padding_or_default = |padding: Padding, default| match padding {
Padding::Default => default,
Padding::Spaces => modifier::Padding::Space,
Padding::None => modifier::Padding::None,
Padding::Zeroes => modifier::Padding::Zero,
};
macro_rules! component {
($name:ident { $($inner:tt)* }) => {
BorrowedFormatItem::Component(Component::$name(modifier::$name {
$($inner)*
}))
}
}
Ok(match *component {
b'%' => BorrowedFormatItem::Literal(b"%"),
b'a' => component!(Weekday {
repr: modifier::WeekdayRepr::Short,
one_indexed: true,
case_sensitive: true,
}),
b'A' => component!(Weekday {
repr: modifier::WeekdayRepr::Long,
one_indexed: true,
case_sensitive: true,
}),
b'b' | b'h' => component!(Month {
repr: modifier::MonthRepr::Short,
padding: modifier::Padding::Zero,
case_sensitive: true,
}),
b'B' => component!(Month {
repr: modifier::MonthRepr::Long,
padding: modifier::Padding::Zero,
case_sensitive: true,
}),
b'c' => BorrowedFormatItem::Compound(&[
component!(Weekday {
repr: modifier::WeekdayRepr::Short,
one_indexed: true,
case_sensitive: true,
}),
BorrowedFormatItem::Literal(b" "),
component!(Month {
repr: modifier::MonthRepr::Short,
padding: modifier::Padding::Zero,
case_sensitive: true,
}),
BorrowedFormatItem::Literal(b" "),
component!(Day {
padding: modifier::Padding::Space
}),
BorrowedFormatItem::Literal(b" "),
component!(Hour {
padding: modifier::Padding::Zero,
is_12_hour_clock: false,
}),
BorrowedFormatItem::Literal(b":"),
component!(Minute {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b":"),
component!(Second {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b" "),
component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
iso_week_based: false,
sign_is_mandatory: false,
}),
]),
b'C' => component!(Year {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::YearRepr::Century,
iso_week_based: false,
sign_is_mandatory: false,
}),
b'd' => component!(Day {
padding: padding_or_default(*padding, modifier::Padding::Zero),
}),
b'D' => BorrowedFormatItem::Compound(&[
component!(Month {
repr: modifier::MonthRepr::Numerical,
padding: modifier::Padding::Zero,
case_sensitive: true,
}),
BorrowedFormatItem::Literal(b"/"),
component!(Day {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b"/"),
component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::LastTwo,
iso_week_based: false,
sign_is_mandatory: false,
}),
]),
b'e' => component!(Day {
padding: padding_or_default(*padding, modifier::Padding::Space),
}),
b'F' => BorrowedFormatItem::Compound(&[
component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
iso_week_based: false,
sign_is_mandatory: false,
}),
BorrowedFormatItem::Literal(b"-"),
component!(Month {
padding: modifier::Padding::Zero,
repr: modifier::MonthRepr::Numerical,
case_sensitive: true,
}),
BorrowedFormatItem::Literal(b"-"),
component!(Day {
padding: modifier::Padding::Zero,
}),
]),
b'g' => component!(Year {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::YearRepr::LastTwo,
iso_week_based: true,
sign_is_mandatory: false,
}),
b'G' => component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
iso_week_based: true,
sign_is_mandatory: false,
}),
b'H' => component!(Hour {
padding: padding_or_default(*padding, modifier::Padding::Zero),
is_12_hour_clock: false,
}),
b'I' => component!(Hour {
padding: padding_or_default(*padding, modifier::Padding::Zero),
is_12_hour_clock: true,
}),
b'j' => component!(Ordinal {
padding: padding_or_default(*padding, modifier::Padding::Zero),
}),
b'k' => component!(Hour {
padding: padding_or_default(*padding, modifier::Padding::Space),
is_12_hour_clock: false,
}),
b'l' => component!(Hour {
padding: padding_or_default(*padding, modifier::Padding::Space),
is_12_hour_clock: true,
}),
b'm' => component!(Month {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::MonthRepr::Numerical,
case_sensitive: true,
}),
b'M' => component!(Minute {
padding: padding_or_default(*padding, modifier::Padding::Zero),
}),
b'n' => BorrowedFormatItem::Literal(b"\n"),
b'O' => {
return Err(Error {
_inner: unused(ErrorInner {
_message: "unsupported modifier",
_span: component.span,
}),
public: InvalidFormatDescription::NotSupported {
what: "modifier",
context: "",
index: component.span.start.byte as _,
},
})
}
b'p' => component!(Period {
is_uppercase: true,
case_sensitive: true
}),
b'P' => component!(Period {
is_uppercase: false,
case_sensitive: true
}),
b'r' => BorrowedFormatItem::Compound(&[
component!(Hour {
padding: modifier::Padding::Zero,
is_12_hour_clock: true,
}),
BorrowedFormatItem::Literal(b":"),
component!(Minute {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b":"),
component!(Second {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b" "),
component!(Period {
is_uppercase: true,
case_sensitive: true,
}),
]),
b'R' => BorrowedFormatItem::Compound(&[
component!(Hour {
padding: modifier::Padding::Zero,
is_12_hour_clock: false,
}),
BorrowedFormatItem::Literal(b":"),
component!(Minute {
padding: modifier::Padding::Zero,
}),
]),
b's' => component!(UnixTimestamp {
precision: modifier::UnixTimestampPrecision::Second,
sign_is_mandatory: false,
}),
b'S' => component!(Second {
padding: padding_or_default(*padding, modifier::Padding::Zero),
}),
b't' => BorrowedFormatItem::Literal(b"\t"),
b'T' => BorrowedFormatItem::Compound(&[
component!(Hour {
padding: modifier::Padding::Zero,
is_12_hour_clock: false,
}),
BorrowedFormatItem::Literal(b":"),
component!(Minute {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b":"),
component!(Second {
padding: modifier::Padding::Zero,
}),
]),
b'u' => component!(Weekday {
repr: modifier::WeekdayRepr::Monday,
one_indexed: true,
case_sensitive: true,
}),
b'U' => component!(WeekNumber {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::WeekNumberRepr::Sunday,
}),
b'V' => component!(WeekNumber {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::WeekNumberRepr::Iso,
}),
b'w' => component!(Weekday {
repr: modifier::WeekdayRepr::Sunday,
one_indexed: true,
case_sensitive: true,
}),
b'W' => component!(WeekNumber {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::WeekNumberRepr::Monday,
}),
b'x' => BorrowedFormatItem::Compound(&[
component!(Month {
repr: modifier::MonthRepr::Numerical,
padding: modifier::Padding::Zero,
case_sensitive: true,
}),
BorrowedFormatItem::Literal(b"/"),
component!(Day {
padding: modifier::Padding::Zero
}),
BorrowedFormatItem::Literal(b"/"),
component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::LastTwo,
iso_week_based: false,
sign_is_mandatory: false,
}),
]),
b'X' => BorrowedFormatItem::Compound(&[
component!(Hour {
padding: modifier::Padding::Zero,
is_12_hour_clock: false,
}),
BorrowedFormatItem::Literal(b":"),
component!(Minute {
padding: modifier::Padding::Zero,
}),
BorrowedFormatItem::Literal(b":"),
component!(Second {
padding: modifier::Padding::Zero,
}),
]),
b'y' => component!(Year {
padding: padding_or_default(*padding, modifier::Padding::Zero),
repr: modifier::YearRepr::LastTwo,
iso_week_based: false,
sign_is_mandatory: false,
}),
b'Y' => component!(Year {
padding: modifier::Padding::Zero,
repr: modifier::YearRepr::Full,
iso_week_based: false,
sign_is_mandatory: false,
}),
b'z' => BorrowedFormatItem::Compound(&[
component!(OffsetHour {
sign_is_mandatory: true,
padding: modifier::Padding::Zero,
}),
component!(OffsetMinute {
padding: modifier::Padding::Zero,
}),
]),
b'Z' => {
return Err(Error {
_inner: unused(ErrorInner {
_message: "unsupported component",
_span: component.span,
}),
public: InvalidFormatDescription::NotSupported {
what: "component",
context: "",
index: component.span.start.byte as _,
},
})
}
_ => {
return Err(Error {
_inner: unused(ErrorInner {
_message: "invalid component",
_span: component.span,
}),
public: InvalidFormatDescription::InvalidComponentName {
name: String::from_utf8_lossy(&[*component]).into_owned(),
index: component.span.start.byte as _,
},
})
}
})
}