Skip to content

Commit

Permalink
Support dates all the way to 1BC (#77)
Browse files Browse the repository at this point in the history
* set unix 0000

* update docstring

* clippy

* fmt

* revert watershed

* add tests

* add more tests
  • Loading branch information
changhc authored Nov 1, 2024
1 parent 680566a commit b3fc4a5
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 42 deletions.
2 changes: 1 addition & 1 deletion fuzz/fuzz_targets/datetime_from_timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
38 changes: 19 additions & 19 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
///
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 37 additions & 14 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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) {
Expand All @@ -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");
}
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit b3fc4a5

Please sign in to comment.