#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum PictureToken {
Year(YearWidth),
Month(NameWidth),
Day(NameWidth),
Hour24(Pad),
Hour12(Pad),
Minute(Pad),
Second(Pad),
AmPm(AmPmCase),
Literal(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Pad {
None,
Zero,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NameWidth {
Numeric(Pad),
Abbreviated,
Full,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum YearWidth {
TwoDigit,
FourDigit,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AmPmCase {
Upper,
Lower,
}
pub(crate) fn parse(pattern: &str) -> Vec<PictureToken> {
let chars: Vec<char> = pattern.chars().collect();
let len = chars.len();
let mut tokens: Vec<PictureToken> = Vec::new();
let mut i = 0;
while i < len {
if chars[i] == '\'' {
i += 1;
let start = i;
while i < len && chars[i] != '\'' {
i += 1;
}
push_literal(&mut tokens, chars[start..i].iter().collect::<String>());
if i < len {
i += 1; }
continue;
}
if chars[i] == '\\' {
i += 1;
if i < len {
push_literal(&mut tokens, chars[i].to_string());
i += 1;
}
continue;
}
if chars[i..].starts_with(&['A', 'M', '/', 'P', 'M']) {
tokens.push(PictureToken::AmPm(AmPmCase::Upper));
i += 5;
continue;
}
if chars[i..].starts_with(&['a', 'm', '/', 'p', 'm']) {
tokens.push(PictureToken::AmPm(AmPmCase::Lower));
i += 5;
continue;
}
let run = count_run(&chars, i);
let token = match chars[i] {
'y' => Some(PictureToken::Year(if run >= 4 {
YearWidth::FourDigit
} else {
YearWidth::TwoDigit
})),
'M' => Some(PictureToken::Month(name_width(run))),
'd' => Some(PictureToken::Day(name_width(run))),
'H' => Some(PictureToken::Hour24(pad(run))),
'h' => Some(PictureToken::Hour12(pad(run))),
'm' => Some(PictureToken::Minute(pad(run))),
's' => Some(PictureToken::Second(pad(run))),
_ => None,
};
match token {
Some(t) => {
tokens.push(t);
i += run;
}
None => {
push_literal(&mut tokens, chars[i].to_string());
i += 1;
}
}
}
tokens
}
fn push_literal(tokens: &mut Vec<PictureToken>, text: String) {
match tokens.last_mut() {
Some(PictureToken::Literal(prev)) => prev.push_str(&text),
_ => tokens.push(PictureToken::Literal(text)),
}
}
fn count_run(chars: &[char], start: usize) -> usize {
let ch = chars[start];
chars[start..].iter().take_while(|&&c| c == ch).count()
}
fn pad(run: usize) -> Pad {
if run >= 2 {
Pad::Zero
} else {
Pad::None
}
}
fn name_width(run: usize) -> NameWidth {
match run {
1 => NameWidth::Numeric(Pad::None),
2 => NameWidth::Numeric(Pad::Zero),
3 => NameWidth::Abbreviated,
_ => NameWidth::Full,
}
}
#[cfg(test)]
mod tests {
use super::PictureToken::*;
use super::*;
fn lit(s: &str) -> PictureToken {
Literal(s.to_string())
}
#[test]
fn a_plain_picture_tokenizes_by_run_length() {
assert_eq!(
parse("MMM d, yyyy"),
vec![
Month(NameWidth::Abbreviated),
lit(" "),
Day(NameWidth::Numeric(Pad::None)),
lit(", "),
Year(YearWidth::FourDigit),
]
);
}
#[test]
fn run_length_picks_the_name_width() {
assert_eq!(parse("d"), vec![Day(NameWidth::Numeric(Pad::None))]);
assert_eq!(parse("dd"), vec![Day(NameWidth::Numeric(Pad::Zero))]);
assert_eq!(parse("ddd"), vec![Day(NameWidth::Abbreviated)]);
assert_eq!(parse("dddd"), vec![Day(NameWidth::Full)]);
assert_eq!(parse("ddddd"), vec![Day(NameWidth::Full)]);
}
#[test]
fn an_escape_is_consumed_and_its_character_is_literal() {
assert_eq!(
parse(r"MMM\ d"),
vec![
Month(NameWidth::Abbreviated),
lit(" "),
Day(NameWidth::Numeric(Pad::None)),
],
"issue #159 case A"
);
assert_eq!(
parse(r"d \a yyyy"),
vec![
Day(NameWidth::Numeric(Pad::None)),
lit(" a "),
Year(YearWidth::FourDigit),
],
"issue #159 case C — and the literals around it merged"
);
}
#[test]
fn an_escaped_format_character_is_not_a_format_character() {
assert_eq!(parse(r"\d\d\d"), vec![lit("ddd")]);
assert_eq!(parse(r"\y\M\d\h"), vec![lit("yMdh")]);
}
#[test]
fn a_backslash_inside_quotes_stays_literal() {
assert_eq!(parse(r"'a\b'"), vec![lit(r"a\b")]);
}
#[test]
fn a_trailing_backslash_is_consumed() {
assert_eq!(parse(r"yyyy\"), vec![Year(YearWidth::FourDigit)]);
assert_eq!(parse(r"\"), vec![]);
}
#[test]
fn quoted_text_is_literal_and_not_tokenized() {
assert_eq!(
parse("'on' d"),
vec![lit("on "), Day(NameWidth::Numeric(Pad::None))]
);
}
#[test]
fn an_unterminated_quote_runs_to_the_end() {
assert_eq!(
parse("d 'tail"),
vec![Day(NameWidth::Numeric(Pad::None)), lit(" tail")]
);
}
#[test]
fn quoted_am_pm_is_literal_text() {
assert_eq!(parse("'AM/PM'"), vec![lit("AM/PM")]);
assert_eq!(parse("AM/PM"), vec![AmPm(AmPmCase::Upper)]);
assert_eq!(parse("am/pm"), vec![AmPm(AmPmCase::Lower)]);
}
#[test]
fn one_picture_carries_both_date_and_time_tokens() {
assert_eq!(
parse("MMM d h:mm"),
vec![
Month(NameWidth::Abbreviated),
lit(" "),
Day(NameWidth::Numeric(Pad::None)),
lit(" "),
Hour12(Pad::None),
lit(":"),
Minute(Pad::Zero),
]
);
}
#[test]
fn an_empty_picture_is_no_tokens() {
assert_eq!(parse(""), vec![]);
}
#[test]
fn a_multibyte_literal_before_am_pm_does_not_panic() {
assert_eq!(parse("«h» AM/PM").last(), Some(&AmPm(AmPmCase::Upper)));
}
}