From d76e1b4b91c2d740c8ef9090d7264429cf719703 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 24 Jun 2018 23:53:33 -0400 Subject: [PATCH] Weekday support --- build_pycompat.py | 6 ++- src/lib.rs | 35 +++++++++--- src/weekday.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++++ tests/pycompat.rs | 108 ++++++++++++++++++------------------- 4 files changed, 215 insertions(+), 66 deletions(-) create mode 100644 src/weekday.rs diff --git a/build_pycompat.py b/build_pycompat.py index 03a6c1d..0ab2af2 100644 --- a/build_pycompat.py +++ b/build_pycompat.py @@ -14,7 +14,10 @@ tests = { # testPertain 'Sep 03', 'Sep of 03', # test_hmBY - Note: This appears to be Python 3 only, no idea why - '02:17NOV2017' + '02:17NOV2017', + # Weekdays + "Thu Sep 10:36:28", "Thu 10:36:28", "Wed", "Wednesday" + # TODO: Tests for when forwarding to the ], 'test_parse_simple': [ "Thu Sep 25 10:36:28 2003", "Thu Sep 25 2003", "2003-09-25T10:49:41", @@ -79,7 +82,6 @@ tests = { 'Tue Apr 4 00:22:12 PDT 1995' ], 'test_parse_default_ignore': [ - "Thu Sep 10:36:28", "Thu 10:36:28", "Wed", "Wednesday" ], } diff --git a/src/lib.rs b/src/lib.rs index ddd3bf9..85c043f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ extern crate rust_decimal; use chrono::DateTime; use chrono::Datelike; +use chrono::Duration; use chrono::FixedOffset; use chrono::Local; use chrono::NaiveDate; @@ -26,6 +27,11 @@ use std::num::ParseIntError; use std::str::FromStr; use std::vec::Vec; +mod weekday; + +use weekday::day_of_week; +use weekday::DayOfWeek; + lazy_static! { static ref ZERO: Decimal = Decimal::new(0, 0); static ref ONE: Decimal = Decimal::new(1, 0); @@ -62,6 +68,7 @@ impl From for ParseInternalError { pub enum ParseError { AmbiguousWeekday, InternalError(ParseInternalError), + InvalidDay, InvalidMonth, UnrecognizedToken(String), InvalidParseResult(ParsingResult), @@ -760,7 +767,7 @@ pub struct ParsingResult { year: Option, month: Option, day: Option, - weekday: Option, + weekday: Option, hour: Option, minute: Option, second: Option, @@ -848,7 +855,7 @@ impl Parser { if let Ok(v) = Decimal::from_str(&value_repr) { i = self.parse_numeric_token(&l, i, &self.info, &mut ymd, &mut res, fuzzy)?; } else if let Some(value) = self.info.get_weekday(&l[i]) { - res.weekday = Some(value != 0); + res.weekday = Some(value); } else if let Some(value) = self.info.get_month(&l[i]) { ymd.append(value as i32, &l[i], Some(YMDLabel::Month)); @@ -1013,21 +1020,33 @@ impl Parser { } fn build_naive(&self, res: &ParsingResult, default: &NaiveDateTime) -> ParseResult { - // TODO: Handle weekday here - dateutils uses relativedelta to accomplish this - if res.weekday.is_some() && res.day.is_none() { - return Err(ParseError::AmbiguousWeekday); - } - let y = res.year.unwrap_or(default.year()); let m = res.month.unwrap_or(default.month() as i32) as u32; + let d_offset = if res.weekday.is_some() && res.day.is_none() { + // TODO: Unwrap not justified + let dow = day_of_week(y as u32, m, default.day()).unwrap(); + println!("dow: {:?}", dow); + + // UNWRAP: We've already check res.weekday() is some + let actual_weekday = (res.weekday.unwrap() + 1) % 7; + let other = DayOfWeek::from_numeral(actual_weekday as u32); + Duration::days(dow.difference(other) as i64) + } else { + Duration::days(0) + }; + // TODO: Change month/day to u32 - let d = NaiveDate::from_ymd( + let mut d = NaiveDate::from_ymd( y, m, min(res.day.unwrap_or(default.day() as i32) as u32, days_in_month(y, m as i32).unwrap()) ); + println!("d: {:?}, d_offset: {:?}", d, d_offset); + + let d = d + d_offset; + let t = NaiveTime::from_hms_micro( res.hour.unwrap_or(default.hour() as i32) as u32, res.minute.unwrap_or(default.minute() as i32) as u32, diff --git a/src/weekday.rs b/src/weekday.rs new file mode 100644 index 0000000..516d452 --- /dev/null +++ b/src/weekday.rs @@ -0,0 +1,132 @@ +use std::cmp::max; + +use ParseResult; +use ParseError; + +#[derive(Debug, PartialEq)] +pub enum DayOfWeek { + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday +} + +impl DayOfWeek { + + pub fn to_numeral(&self) -> u32 { + match self { + DayOfWeek::Sunday => 0, + DayOfWeek::Monday => 1, + DayOfWeek::Tuesday => 2, + DayOfWeek::Wednesday => 3, + DayOfWeek::Thursday => 4, + DayOfWeek::Friday => 5, + DayOfWeek::Saturday => 6, + } + } + + pub fn from_numeral(num: u32) -> DayOfWeek { + match num % 7 { + 0 => DayOfWeek::Sunday, + 1 => DayOfWeek::Monday, + 2 => DayOfWeek::Tuesday, + 3 => DayOfWeek::Wednesday, + 4 => DayOfWeek::Thursday, + 5 => DayOfWeek::Friday, + 6 => DayOfWeek::Saturday, + _ => panic!("Unreachable.") + } + } + + /// Given the current day of the week, how many days until the next day? + pub fn difference(&self, other: DayOfWeek) -> u32 { + // Have to use i32 because of wraparound issues + let s_num = self.to_numeral() as i32; + let o_num = other.to_numeral() as i32; + + if o_num - s_num >= 0 { + (o_num - s_num) as u32 + } else { + (7 + o_num - s_num) as u32 + } + } +} + +pub fn day_of_week(year: u32, month: u32, day: u32) -> ParseResult { + // From https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Schwerdtfeger's_method + let (c, g) = match month { + 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 => { + let c = year / 100; + (c, year - 100 * c) + }, + 1 | 2 => { + let c = (year - 1) / 100; + (c, year - 1 - 100 * c) + }, + _ => return Err(ParseError::InvalidMonth) + }; + + let e = match month { + 1 | 5 => 0, + 2 | 6 => 3, + 3 | 11 => 2, + 4 | 7 => 5, + 8 => 1, + 9 | 12 => 4, + 10 => 6, + _ => panic!("Unreachable.") + }; + + // This implementation is Gregorian-only. + let f = match c % 4 { + 0 => 0, + 1 => 5, + 2 => 3, + 3 => 1, + _ => panic!("Unreachable.") + }; + + match (day + e + f + g + g / 4) % 7 { + 0 => Ok(DayOfWeek::Sunday), + 1 => Ok(DayOfWeek::Monday), + 2 => Ok(DayOfWeek::Tuesday), + 3 => Ok(DayOfWeek::Wednesday), + 4 => Ok(DayOfWeek::Thursday), + 5 => Ok(DayOfWeek::Friday), + 6 => Ok(DayOfWeek::Saturday), + _ => panic!("Unreachable.") + } +} + +mod test { + + use weekday::day_of_week; + use weekday::DayOfWeek; + + #[test] + fn day_of_week_examples() { + assert_eq!(day_of_week(2018, 6, 24).unwrap(), DayOfWeek::Sunday); + assert_eq!(day_of_week(2003, 9, 25).unwrap(), DayOfWeek::Thursday); + } + + #[test] + fn weekday_difference() { + + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Sunday), 0); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Monday), 1); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Tuesday), 2); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Wednesday), 3); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Thursday), 4); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Friday), 5); + assert_eq!(DayOfWeek::Sunday.difference(DayOfWeek::Saturday), 6); + assert_eq!(DayOfWeek::Monday.difference(DayOfWeek::Sunday), 6); + assert_eq!(DayOfWeek::Tuesday.difference(DayOfWeek::Sunday), 5); + assert_eq!(DayOfWeek::Wednesday.difference(DayOfWeek::Sunday), 4); + assert_eq!(DayOfWeek::Thursday.difference(DayOfWeek::Sunday), 3); + assert_eq!(DayOfWeek::Friday.difference(DayOfWeek::Sunday), 2); + assert_eq!(DayOfWeek::Saturday.difference(DayOfWeek::Sunday), 1); + } +} \ No newline at end of file diff --git a/tests/pycompat.rs b/tests/pycompat.rs index 9118b4b..8e7805c 100644 --- a/tests/pycompat.rs +++ b/tests/pycompat.rs @@ -639,6 +639,58 @@ fn test_parse_default42() { Some(default_rsdate), false, HashMap::new()); } +#[test] +fn test_parse_default43() { + let info = ParserInfo::default(); + let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); + let pdt = PyDateTime { + year: 2003, month: 9, day: 25, + hour: 10, minute: 36, second: 28, + micros: 0, tzo: None + }; + parse_and_assert(pdt, info, "Thu Sep 10:36:28", None, None, false, false, + Some(default_rsdate), false, HashMap::new()); +} + +#[test] +fn test_parse_default44() { + let info = ParserInfo::default(); + let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); + let pdt = PyDateTime { + year: 2003, month: 9, day: 25, + hour: 10, minute: 36, second: 28, + micros: 0, tzo: None + }; + parse_and_assert(pdt, info, "Thu 10:36:28", None, None, false, false, + Some(default_rsdate), false, HashMap::new()); +} + +#[test] +fn test_parse_default45() { + let info = ParserInfo::default(); + let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); + let pdt = PyDateTime { + year: 2003, month: 10, day: 1, + hour: 0, minute: 0, second: 0, + micros: 0, tzo: None + }; + parse_and_assert(pdt, info, "Wed", None, None, false, false, + Some(default_rsdate), false, HashMap::new()); +} + +#[test] +fn test_parse_default46() { + let info = ParserInfo::default(); + let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); + let pdt = PyDateTime { + year: 2003, month: 10, day: 1, + hour: 0, minute: 0, second: 0, + micros: 0, tzo: None + }; + parse_and_assert(pdt, info, "Wednesday", None, None, false, false, + Some(default_rsdate), false, HashMap::new()); +} + #[test] fn test_parse_simple0() { let pdt = PyDateTime { @@ -1645,59 +1697,3 @@ fn test_parse_ignoretz7() { parse_and_assert(pdt, info, "Tue Apr 4 00:22:12 PDT 1995", None, None, false, false, None, true, HashMap::new()); } - -#[test] -#[ignore] -fn test_parse_default_ignore0() { - let info = ParserInfo::default(); - let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); - let pdt = PyDateTime { - year: 2003, month: 9, day: 25, - hour: 10, minute: 36, second: 28, - micros: 0, tzo: None - }; - parse_and_assert(pdt, info, "Thu Sep 10:36:28", None, None, false, false, - Some(default_rsdate), false, HashMap::new()); -} - -#[test] -#[ignore] -fn test_parse_default_ignore1() { - let info = ParserInfo::default(); - let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); - let pdt = PyDateTime { - year: 2003, month: 9, day: 25, - hour: 10, minute: 36, second: 28, - micros: 0, tzo: None - }; - parse_and_assert(pdt, info, "Thu 10:36:28", None, None, false, false, - Some(default_rsdate), false, HashMap::new()); -} - -#[test] -#[ignore] -fn test_parse_default_ignore2() { - let info = ParserInfo::default(); - let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); - let pdt = PyDateTime { - year: 2003, month: 10, day: 1, - hour: 0, minute: 0, second: 0, - micros: 0, tzo: None - }; - parse_and_assert(pdt, info, "Wed", None, None, false, false, - Some(default_rsdate), false, HashMap::new()); -} - -#[test] -#[ignore] -fn test_parse_default_ignore3() { - let info = ParserInfo::default(); - let default_rsdate = &NaiveDate::from_ymd(2003, 9, 25).and_hms(0, 0, 0); - let pdt = PyDateTime { - year: 2003, month: 10, day: 1, - hour: 0, minute: 0, second: 0, - micros: 0, tzo: None - }; - parse_and_assert(pdt, info, "Wednesday", None, None, false, false, - Some(default_rsdate), false, HashMap::new()); -}