Skip to content

Commit

Permalink
prevent non-exact dates from timestamp
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed May 29, 2023
1 parent 8be0ab4 commit 20a1c48
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 23 deletions.
16 changes: 12 additions & 4 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ impl Date {
match Self::parse_bytes_rfc3339(bytes) {
Ok(d) => Ok(d),
Err(e) => match int_parse_bytes(bytes) {
Some(int) => Self::from_timestamp(int),
Some(int) => Self::from_timestamp(int, true),
None => Err(e),
},
}
Expand All @@ -183,18 +183,26 @@ impl Date {
/// # Arguments
///
/// * `timestamp` - timestamp in either seconds or milliseconds
/// * `require_exact` - if true, then the timestamp must be exactly at midnight, otherwise it will be rounded down
///
/// # Examples
///
/// ```
/// use speedate::Date;
///
/// let d = Date::from_timestamp(1_654_560_000).unwrap();
/// let d = Date::from_timestamp(1_654_560_000, true).unwrap();
/// assert_eq!(d.to_string(), "2022-06-07");
/// ```
pub fn from_timestamp(timestamp: i64) -> Result<Self, ParseError> {
pub fn from_timestamp(timestamp: i64, require_exact: bool) -> Result<Self, ParseError> {
let (timestamp_second, _) = Self::timestamp_watershed(timestamp)?;
Self::from_timestamp_calc(timestamp_second)
let d = Self::from_timestamp_calc(timestamp_second)?;
if require_exact {
let time_second = timestamp_second.rem_euclid(86_400);
if time_second != 0 {
return Err(ParseError::DateNotExact);
}
}
Ok(d)
}

/// Unix timestamp in seconds (number of seconds between self and 1970-01-01)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub enum ParseError {
InvalidCharDateTimeSep,
/// invalid date separator, expected `-`
InvalidCharDateSep,
/// Timestamp is not an exact date
DateNotExact,
/// invalid character in year
InvalidCharYear,
/// invalid character in month
Expand Down
43 changes: 24 additions & 19 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,54 +99,54 @@ 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_unix_before_watershed: ok => "20000000000", "2603-10-11";
date_unix_after_watershed: ok => "20000000001", "1970-08-20";
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;
}

#[test]
fn date_from_timestamp_extremes() {
match Date::from_timestamp(i64::MIN) {
match Date::from_timestamp(i64::MIN, false) {
Ok(dt) => panic!("unexpectedly valid, {}", dt),
Err(e) => assert_eq!(e, ParseError::DateTooSmall),
}
match Date::from_timestamp(i64::MAX) {
match Date::from_timestamp(i64::MAX, false) {
Ok(dt) => panic!("unexpectedly valid, {}", dt),
Err(e) => assert_eq!(e, ParseError::DateTooLarge),
}
match Date::from_timestamp(-30_610_224_000_000) {
match Date::from_timestamp(-30_610_224_000_000, false) {
Ok(dt) => panic!("unexpectedly valid, {}", dt),
Err(e) => assert_eq!(e, ParseError::DateTooSmall),
}
let d = Date::from_timestamp(-11_676_096_000 + 1000).unwrap();
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).unwrap();
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).unwrap();
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) {
match Date::from_timestamp(253_402_300_800_000, false) {
Ok(dt) => panic!("unexpectedly valid, {}", dt),
Err(e) => assert_eq!(e, ParseError::DateTooLarge),
}
}

#[test]
fn date_watershed() {
let dt = Date::from_timestamp(20_000_000_000).unwrap();
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).unwrap();
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) {
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_001).unwrap();
let dt = Date::from_timestamp(-20_000_000_001, false).unwrap();
assert_eq!(dt.to_string(), "1969-05-14");
}

#[test]
fn date_from_timestamp_milliseconds() {
let d1 = Date::from_timestamp(1_654_472_524).unwrap();
let d1 = Date::from_timestamp(1_654_472_524, false).unwrap();
assert_eq!(
d1,
Date {
Expand All @@ -155,13 +155,13 @@ fn date_from_timestamp_milliseconds() {
day: 5
}
);
let d2 = Date::from_timestamp(1_654_472_524_000).unwrap();
let d2 = Date::from_timestamp(1_654_472_524_000, false).unwrap();
assert_eq!(d2, d1);
}

fn try_date_timestamp(ts: i64, check_timestamp: bool) {
let chrono_date = NaiveDateTime::from_timestamp_opt(ts, 0).unwrap().date();
let d = Date::from_timestamp(ts).unwrap();
let d = Date::from_timestamp(ts, false).unwrap();
// println!("{} => {:?}", ts, d);
assert_eq!(
d,
Expand Down Expand Up @@ -203,9 +203,14 @@ fn date_comparison() {

#[test]
fn date_timestamp() {
let d = Date::from_timestamp(1_654_560_000).unwrap();
let d = Date::from_timestamp(1_654_560_000, true).unwrap();
assert_eq!(d.to_string(), "2022-06-07");
assert_eq!(d.timestamp(), 1_654_560_000);

match Date::from_timestamp(1_654_560_001, true) {
Ok(d) => panic!("unexpectedly valid, {}", d),
Err(e) => assert_eq!(e, ParseError::DateNotExact),
}
}

macro_rules! date_from_timestamp {
Expand All @@ -216,7 +221,7 @@ macro_rules! date_from_timestamp {
fn [< date_from_timestamp_ $year _ $month _ $day >]() {
let chrono_date = NaiveDate::from_ymd_opt($year, $month, $day).unwrap();
let ts = chrono_date.and_hms_opt(0, 0, 0).unwrap().timestamp();
let d = Date::from_timestamp(ts).unwrap();
let d = Date::from_timestamp(ts, false).unwrap();
assert_eq!(
d,
Date {
Expand Down Expand Up @@ -409,7 +414,7 @@ fn datetime_from_timestamp_specific() {

let d = DateTime::from_timestamp(253_402_300_799_000, 999999).unwrap();
assert_eq!(d.to_string(), "9999-12-31T23:59:59.999999");
match Date::from_timestamp(253_402_300_800_000) {
match Date::from_timestamp(253_402_300_800_000, false) {
Ok(dt) => panic!("unexpectedly valid, {}", dt),
Err(e) => assert_eq!(e, ParseError::DateTooLarge),
}
Expand Down

0 comments on commit 20a1c48

Please sign in to comment.