diff --git a/fuzz/fuzz_targets/datetime_from_timestamp.rs b/fuzz/fuzz_targets/datetime_from_timestamp.rs index 5f90753..30dcf5e 100644 --- a/fuzz/fuzz_targets/datetime_from_timestamp.rs +++ b/fuzz/fuzz_targets/datetime_from_timestamp.rs @@ -20,7 +20,7 @@ fn check_timestamp(timestamp: i64, microseconds: u32) { if let Some(mut chrono_dt) = NaiveDateTime::from_timestamp_opt(chrono_seconds, chrono_nano) { let year = chrono_dt.year(); - if year >= 1600 && year <= 9999 { + if year >= 0 && year <= 9999 { let dt = match DateTime::from_timestamp(timestamp, microseconds) { Ok(dt) => dt, Err(e) => panic!( diff --git a/src/date.rs b/src/date.rs index 0b8435f..bbfbcdd 100644 --- a/src/date.rs +++ b/src/date.rs @@ -57,10 +57,10 @@ impl FromStr for Date { // 2e10 if greater than this, the number is in ms, if less than or equal, it's in seconds // (in seconds this is 11th October 2603, in ms it's 20th August 1970) pub(crate) const MS_WATERSHED: i64 = 20_000_000_000; -// 1600-01-01 as a unix timestamp used for from_timestamp below -const UNIX_1600: i64 = -11_676_096_000; // 9999-12-31T23:59:59 as a unix timestamp, used as max allowed value below const UNIX_9999: i64 = 253_402_300_799; +// 0000-01-01T00:00:00+00:00 as a unix timestamp, used as min allowed value below +const UNIX_0000: i64 = -62_167_219_200; impl Date { /// Parse a date from a string using RFC 3339 format @@ -182,15 +182,16 @@ impl Date { /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// - /// Input must be between `-11,676,096,000` (`1600-01-01`) and `253,402,300,799,000` (`9999-12-31`) inclusive. + /// Input must be between `-62,167,219,200,000` (`0000-01-01`) and `253,402,300,799,000` (`9999-12-31`) inclusive. /// /// If the absolute value is > 2e10 (`20,000,000,000`) it is interpreted as being in milliseconds. /// /// That means: - /// * `20_000_000_000` is `2603-10-11` - /// * `20_000_000_001` is `1970-08-20` - /// * `-20_000_000_000` gives an error - `DateTooSmall` as it would be before 1600 - /// * `-20_000_000_001` is `1969-05-14` + /// * `20,000,000,000` is `2603-10-11` + /// * `20,000,000,001` is `1970-08-20` + /// * `-62,167,219,200,001` gives an error - `DateTooSmall` as it would be before 0000-01-01 + /// * `-20,000,000,001` is `1969-05-14` + /// * `-20,000,000,000` is `1336-03-23` /// /// # Arguments /// @@ -225,10 +226,9 @@ impl Date { /// assert_eq!(d.timestamp(), 1_654_560_000); /// ``` pub fn timestamp(&self) -> i64 { - let days = (self.year - 1600) as i64 * 365 - + (self.ordinal_day() - 1) as i64 - + intervening_leap_years(self.year - 1600) as i64; - days * 86400 + UNIX_1600 + let days = + (self.year as i64) * 365 + (self.ordinal_day() - 1) as i64 + intervening_leap_years(self.year as i64); + days * 86400 + UNIX_0000 } /// Current date. Internally, this uses [DateTime::now]. @@ -285,20 +285,20 @@ impl Date { } pub(crate) fn from_timestamp_calc(timestamp_second: i64) -> Result<(Self, u32), ParseError> { - if timestamp_second < UNIX_1600 { + if timestamp_second < UNIX_0000 { return Err(ParseError::DateTooSmall); } if timestamp_second > UNIX_9999 { return Err(ParseError::DateTooLarge); } - let seconds_diff = timestamp_second - UNIX_1600; + let seconds_diff = timestamp_second - UNIX_0000; let delta_days = seconds_diff / 86_400; - let delta_years = (delta_days / 365) as u16; - let leap_years = intervening_leap_years(delta_years) as i64; + let delta_years = delta_days / 365; + let leap_years = intervening_leap_years(delta_years); // year day is the day of the year, starting from 1 let mut ordinal_day: i16 = (delta_days % 365 - leap_years + 1) as i16; - let mut year: u16 = 1600 + delta_years; + let mut year: u16 = delta_years as u16; let mut leap_year: bool = is_leap_year(year); while ordinal_day < 1 { year -= 1; @@ -377,9 +377,9 @@ fn is_leap_year(year: u16) -> bool { } } -/// internal function to calculate the number of leap years since 1600, `delta_years` is the number of -/// years since 1600 -fn intervening_leap_years(delta_years: u16) -> u16 { +/// internal function to calculate the number of leap years since 0000, `delta_years` is the number of +/// years since 0000 +fn intervening_leap_years(delta_years: i64) -> i64 { if delta_years == 0 { 0 } else { diff --git a/src/datetime.rs b/src/datetime.rs index 6f227c4..80788a3 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -440,16 +440,16 @@ impl DateTime { /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// - /// Input must be between `-11_676_096_000` (`1600-01-01T00:00:00`) and - /// `253_402_300_799_000` (`9999-12-31T23:59:59.999999`) inclusive. + /// Input must be between `-62,167,219,200,000` (`0000-01-01`) and `253,402,300,799,000` (`9999-12-31`) inclusive. /// - /// If the absolute value is > 2e10 (`20_000_000_000`) it is interpreted as being in milliseconds. + /// If the absolute value is > 2e10 (`20,000,000,000`) it is interpreted as being in milliseconds. /// /// That means: - /// * `20_000_000_000` is `2603-10-11T11:33:20` - /// * `20_000_000_001` is `1970-08-20T11:33:20.001` - /// * `-20_000_000_000` gives an error - `DateTooSmall` as it would be before 1600 - /// * `-20_000_000_001` is `1969-05-14T12:26:39.999` + /// * `20,000,000,000` is `2603-10-11` + /// * `20,000,000,001` is `1970-08-20` + /// * `-62,167,219,200,001` gives an error - `DateTooSmall` as it would be before 0000-01-01 + /// * `-20,000,000,001` is `1969-05-14` + /// * `-20,000,000,000` is `1336-03-23` /// /// # Arguments /// diff --git a/src/lib.rs b/src/lib.rs index ce3e989..c5545ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ pub enum ParseError { DurationHourValueTooLarge, /// durations may not exceed 999,999,999 days DurationDaysTooLarge, - /// dates before 1600 are not supported as unix timestamps + /// dates before 0000 are not supported as unix timestamps DateTooSmall, /// dates after 9999 are not supported as unix timestamps DateTooLarge, diff --git a/tests/main.rs b/tests/main.rs index 3d329c1..259ec3d 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -129,9 +129,26 @@ param_tests! { date_normal_leap_year: ok => "2004-02-29", "2004-02-29"; date_special_100_not_leap: err => "1900-02-29", OutOfRangeDay; date_special_400_leap: ok => "2000-02-29", "2000-02-29"; + date_special_1600ad_leap: ok => "1600-02-29", "1600-02-29"; + date_special_1200ad_leap: ok => "1200-02-29", "1200-02-29"; + date_special_1201ad_not_leap: err => "1201-02-29", OutOfRangeDay; + date_special_1202ad_not_leap: err => "1202-02-29", OutOfRangeDay; + date_special_1203ad_not_leap: err => "1203-02-29", OutOfRangeDay; + date_special_1204ad_leap: ok => "1204-02-29", "1204-02-29"; + date_special_1300ad_not_leap: err => "1300-02-29", OutOfRangeDay; + date_special_1400ad_not_leap: err => "1400-02-29", OutOfRangeDay; + date_special_1500ad_not_leap: err => "1500-02-29", OutOfRangeDay; + date_special_1bc_leap: ok => "0000-02-29", "0000-02-29"; + date_special_1ad_not_leap: err => "0001-02-29", OutOfRangeDay; + date_special_4ad_leap: ok => "0004-02-29", "0004-02-29"; + date_special_100ad_not_leap: err => "0100-02-29", OutOfRangeDay; + date_special_200ad_not_leap: err => "0200-02-29", OutOfRangeDay; + date_special_300ad_not_leap: err => "0300-02-29", OutOfRangeDay; + date_special_400ad_leap: ok => "0400-02-29", "0400-02-29"; + date_special_404ad_leap: ok => "0404-02-29", "0404-02-29"; date_unix_before_watershed: ok => "19999872000", "2603-10-10"; date_unix_after_watershed: ok => "20044800000", "1970-08-21"; - date_unix_too_low: err => "-20000000000", DateTooSmall; + date_unix_too_low: err => "-62167219200001", DateTooSmall; } #[test] @@ -144,14 +161,12 @@ fn date_from_timestamp_extremes() { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooLarge), } - match Date::from_timestamp(-30_610_224_000_000, false) { + let d = Date::from_timestamp(-62_167_219_200_000, false).unwrap(); + assert_eq!(d.to_string(), "0000-01-01"); + match Date::from_timestamp(-62_167_219_200_001, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooSmall), } - let d = Date::from_timestamp(-11_676_096_000 + 1000, false).unwrap(); - assert_eq!(d.to_string(), "1600-01-01"); - let d = Date::from_timestamp(-11_673_417_600, false).unwrap(); - assert_eq!(d.to_string(), "1600-02-01"); let d = Date::from_timestamp(253_402_300_799_000, false).unwrap(); assert_eq!(d.to_string(), "9999-12-31"); match Date::from_timestamp(253_402_300_800_000, false) { @@ -160,16 +175,26 @@ fn date_from_timestamp_extremes() { } } +#[test] +fn date_from_timestamp_special_dates() { + let d = Date::from_timestamp(-11_676_096_000 + 1000, false).unwrap(); + assert_eq!(d.to_string(), "1600-01-01"); + // check if there is any error regarding offset at the second level + // and if rounding down works + let d = Date::from_timestamp(-11_676_096_000 + 86399, false).unwrap(); + assert_eq!(d.to_string(), "1600-01-01"); + let d = Date::from_timestamp(-11_673_417_600, false).unwrap(); + assert_eq!(d.to_string(), "1600-02-01"); +} + #[test] fn date_watershed() { let dt = Date::from_timestamp(20_000_000_000, false).unwrap(); assert_eq!(dt.to_string(), "2603-10-11"); let dt = Date::from_timestamp(20_000_000_001, false).unwrap(); assert_eq!(dt.to_string(), "1970-08-20"); - match Date::from_timestamp(-20_000_000_000, false) { - Ok(d) => panic!("unexpectedly valid, {d}"), - Err(e) => assert_eq!(e, ParseError::DateTooSmall), - } + let dt = Date::from_timestamp(-20_000_000_000, false).unwrap(); + assert_eq!(dt.to_string(), "1336-03-23"); let dt = Date::from_timestamp(-20_000_000_001, false).unwrap(); assert_eq!(dt.to_string(), "1969-05-14"); } @@ -462,12 +487,10 @@ fn datetime_watershed() { assert_eq!(dt.to_string(), "2603-10-11T11:33:20"); let dt = DateTime::from_timestamp(20_000_000_001, 0).unwrap(); assert_eq!(dt.to_string(), "1970-08-20T11:33:20.001000"); - match DateTime::from_timestamp(-20_000_000_000, 0) { - Ok(dt) => panic!("unexpectedly valid, {dt}"), - Err(e) => assert_eq!(e, ParseError::DateTooSmall), - } let dt = DateTime::from_timestamp(-20_000_000_001, 0).unwrap(); assert_eq!(dt.to_string(), "1969-05-14T12:26:39.999000"); + let dt = DateTime::from_timestamp(-20_000_000_000, 0).unwrap(); + assert_eq!(dt.to_string(), "1336-03-23T12:26:40"); } #[test]