use chrono::DateTime;
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timestamp {
seconds: i64,
nanos: u32,
}
impl Timestamp {
pub const fn new(unix_seconds: i64, nanoseconds: u32) -> Option<Self> {
if nanoseconds < 1_000_000_000 {
Some(Self {
seconds: unix_seconds,
nanos: nanoseconds,
})
} else {
None
}
}
pub const fn unix_seconds(self) -> i64 {
self.seconds
}
pub const fn nanoseconds(self) -> u32 {
self.nanos
}
}
pub(crate) fn rfc3339(value: &Value) -> Option<Timestamp> {
let text = value.as_str()?;
if text
.split_once('.')
.is_some_and(|(_, tail)| tail.bytes().take_while(u8::is_ascii_digit).count() > 9)
{
return None;
}
let date = DateTime::parse_from_rfc3339(text).ok()?;
Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos())
}
pub(crate) fn milliseconds(value: &Value) -> Option<Timestamp> {
let millis = value.as_i64()?;
Timestamp::new(
millis.div_euclid(1000),
(millis.rem_euclid(1000) as u32) * 1_000_000,
)
}
pub(crate) fn seconds(value: &Value) -> Option<Timestamp> {
let number = value.as_number()?.to_string();
let negative = number.starts_with('-');
let unsigned = number.strip_prefix('-').unwrap_or(&number);
let (mantissa, exponent) = unsigned.split_once(['e', 'E']).unwrap_or((unsigned, "0"));
let fraction_len = mantissa
.split_once('.')
.map_or(0, |(_, fraction)| fraction.len());
let significant = mantissa.trim_end_matches('0');
let trailing_zeros = mantissa.len() - significant.len();
let coefficient = significant
.bytes()
.filter(|byte| *byte != b'.')
.try_fold(0_i128, |total, digit| {
total.checked_mul(10)?.checked_add(i128::from(digit - b'0'))
})?;
if coefficient == 0 {
return Timestamp::new(0, 0);
}
let scale = exponent
.parse::<i64>()
.ok()?
.checked_sub(i64::try_from(fraction_len).ok()?)?
.checked_add(i64::try_from(trailing_zeros).ok()?)?
.checked_add(9)?;
let factor = 10_i128.checked_pow(u32::try_from(scale).ok()?)?;
let total = coefficient.checked_mul(factor)?;
let total = if negative {
total.checked_neg()?
} else {
total
};
Timestamp::new(
total.div_euclid(1_000_000_000).try_into().ok()?,
total.rem_euclid(1_000_000_000) as u32,
)
}