Skip to content

Commit

Permalink
fix(common-time): allow building nanos timestamp from parts split fro…
Browse files Browse the repository at this point in the history
…m i64::MIN (#3493)

Signed-off-by: Zhenchi <[email protected]>
  • Loading branch information
zhongzc authored Mar 13, 2024
1 parent cafb470 commit 904484b
Showing 1 changed file with 83 additions and 7 deletions.
90 changes: 83 additions & 7 deletions src/common/time/src/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::{error, Interval};
/// - for [TimeUnit::Second]: [-262144-01-01 00:00:00, +262143-12-31 23:59:59]
/// - for [TimeUnit::Millisecond]: [-262144-01-01 00:00:00.000, +262143-12-31 23:59:59.999]
/// - for [TimeUnit::Microsecond]: [-262144-01-01 00:00:00.000000, +262143-12-31 23:59:59.999999]
/// - for [TimeUnit::Nanosecond]: [1677-09-21 00:12:43.145225, 2262-04-11 23:47:16.854775807]
/// - for [TimeUnit::Nanosecond]: [1677-09-21 00:12:43.145224192, 2262-04-11 23:47:16.854775807]
///
/// # Note:
/// For values out of range, you can still store these timestamps, but while performing arithmetic
Expand Down Expand Up @@ -187,28 +187,28 @@ impl Timestamp {
Self { unit, value }
}

pub fn new_second(value: i64) -> Self {
pub const fn new_second(value: i64) -> Self {
Self {
value,
unit: TimeUnit::Second,
}
}

pub fn new_millisecond(value: i64) -> Self {
pub const fn new_millisecond(value: i64) -> Self {
Self {
value,
unit: TimeUnit::Millisecond,
}
}

pub fn new_microsecond(value: i64) -> Self {
pub const fn new_microsecond(value: i64) -> Self {
Self {
value,
unit: TimeUnit::Microsecond,
}
}

pub fn new_nanosecond(value: i64) -> Self {
pub const fn new_nanosecond(value: i64) -> Self {
Self {
value,
unit: TimeUnit::Nanosecond,
Expand Down Expand Up @@ -281,8 +281,26 @@ impl Timestamp {
.and_then(|v| v.checked_add(micros as i64))
.map(Timestamp::new_microsecond)
} else {
// Refer to <https://github.com/chronotope/chrono/issues/1289>
//
// subsec nanos are always non-negative, however the timestamp itself (both in seconds and in nanos) can be
// negative. Now i64::MIN is NOT dividable by 1_000_000_000, so
//
// (sec * 1_000_000_000) + nsec
//
// may underflow (even when in theory we COULD represent the datetime as i64) because we add the non-negative
// nanos AFTER the multiplication. This is fixed by converting the negative case to
//
// ((sec + 1) * 1_000_000_000) + (nsec - 1_000_000_000)
let mut sec = sec;
let mut nsec = nsec as i64;
if sec < 0 && nsec > 0 {
nsec -= 1_000_000_000;
sec += 1;
}

sec.checked_mul(1_000_000_000)
.and_then(|v| v.checked_add(nsec as i64))
.and_then(|v| v.checked_add(nsec))
.map(Timestamp::new_nanosecond)
}
}
Expand Down Expand Up @@ -425,6 +443,20 @@ impl Timestamp {
}
}

impl Timestamp {
pub const MIN_SECOND: Self = Self::new_second(-8_334_601_228_800);
pub const MAX_SECOND: Self = Self::new_second(8_210_266_876_799);

pub const MIN_MILLISECOND: Self = Self::new_millisecond(-8_334_601_228_800_000);
pub const MAX_MILLISECOND: Self = Self::new_millisecond(8_210_266_876_799_999);

pub const MIN_MICROSECOND: Self = Self::new_microsecond(-8_334_601_228_800_000_000);
pub const MAX_MICROSECOND: Self = Self::new_microsecond(8_210_266_876_799_999_999);

pub const MIN_NANOSECOND: Self = Self::new_nanosecond(i64::MIN);
pub const MAX_NANOSECOND: Self = Self::new_nanosecond(i64::MAX);
}

/// Converts the naive datetime (which has no specific timezone) to a
/// nanosecond epoch timestamp in UTC.
fn naive_datetime_to_timestamp(
Expand Down Expand Up @@ -586,6 +618,7 @@ impl Hash for Timestamp {
mod tests {
use std::collections::hash_map::DefaultHasher;

use chrono_tz::Tz;
use rand::Rng;
use serde_json::Value;

Expand Down Expand Up @@ -1297,7 +1330,7 @@ mod tests {
"+262142-12-31 23:59:59Z",
"+262142-12-31 23:59:59.999Z",
"+262142-12-31 23:59:59.999999Z",
"1677-09-21 00:12:43.145225Z",
"1677-09-21 00:12:43.145224192Z",
"2262-04-11 23:47:16.854775807Z",
"+100000-01-01 00:00:01.5Z",
];
Expand All @@ -1306,4 +1339,47 @@ mod tests {
Timestamp::from_str_utc(s).unwrap();
}
}

#[test]
fn test_min_nanos_roundtrip() {
let (sec, nsec) = Timestamp::MIN_NANOSECOND.split();
let ts = Timestamp::from_splits(sec, nsec).unwrap();
assert_eq!(Timestamp::MIN_NANOSECOND, ts);
}

#[test]
fn test_timestamp_bound_format() {
assert_eq!(
"1677-09-21 00:12:43.145224192",
Timestamp::MIN_NANOSECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"2262-04-11 23:47:16.854775807",
Timestamp::MAX_NANOSECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"-262143-01-01 00:00:00",
Timestamp::MIN_MICROSECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"+262142-12-31 23:59:59.999999",
Timestamp::MAX_MICROSECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"-262143-01-01 00:00:00",
Timestamp::MIN_MILLISECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"+262142-12-31 23:59:59.999",
Timestamp::MAX_MILLISECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"-262143-01-01 00:00:00",
Timestamp::MIN_SECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
assert_eq!(
"+262142-12-31 23:59:59",
Timestamp::MAX_SECOND.to_timezone_aware_string(Some(&Timezone::Named(Tz::UTC)))
);
}
}

0 comments on commit 904484b

Please sign in to comment.