From 6ca3b4aabc1d477d8322b09f53837cf3388642a1 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 31 Mar 2025 12:11:14 -0700 Subject: [PATCH] time and date input format tests --- rust/Cargo.lock | 12 +- rust/pspp/Cargo.toml | 2 +- rust/pspp/src/calendar.rs | 19 +- rust/pspp/src/format/display.rs | 4 +- rust/pspp/src/format/mod.rs | 19 +- rust/pspp/src/format/parse.rs | 507 +++++++++++++++++++++++++++----- 6 files changed, 463 insertions(+), 100 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8495ff462e..181a98afc8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -238,16 +238,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1571,6 +1571,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/rust/pspp/Cargo.toml b/rust/pspp/Cargo.toml index b55be57778..e563b43c58 100644 --- a/rust/pspp/Cargo.toml +++ b/rust/pspp/Cargo.toml @@ -17,7 +17,7 @@ num-derive = "0.4.0" num-traits = "0.2.16" ordered-float = "3.7.0" thiserror = "1.0" -chrono = "0.4.26" +chrono = "0.4.40" finl_unicode = "1.2.0" unicase = "2.6.0" libc = "0.2.147" diff --git a/rust/pspp/src/calendar.rs b/rust/pspp/src/calendar.rs index 7a8562d061..239d084776 100644 --- a/rust/pspp/src/calendar.rs +++ b/rust/pspp/src/calendar.rs @@ -1,4 +1,5 @@ -use chrono::{Datelike, Days, NaiveDate}; +use chrono::{Datelike, Days, Month, NaiveDate}; +use num::FromPrimitive; use thiserror::Error as ThisError; use crate::format::Settings; @@ -102,11 +103,19 @@ pub fn calendar_gregorian_adjust( } } -pub fn calendar_raw_gregorian_to_offset(y: i32, m: i32, d: i32) -> i32 { - fn is_leap_year(y: i32) -> bool { - y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) - } +pub fn is_leap_year(y: i32) -> bool { + NaiveDate::from_yo_opt(y, 1).unwrap().leap_year() +} +pub fn days_in_month(year: i32, month: i32) -> i32 { + Month::from_i32(month) + .unwrap() + .num_days(year) + .unwrap() + .into() +} + +pub fn calendar_raw_gregorian_to_offset(y: i32, m: i32, d: i32) -> i32 { -577735 + 365 * (y - 1) + (y - 1) / 4 - (y - 1) / 100 + (y - 1) / 400 + (367 * m - 362) / 12 diff --git a/rust/pspp/src/format/display.rs b/rust/pspp/src/format/display.rs index f73b735295..8ff4c41898 100644 --- a/rust/pspp/src/format/display.rs +++ b/rust/pspp/src/format/display.rs @@ -82,7 +82,7 @@ impl<'a, 'b> Display for DisplayValue<'a, 'b> { | Type::MoYr | Type::WkYr | Type::DateTime - | Type::YMDHMS + | Type::YmdHms | Type::MTime | Type::Time | Type::DTime @@ -454,7 +454,7 @@ impl<'a, 'b> DisplayValue<'a, 'b> { if year <= 9999 { write!(&mut output, "{year:04}").unwrap(); } else if self.format.type_ == Type::DateTime - || self.format.type_ == Type::YMDHMS + || self.format.type_ == Type::YmdHms { write!(&mut output, "****").unwrap(); } else { diff --git a/rust/pspp/src/format/mod.rs b/rust/pspp/src/format/mod.rs index ff350f2d16..13df780d52 100644 --- a/rust/pspp/src/format/mod.rs +++ b/rust/pspp/src/format/mod.rs @@ -98,7 +98,7 @@ impl From for Category { | Type::MoYr | Type::WkYr | Type::DateTime - | Type::YMDHMS => Self::Date, + | Type::YmdHms => Self::Date, Type::MTime | Type::Time | Type::DTime => Self::Time, Type::WkDay | Type::Month => Self::DateComponent, Type::A | Type::AHex => Self::String, @@ -169,7 +169,7 @@ pub enum Type { MoYr, WkYr, DateTime, - YMDHMS, + YmdHms, MTime, Time, DTime, @@ -235,7 +235,7 @@ impl Type { Self::MoYr => 6, Self::WkYr => 8, Self::DateTime => 17, - Self::YMDHMS => 16, + Self::YmdHms => 16, Self::MTime => 5, Self::Time => 5, Self::DTime => 8, @@ -275,7 +275,7 @@ impl Type { | Self::MoYr | Self::WkYr => 0, Self::DateTime => width - 21, - Self::YMDHMS => width - 20, + Self::YmdHms => width - 20, Self::MTime => width - 6, Self::Time => width - 9, Self::DTime => width - 12, @@ -363,7 +363,7 @@ impl Type { Self::MoYr => "MOYR", Self::WkYr => "WKYR", Self::DateTime => "DATETIME", - Self::YMDHMS => "YMDHMS", + Self::YmdHms => "YMDHMS", Self::MTime => "MTIME", Self::Time => "TIME", Self::DTime => "DTIME", @@ -659,7 +659,7 @@ impl TryFrom for Type { 38 => Ok(Self::EDate), 39 => Ok(Self::SDate), 40 => Ok(Self::MTime), - 41 => Ok(Self::YMDHMS), + 41 => Ok(Self::YmdHms), _ => Err(Error::UnknownFormat { value: source }), } } @@ -854,6 +854,9 @@ impl Settings { ..self } } + pub fn with_epoch(self, epoch: Epoch) -> Self { + Self { epoch, ..self } + } fn number_style(&self, type_: Type) -> &NumberStyle { static DEFAULT: LazyLock = LazyLock::new(|| NumberStyle::new("", "", Decimal::Dot, None, false)); @@ -912,7 +915,7 @@ impl Settings { | Type::MoYr | Type::WkYr | Type::DateTime - | Type::YMDHMS + | Type::YmdHms | Type::MTime | Type::Time | Type::DTime @@ -1129,7 +1132,7 @@ impl DateTemplate { Type::MoYr => ("mmm yy", "mmm yyyy"), Type::WkYr => ("ww WK yy", "ww WK yyyy"), Type::DateTime => ("dd-mmm-yyyy HH:MM", "dd-mmm-yyyy HH:MM:SS"), - Type::YMDHMS => ("yyyy-mm-dd HH:MM", "yyyy-mm-dd HH:MM:SS"), + Type::YmdHms => ("yyyy-mm-dd HH:MM", "yyyy-mm-dd HH:MM:SS"), Type::MTime => ("MM", "MM:SS"), Type::Time => ("HH:MM", "HH:MM:SS"), Type::DTime => ("D HH:MM", "D HH:MM:SS"), diff --git a/rust/pspp/src/format/parse.rs b/rust/pspp/src/format/parse.rs index 217186cb3b..20617fc13a 100644 --- a/rust/pspp/src/format/parse.rs +++ b/rust/pspp/src/format/parse.rs @@ -109,7 +109,7 @@ pub struct ParseValue<'a> { } impl Format { - fn parser(&self) -> ParseValue { + pub fn parser(&self) -> ParseValue { ParseValue::new(*self) } } @@ -159,7 +159,7 @@ impl<'a> ParseValue<'a> { | Type::MoYr | Type::WkYr | Type::DateTime - | Type::YMDHMS + | Type::YmdHms | Type::MTime | Type::Time | Type::DTime => self.parse_date(s), @@ -288,7 +288,6 @@ impl<'a> ParseValue<'a> { } write!(&mut number, "{exponent}").unwrap(); } - println!("{number}"); match f64::from_str(&number) { Ok(value) => Ok(Value::Number(Some(value))), @@ -318,7 +317,7 @@ impl<'a> ParseValue<'a> { let mut time_sign = None; let mut time = 0.0; - let mut iter = DateTemplate::for_format(self.format).unwrap(); + let mut iter = DateTemplate::new(self.format.type_, 0).unwrap(); let template_width = iter.len(); while let Some(TemplateItem { c, n }) = iter.next() { match c { @@ -338,7 +337,7 @@ impl<'a> ParseValue<'a> { .is_some_and(|item| item.c.is_ascii_alphabetic()) { usize::MAX - } else if orig_input.len() >= template_width + 2 { + } else if p.0.len() >= template_width + 2 { 4 } else { 2 @@ -425,8 +424,9 @@ impl<'a> ParseValue<'a> { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] enum Sign { + #[default] Positive, Negative, } @@ -469,7 +469,7 @@ fn parse_yday<'a>(p: &mut StrParser<'a>) -> Result { if !(1..=366).contains(&yday) { return Err(ParseErrorKind::InvalidYDay(yday)); } - p.0 = &p.0[..3]; + p.0 = &p.0[3..]; Ok(yday) } @@ -483,7 +483,7 @@ fn parse_month<'a>(p: &mut StrParser<'a>) -> Result { let name = p.strip_matches(|c| c.is_ascii_alphabetic()); static ENGLISH_NAMES: [&str; 12] = [ - "jan", "fe", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", + "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", ]; if let Some(month) = match_name(&name[..3.min(name.len())], &ENGLISH_NAMES) { return Ok(month); @@ -492,7 +492,7 @@ fn parse_month<'a>(p: &mut StrParser<'a>) -> Result { static ROMAN_NAMES: [&str; 12] = [ "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii", ]; - if let Some(month) = match_name(name, &ENGLISH_NAMES) { + if let Some(month) = match_name(name, &ROMAN_NAMES) { return Ok(month); } } @@ -559,12 +559,13 @@ fn parse_year<'a>( settings: &Settings, max_digits: usize, ) -> Result { - let head = p.0; + let head = p.clone().strip_matches(|c| c.is_ascii_digit()); let head = if head.len() > max_digits { head.get(..max_digits).ok_or(ParseErrorKind::DateSyntax)? } else { head }; + let year = head .parse::() .map_err(|_| ParseErrorKind::DateSyntax)?; @@ -649,11 +650,12 @@ mod test { }; use encoding_rs::UTF_8; - use serde::de::Expected; use crate::{ - format::{parse::Sign, DateTemplate, Format, Type}, - settings::Settings, + calendar::{days_in_month, is_leap_year}, + dictionary::Value, + format::{parse::Sign, Epoch, Format, Settings as FormatSettings, Type}, + settings::{self, Settings as PsppSettings}, }; fn test(name: &str, type_: Type) { @@ -661,8 +663,7 @@ mod test { let input_stream = BufReader::new(File::open(base.join("num-in.txt")).unwrap()); let expected_stream = BufReader::new(File::open(base.join(name)).unwrap()); let format = Format::new(type_, 40, 1).unwrap(); - let settings = Settings::global().formats.number_style(type_); - println!("{:?}", settings); + let settings = PsppSettings::global().formats.number_style(type_); for (line_number, (input, expected)) in input_stream .lines() .map(|result| result.unwrap()) @@ -725,6 +726,29 @@ mod test { second: i32, } + impl TestDate { + const fn new( + year: i32, + month: i32, + day: i32, + yday: i32, + hour: i32, + minute: i32, + second: i32, + ) -> Self { + Self { + year, + month, + day, + yday, + hour, + minute, + second, + } + } + } + + #[derive(Copy, Clone, Debug)] struct ExpectDate { year: i32, month: i32, @@ -733,13 +757,28 @@ mod test { sign: Sign, } - struct DateVisitor<'a> { + impl Default for ExpectDate { + fn default() -> Self { + Self { + year: 0, + month: 0, + day: 1, + time: 0, + sign: Sign::default(), + } + } + } + + struct DateTester<'a> { date: TestDate, template: &'a str, + formatted: String, + type_: Type, } - impl<'a> DateVisitor<'a> { - fn visit(&self, formatted: String, expected: ExpectDate) { + impl<'a> DateTester<'a> { + fn visit(&self, extra: &str, mut expected: ExpectDate) { + let formatted = format!("{}{extra}", self.formatted); if !self.template.is_empty() { fn years(y: i32) -> Vec { match y { @@ -749,18 +788,17 @@ mod test { } let mut iter = self.template.chars(); let first = iter.next().unwrap(); - let next = DateVisitor { + let next = DateTester { date: self.date.clone(), template: iter.as_str(), + formatted: formatted.clone(), + type_: self.type_, }; match first { 'd' => { - let expected = ExpectDate { - day: self.date.day, - ..expected - }; - next.visit(format!("{formatted}{}", self.date.day), expected); - next.visit(format!("{formatted}{:02}", self.date.day), expected); + expected.day = self.date.day; + next.visit(&format!("{}", self.date.day), expected); + next.visit(&format!("{:02}", self.date.day), expected); } 'm' => { let m = self.date.month as usize - 1; @@ -784,89 +822,396 @@ mod test { ]; let roman = ROMAN[m]; let english = ENGLISH[m]; - let expected = ExpectDate { - month: self.date.month, - ..expected - }; - for formatted in [ - format!("{formatted}{}", self.date.month), - format!("{formatted}{:02}", self.date.month), - format!("{formatted}{}", roman), - format!("{formatted}{}", roman.to_ascii_uppercase()), - format!("{formatted}{}", english), - format!("{formatted}{}", english.to_ascii_uppercase()), - format!("{formatted}{}", &english[..3]), - format!("{formatted}{}", &english[..3].to_ascii_uppercase()), - ] { - next.visit(formatted, expected); - } + + expected.month = self.date.month; + next.visit(&format!("{}", self.date.month), expected); + next.visit(&format!("{:02}", self.date.month), expected); + next.visit(roman, expected); + next.visit(&roman.to_ascii_uppercase(), expected); + next.visit(english, expected); + next.visit(&english[..3], expected); + next.visit(&english.to_ascii_uppercase(), expected); + next.visit(&english[..3].to_ascii_uppercase(), expected); } 'y' => { - let expected = ExpectDate { - year: self.date.year, - ..expected - }; + expected.year = self.date.year; for year in years(self.date.year) { - next.visit(format!("{formatted}{year}"), expected); + next.visit(&format!("{year}"), expected); } } 'j' => { - let expected = ExpectDate { - year: self.date.year, - month: self.date.month, - day: self.date.day, - ..expected - }; + expected.year = self.date.year; + expected.month = self.date.month; + expected.day = self.date.day; for year in years(self.date.year) { - next.visit(format!("{formatted}{year}{:03}", self.date.yday), expected); + next.visit(&format!("{year}{:03}", self.date.yday), expected); } } 'q' => { let quarter = (self.date.month - 1) / 3 + 1; let month = (quarter - 1) * 3 + 1; - next.visit( - format!("{formatted}{}", quarter), - ExpectDate { month, ..expected }, - ); + next.visit(&format!("{}", quarter), ExpectDate { month, ..expected }); } 'w' => { let week = (self.date.yday - 1) / 7 + 1; - let mut month = self.date.month; - let mut day = self.date.day - (self.date.yday - 1) % 7; - if day < 1 { - month -= 1; - day += days_in_month(self.date.year, month); + expected.month = self.date.month; + expected.day = self.date.day - (self.date.yday - 1) % 7; + if expected.day < 1 { + expected.month -= 1; + expected.day += days_in_month(self.date.year, expected.month + 1); } + next.visit(&format!("{week}"), expected); + } + 'H' => { + expected.time += self.date.hour * 3600; + next.visit(&format!("{}", self.date.hour), expected); + next.visit(&format!("{:02}", self.date.hour), expected); + } + 'M' => { + expected.time += self.date.minute * 60; + next.visit(&format!("{}", self.date.minute), expected); + next.visit(&format!("{:02}", self.date.minute), expected); + } + 'S' => { + expected.time += self.date.second; + next.visit(&format!("{}", self.date.second), expected); + next.visit(&format!("{:02}", self.date.second), expected); + } + '-' => { + for c in b" -.,/" { + next.visit(&format!("{}", *c as char), expected); + } + } + ':' => { + for c in b" :" { + next.visit(&format!("{}", *c as char), expected); + } + } + ' ' => { + next.visit(" ", expected); + } + 'Q' => { + for s in ["q", " q", "q ", " q "] { + for s in [String::from(s), s.to_ascii_uppercase()] { + next.visit(&s, expected); + } + } + } + 'W' => { + for s in ["wk", " wk", "wk ", " wk "] { + for s in [String::from(s), s.to_ascii_uppercase()] { + next.visit(&s, expected); + } + } + } + '+' => { + next.visit("", expected); + next.visit( + "+", + ExpectDate { + sign: Sign::Positive, + ..expected + }, + ); next.visit( - format!("{formatted}{week}"), + "-", ExpectDate { - month, - day, + sign: Sign::Negative, ..expected }, ); } + _ => unreachable!(), } + } else { + assert!((1582..=2100).contains(&expected.year)); + assert!((1..=12).contains(&expected.month)); + assert!((1..=31).contains(&expected.day)); + + let ExpectDate { + year, + month, + day, + time, + sign, + }: ExpectDate = expected; + + const EPOCH: i32 = -577734; // 14 Oct 1582 + let expected = (EPOCH - 1 + 365 * (year - 1) + (year - 1) / 4 - (year - 1) / 100 + + (year - 1) / 400 + + (367 * month - 362) / 12 + + if month <= 2 { + 0 + } else if month >= 2 && is_leap_year(year) { + -1 + } else { + -2 + } + + day) as i64 + * 86400; + let expected = if sign == Sign::Negative && expected > 0 { + expected - time as i64 + } else { + expected + time as i64 + }; + let settings = FormatSettings::default().with_epoch(Epoch(1930)); + let parsed = Format::new(self.type_, 40, 0) + .unwrap() + .parser() + .with_settings(&settings) + .parse(&formatted, UTF_8) + .unwrap(); + assert_eq!(parsed, Value::Number(Some(expected as f64))); + } + } + + fn test(template: &str, type_: Type) { + static DATES: [TestDate; 20] = [ + TestDate::new(1648, 6, 10, 162, 0, 0, 0), + TestDate::new(1680, 6, 30, 182, 4, 50, 38), + TestDate::new(1716, 7, 24, 206, 12, 31, 35), + TestDate::new(1768, 6, 19, 171, 12, 47, 53), + TestDate::new(1819, 8, 2, 214, 1, 26, 0), + TestDate::new(1839, 3, 27, 86, 20, 58, 11), + TestDate::new(1903, 4, 19, 109, 7, 36, 5), + TestDate::new(1929, 8, 25, 237, 15, 43, 49), + TestDate::new(1941, 9, 29, 272, 4, 25, 9), + TestDate::new(1943, 4, 19, 109, 6, 49, 27), + TestDate::new(1943, 10, 7, 280, 2, 57, 52), + TestDate::new(1992, 3, 17, 77, 16, 45, 44), + TestDate::new(1996, 2, 25, 56, 21, 30, 57), + TestDate::new(1941, 9, 29, 272, 4, 25, 9), + TestDate::new(1943, 4, 19, 109, 6, 49, 27), + TestDate::new(1943, 10, 7, 280, 2, 57, 52), + TestDate::new(1992, 3, 17, 77, 16, 45, 44), + TestDate::new(1996, 2, 25, 56, 21, 30, 57), + TestDate::new(2038, 11, 10, 314, 22, 30, 4), + TestDate::new(2094, 7, 18, 199, 1, 56, 51), + ]; + for date in &DATES { + let visitor = DateTester { + date: date.clone(), + template, + formatted: "".to_string(), + type_, + }; + visitor.visit("", ExpectDate::default()); + } + } + } + + #[test] + fn date() { + DateTester::test("d-m-y", Type::Date); + } + + #[test] + fn adate() { + DateTester::test("m-d-y", Type::ADate); + } + + #[test] + fn edate() { + DateTester::test("d-m-y", Type::EDate); + } + + #[test] + fn jdate() { + DateTester::test("j", Type::JDate); + } + + #[test] + fn sdate() { + DateTester::test("y-m-d", Type::SDate); + } + + #[test] + fn qyr() { + DateTester::test("qQy", Type::QYr); + } + + #[test] + fn moyr() { + DateTester::test("m-y", Type::MoYr); + } + + #[test] + fn wkyr() { + DateTester::test("wWy", Type::WkYr); + } + + #[test] + fn datetime_without_seconds() { + // A more exhaustive template would be "d-m-y +H:M", but that takes much + // longer to run. We get confidence about delimiters from our other + // tests. + DateTester::test("d m-y +H:M", Type::DateTime); + } + + #[test] + fn datetime_with_seconds() { + // A more exhaustive template would be "d-m-y +H:M:S", but that takes + // much longer to run. We get confidence about delimiters from our other + // tests. + DateTester::test("d-m y +H M:S", Type::DateTime); + } + + #[test] + fn ymdhms_without_seconds() { + // A more exhaustive template would be "y-m-d +H:M", but that takes much + // longer to run. We get confidence about delimiters from our other tests. + DateTester::test("y m-d +H:M", Type::YmdHms); + } + + #[test] + fn ymdhms_with_seconds() { + // A more exhaustive template would be "y-m-d +H:M:S", but that takes + // much longer to run. We get confidence about delimiters from our other + // tests. + DateTester::test("y-m d +H M:S", Type::YmdHms); + } + + #[derive(Clone, Debug)] + struct TestTime { + days: i32, + hours: i32, + minutes: i32, + seconds: f64, + } + + impl TestTime { + const fn new(days: i32, hours: i32, minutes: i32, seconds: f64) -> Self { + Self { + days, + hours, + minutes, + seconds, } } } - fn days_in_month(year: i32, month: i32) -> i32 { - match month { - 0 => 31, - 1 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29, - 1 => 28, - 2 => 31, - 3 => 30, - 4 => 31, - 5 => 30, - 6 => 31, - 7 => 31, - 8 => 30, - 9 => 31, - 10 => 30, - 11 => 31, - _ => unreachable!(), + struct TimeTester<'a> { + time: TestTime, + template: &'a str, + formatted: String, + type_: Type, + } + + impl<'a> TimeTester<'a> { + fn visit(&self, extra: &str, mut expected: f64, sign: Sign) { + let formatted = format!("{}{extra}", self.formatted); + if !self.template.is_empty() { + let mut iter = self.template.chars(); + let first = iter.next().unwrap(); + let next = TimeTester { + time: self.time.clone(), + template: iter.as_str(), + formatted: formatted.clone(), + type_: self.type_, + }; + match first { + '+' => { + next.visit("", expected, sign); + next.visit("+", expected, Sign::Positive); + next.visit("-", expected, Sign::Negative); + } + 'D' => { + expected += (self.time.days * 86400) as f64; + next.visit(&format!("{}", self.time.days), expected, sign); + next.visit(&format!("{:02}", self.time.days), expected, sign); + } + 'H' => { + expected += (self.time.hours * 3600) as f64; + next.visit(&format!("{}", self.time.hours), expected, sign); + next.visit(&format!("{:02}", self.time.hours), expected, sign); + } + 'M' => { + expected += (self.time.minutes * 60) as f64; + next.visit(&format!("{}", self.time.minutes), expected, sign); + next.visit(&format!("{:02}", self.time.minutes), expected, sign); + } + 'S' => { + expected += self.time.seconds as f64; + next.visit(&format!("{}", self.time.seconds), expected, sign); + next.visit(&format!("{:02}", self.time.seconds), expected, sign); + } + ':' => { + for c in b" :" { + next.visit(&format!("{}", *c as char), expected, sign); + } + } + ' ' => { + next.visit(" ", expected, sign); + } + _ => unreachable!(), + } + } else { + let expected = match sign { + Sign::Positive => expected, + Sign::Negative => -expected, + }; + + let parsed = Format::new(self.type_, 40, 0) + .unwrap() + .parser() + .parse(&formatted, UTF_8) + .unwrap() + .as_number() + .unwrap() + .unwrap(); + assert_eq!((parsed * 1000.0).round(), (expected as f64) * 1000.0); + } } + + fn test(template: &str, type_: Type) { + static TIMES: [TestTime; 20] = [ + TestTime::new(0, 0, 0, 0.00), + TestTime::new(1, 4, 50, 38.68), + TestTime::new(5, 12, 31, 35.82), + TestTime::new(0, 12, 47, 53.41), + TestTime::new(3, 1, 26, 0.69), + TestTime::new(1, 20, 58, 11.19), + TestTime::new(12, 7, 36, 5.98), + TestTime::new(52, 15, 43, 49.27), + TestTime::new(7, 4, 25, 9.24), + TestTime::new(0, 6, 49, 27.89), + TestTime::new(20, 2, 57, 52.56), + TestTime::new(555, 16, 45, 44.12), + TestTime::new(120, 21, 30, 57.27), + TestTime::new(0, 4, 25, 9.98), + TestTime::new(3, 6, 49, 27.24), + TestTime::new(5, 2, 57, 52.13), + TestTime::new(0, 16, 45, 44.35), + TestTime::new(1, 21, 30, 57.32), + TestTime::new(10, 22, 30, 4.27), + TestTime::new(22, 1, 56, 51.18), + ]; + for time in &TIMES { + let visitor = TimeTester { + time: time.clone(), + template, + formatted: "".to_string(), + type_, + }; + visitor.visit("", 0.0, Sign::default()); + } + } + } + + #[test] + fn mtime() { + TimeTester::test("+M:S", Type::MTime); + } + + #[test] + fn time() { + TimeTester::test("+H:M", Type::Time); + TimeTester::test("+H:M:S", Type::Time); + } + + #[test] + fn dtime() { + TimeTester::test("+D H:M", Type::DTime); + TimeTester::test("+D H:M:S", Type::DTime); } } -- 2.30.2