use std::str::FromStr;
use chrono::{prelude::*, Duration, LocalResult};
use ordered_float::NotNan;
use serde::{Deserialize, Serialize, Serializer};
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NumericDate(#[serde(serialize_with = "interop_serialize")] NotNan<f64>);
fn interop_serialize<S>(x: &f64, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if x.fract() != 0.0 {
s.serialize_f64(*x)
} else {
s.serialize_i64(*x as i64)
}
}
#[derive(thiserror::Error, Debug)]
pub enum NumericDateConversionError {
#[error("Not a number")]
Nan,
#[error("Invalid float literal")]
InvalidFloatLiteral,
}
impl From<ordered_float::FloatIsNan> for NumericDateConversionError {
fn from(_: ordered_float::FloatIsNan) -> Self {
Self::Nan
}
}
impl NumericDate {
pub fn as_seconds(self) -> f64 {
*self.0
}
pub fn try_from_seconds(seconds: f64) -> Result<Self, NumericDateConversionError> {
let seconds = NotNan::new(seconds)?;
Ok(NumericDate(seconds))
}
fn into_whole_seconds_and_fractional_nanoseconds(self) -> (i64, u32) {
let whole_seconds = self.0.floor() as i64;
let fractional_nanoseconds = ((self.0 - self.0.floor()) * 1_000_000_000.0).floor() as u32;
assert!(fractional_nanoseconds < 1_000_000_000);
(whole_seconds, fractional_nanoseconds)
}
}
impl std::ops::Add<Duration> for NumericDate {
type Output = NumericDate;
fn add(self, rhs: Duration) -> Self::Output {
let self_dtu: DateTime<Utc> = self.into();
Self::Output::from(self_dtu + rhs)
}
}
impl std::ops::Sub<NumericDate> for NumericDate {
type Output = Duration;
fn sub(self, rhs: NumericDate) -> Self::Output {
let self_dtu: DateTime<Utc> = self.into();
let rhs_dtu: DateTime<Utc> = rhs.into();
self_dtu - rhs_dtu
}
}
impl std::ops::Sub<Duration> for NumericDate {
type Output = NumericDate;
fn sub(self, rhs: Duration) -> Self::Output {
let self_dtu: DateTime<Utc> = self.into();
Self::Output::from(self_dtu - rhs)
}
}
impl From<i32> for NumericDate {
fn from(value: i32) -> Self {
Self(NotNan::new(value as f64).unwrap())
}
}
impl TryFrom<i64> for NumericDate {
type Error = NumericDateConversionError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
Self::try_from_seconds(value as f64)
}
}
impl TryFrom<f64> for NumericDate {
type Error = NumericDateConversionError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
Self::try_from_seconds(value)
}
}
impl From<DateTime<Utc>> for NumericDate {
fn from(dtu: DateTime<Utc>) -> Self {
let whole_seconds = dtu.timestamp() as f64;
let fractional_seconds = match dtu.timestamp_nanos_opt() {
Some(nanos) => nanos.rem_euclid(1_000_000_000) as f64 * 1.0e-9,
None => dtu.timestamp_micros().rem_euclid(1_000_000) as f64 * 1.0e-6,
};
Self::try_from_seconds(whole_seconds + fractional_seconds)
.unwrap()
}
}
impl From<DateTime<FixedOffset>> for NumericDate {
fn from(dtfo: DateTime<FixedOffset>) -> Self {
DateTime::<Utc>::from(dtfo).into()
}
}
impl From<NumericDate> for DateTime<Utc> {
fn from(nd: NumericDate) -> Self {
let (whole_seconds, fractional_nanoseconds) =
nd.into_whole_seconds_and_fractional_nanoseconds();
Utc.timestamp_opt(whole_seconds, fractional_nanoseconds)
.unwrap()
}
}
impl From<NumericDate> for LocalResult<DateTime<Utc>> {
fn from(nd: NumericDate) -> Self {
let (whole_seconds, fractional_nanoseconds) =
nd.into_whole_seconds_and_fractional_nanoseconds();
Utc.timestamp_opt(whole_seconds, fractional_nanoseconds)
}
}
impl FromStr for NumericDate {
type Err = NumericDateConversionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let f: NotNan<f64> = s
.parse()
.map_err(|_| NumericDateConversionError::InvalidFloatLiteral)?;
Ok(Self(f))
}
}