#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date {
pub days_since_epoch: i32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Time {
pub micros_since_midnight: i64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp {
pub micros_since_epoch: i64,
}
impl Date {
#[must_use]
pub fn new(days_since_epoch: i32) -> Self {
Self { days_since_epoch }
}
}
impl Time {
#[must_use]
pub fn new(micros_since_midnight: i64) -> Self {
Self {
micros_since_midnight,
}
}
}
impl Timestamp {
#[must_use]
pub fn new(micros_since_epoch: i64) -> Self {
Self { micros_since_epoch }
}
}
#[cfg(feature = "chrono")]
mod chrono_interop {
use super::{Date, Time, Timestamp};
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
const DAYS_CE_TO_UNIX_EPOCH: i32 = 719_163;
const MICROS_PER_SEC: i64 = 1_000_000;
const NANOS_PER_MICRO: u32 = 1_000;
impl From<Date> for NaiveDate {
fn from(value: Date) -> Self {
NaiveDate::from_num_days_from_ce_opt(value.days_since_epoch + DAYS_CE_TO_UNIX_EPOCH)
.expect("XL_T_DATE value is outside chrono::NaiveDate's representable range")
}
}
impl From<NaiveDate> for Date {
fn from(value: NaiveDate) -> Self {
Date::new(value.num_days_from_ce() - DAYS_CE_TO_UNIX_EPOCH)
}
}
impl From<Time> for NaiveTime {
fn from(value: Time) -> Self {
let secs = value.micros_since_midnight.div_euclid(MICROS_PER_SEC);
let micros = value.micros_since_midnight.rem_euclid(MICROS_PER_SEC);
NaiveTime::from_num_seconds_from_midnight_opt(
u32::try_from(secs)
.expect("XL_T_TIME value is outside chrono::NaiveTime's representable range"),
micros as u32 * NANOS_PER_MICRO,
)
.expect("XL_T_TIME value is outside chrono::NaiveTime's representable range")
}
}
impl From<NaiveTime> for Time {
fn from(value: NaiveTime) -> Self {
let secs = i64::from(value.num_seconds_from_midnight());
let micros = i64::from(value.nanosecond() / NANOS_PER_MICRO);
Time::new(secs * MICROS_PER_SEC + micros)
}
}
impl From<Timestamp> for NaiveDateTime {
fn from(value: Timestamp) -> Self {
DateTime::from_timestamp_micros(value.micros_since_epoch)
.expect(
"XL_T_TIMESTAMP value is outside chrono::NaiveDateTime's representable range",
)
.naive_utc()
}
}
impl From<NaiveDateTime> for Timestamp {
fn from(value: NaiveDateTime) -> Self {
Timestamp::new(value.and_utc().timestamp_micros())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn newtypes_carry_the_wire_value_verbatim() {
assert_eq!(Date::new(-1).days_since_epoch, -1);
assert_eq!(Time::new(0).micros_since_midnight, 0);
assert_eq!(Timestamp::new(i64::MIN).micros_since_epoch, i64::MIN);
}
#[cfg(feature = "chrono")]
mod chrono {
use super::*;
use ::chrono::{NaiveDate, NaiveDateTime, NaiveTime};
#[test]
fn date_zero_is_the_unix_epoch() {
let date: NaiveDate = Date::new(0).into();
assert_eq!(date, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
}
#[test]
fn date_round_trips_through_chrono_in_both_directions() {
for days in [-25_567, -1, 0, 1, 19_000, 2_958_465 - 25_569] {
let date: NaiveDate = Date::new(days).into();
assert_eq!(Date::from(date), Date::new(days), "days = {days}");
}
}
#[test]
fn time_keeps_microsecond_precision() {
let time: NaiveTime = Time::new(13 * 3_600_000_000 + 45 * 60_000_000 + 1).into();
assert_eq!(time, NaiveTime::from_hms_micro_opt(13, 45, 0, 1).unwrap());
assert_eq!(
Time::from(time),
Time::new(13 * 3_600_000_000 + 45 * 60_000_000 + 1)
);
}
#[test]
fn timestamp_round_trips_across_the_epoch() {
for micros in [-1_000_000, -1, 0, 1, 1_700_000_000_000_000] {
let stamp: NaiveDateTime = Timestamp::new(micros).into();
assert_eq!(
Timestamp::from(stamp),
Timestamp::new(micros),
"micros = {micros}"
);
}
}
}
}