}
impl Format {
- fn parser(&self) -> ParseValue {
+ pub fn parser(&self) -> ParseValue {
ParseValue::new(*self)
}
}
| Type::MoYr
| Type::WkYr
| Type::DateTime
- | Type::YMDHMS
+ | Type::YmdHms
| Type::MTime
| Type::Time
| Type::DTime => self.parse_date(s),
}
write!(&mut number, "{exponent}").unwrap();
}
- println!("{number}");
match f64::from_str(&number) {
Ok(value) => Ok(Value::Number(Some(value))),
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 {
.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
}
}
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum Sign {
+ #[default]
Positive,
Negative,
}
if !(1..=366).contains(&yday) {
return Err(ParseErrorKind::InvalidYDay(yday));
}
- p.0 = &p.0[..3];
+ p.0 = &p.0[3..];
Ok(yday)
}
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);
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);
}
}
settings: &Settings,
max_digits: usize,
) -> Result<i32, ParseErrorKind> {
- 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::<i32>()
.map_err(|_| ParseErrorKind::DateSyntax)?;
};
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) {
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())
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,
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<i32> {
match y {
}
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;
];
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);
}
}