Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
Comment on lines -151 to -154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we restore these test cases? Not immediately clear to me why they were deleted.

Copy link
Contributor Author

@changhc changhc Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed them because this test case is called date_from_timestamp_extremes and 1600 is no longer "an extreme value" (and I added 0000-01-01 instead.) I'll create a new test case and add them back.

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