use crate::constants::{J2000_UTC_UNIX_MS, MS_PER_DAY};
use crate::types::BrightDateError;
use chrono::{DateTime, TimeZone, Utc};
pub const PS_PER_NS: i128 = 1_000;
pub const PS_PER_US: i128 = 1_000_000;
pub const PS_PER_MS: i128 = 1_000_000_000;
pub const PS_PER_S: i128 = 1_000_000_000_000;
pub const PS_PER_DAY: i128 = 86_400 * PS_PER_S;
pub const J2000_UNIX_MS_I64: i64 = 946_727_935_816;
pub const J2000_UNIX_PS: i128 = (J2000_UNIX_MS_I64 as i128) * PS_PER_MS;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct ExactBrightDate {
picoseconds: i128,
}
impl ExactBrightDate {
#[inline]
pub const fn from_picoseconds(picoseconds: i128) -> Self {
Self { picoseconds }
}
#[inline]
pub const fn from_unix_ms(unix_ms: i64) -> Self {
Self {
picoseconds: ((unix_ms - J2000_UNIX_MS_I64) as i128) * PS_PER_MS,
}
}
#[inline]
pub const fn from_unix_seconds(unix_seconds: i64) -> Self {
Self {
picoseconds: (unix_seconds as i128) * PS_PER_S - J2000_UNIX_PS,
}
}
#[inline]
pub const fn from_unix_nanos(unix_nanos: i128) -> Self {
Self {
picoseconds: unix_nanos * PS_PER_NS - J2000_UNIX_PS,
}
}
pub fn from_date_time(dt: DateTime<Utc>) -> Self {
Self::from_unix_ms(dt.timestamp_millis())
}
pub fn from_iso(s: &str) -> Result<Self, BrightDateError> {
let dt = DateTime::parse_from_rfc3339(s)
.map_err(|e| BrightDateError::ParseError(format!("invalid ISO 8601: {s}: {e}")))?;
Ok(Self::from_unix_ms(dt.timestamp_millis()))
}
pub fn from_brightdate(bd: f64) -> Result<Self, BrightDateError> {
if !bd.is_finite() {
return Err(BrightDateError::InvalidInput(format!(
"expected finite BrightDate, got {bd}"
)));
}
let days_floor = bd.floor();
let frac = bd - days_floor;
let days_i = days_floor as i128;
let frac_ps = (frac * PS_PER_DAY as f64).round() as i128;
Ok(Self {
picoseconds: days_i * PS_PER_DAY + frac_ps,
})
}
#[inline]
pub const fn epoch() -> Self {
Self { picoseconds: 0 }
}
pub fn now() -> Self {
Self::from_unix_ms(Utc::now().timestamp_millis())
}
#[inline]
pub const fn picoseconds(self) -> i128 {
self.picoseconds
}
pub fn to_unix_ms(self) -> i64 {
let ms = self.picoseconds.div_euclid(PS_PER_MS);
(ms + J2000_UNIX_MS_I64 as i128) as i64
}
pub fn to_unix_seconds(self) -> i64 {
let total_ps = self.picoseconds + J2000_UNIX_PS;
total_ps.div_euclid(PS_PER_S) as i64
}
pub fn to_brightdate(self) -> f64 {
let days = self.picoseconds.div_euclid(PS_PER_DAY);
let remainder = self.picoseconds.rem_euclid(PS_PER_DAY);
days as f64 + (remainder as f64 / PS_PER_DAY as f64)
}
pub fn to_date_time(self) -> DateTime<Utc> {
Utc.timestamp_millis_opt(self.to_unix_ms())
.single()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH)
}
pub fn to_iso(self) -> String {
self.to_date_time()
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string()
}
#[inline]
pub const fn add_picoseconds(self, ps: i128) -> Self {
Self {
picoseconds: self.picoseconds + ps,
}
}
#[inline]
pub const fn add_nanoseconds(self, ns: i128) -> Self {
Self {
picoseconds: self.picoseconds + ns * PS_PER_NS,
}
}
#[inline]
pub const fn add_microseconds(self, us: i128) -> Self {
Self {
picoseconds: self.picoseconds + us * PS_PER_US,
}
}
#[inline]
pub const fn add_milliseconds(self, ms: i128) -> Self {
Self {
picoseconds: self.picoseconds + ms * PS_PER_MS,
}
}
#[inline]
pub const fn add_seconds(self, s: i128) -> Self {
Self {
picoseconds: self.picoseconds + s * PS_PER_S,
}
}
#[inline]
pub const fn add_days(self, days: i128) -> Self {
Self {
picoseconds: self.picoseconds + days * PS_PER_DAY,
}
}
#[inline]
pub const fn subtract_picoseconds(self, ps: i128) -> Self {
Self {
picoseconds: self.picoseconds - ps,
}
}
#[inline]
pub const fn subtract_nanoseconds(self, ns: i128) -> Self {
Self {
picoseconds: self.picoseconds - ns * PS_PER_NS,
}
}
#[inline]
pub const fn subtract_milliseconds(self, ms: i128) -> Self {
Self {
picoseconds: self.picoseconds - ms * PS_PER_MS,
}
}
#[inline]
pub const fn subtract_days(self, days: i128) -> Self {
Self {
picoseconds: self.picoseconds - days * PS_PER_DAY,
}
}
#[inline]
pub const fn difference_picoseconds(self, other: Self) -> i128 {
self.picoseconds - other.picoseconds
}
#[inline]
pub const fn difference_nanoseconds(self, other: Self) -> i128 {
(self.picoseconds - other.picoseconds) / PS_PER_NS
}
#[inline]
pub const fn difference_microseconds(self, other: Self) -> i128 {
(self.picoseconds - other.picoseconds) / PS_PER_US
}
#[inline]
pub const fn difference_milliseconds(self, other: Self) -> i128 {
(self.picoseconds - other.picoseconds) / PS_PER_MS
}
#[inline]
pub const fn difference_seconds(self, other: Self) -> i128 {
(self.picoseconds - other.picoseconds) / PS_PER_S
}
#[inline]
pub const fn is_before(self, other: Self) -> bool {
self.picoseconds < other.picoseconds
}
#[inline]
pub const fn is_after(self, other: Self) -> bool {
self.picoseconds > other.picoseconds
}
pub fn encode(self) -> String {
format!("EBD1:{}", self.picoseconds)
}
pub fn decode(encoded: &str) -> Result<Self, BrightDateError> {
let body = encoded.strip_prefix("EBD1:").ok_or_else(|| {
BrightDateError::ParseError(format!(
"ExactBrightDate encoding must start with \"EBD1:\", got: {encoded:?}"
))
})?;
let ps: i128 = body.parse().map_err(|_| {
BrightDateError::ParseError(format!(
"ExactBrightDate encoding has invalid picosecond body: {body:?}"
))
})?;
Ok(Self::from_picoseconds(ps))
}
pub fn to_be_bytes(self) -> [u8; 16] {
self.picoseconds.to_be_bytes()
}
pub fn from_be_bytes(bytes: [u8; 16]) -> Self {
Self::from_picoseconds(i128::from_be_bytes(bytes))
}
}
impl std::fmt::Display for ExactBrightDate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.picoseconds)
}
}
impl From<crate::BrightInstant> for ExactBrightDate {
fn from(inst: crate::BrightInstant) -> Self {
let ps = (inst.tai_seconds_since_j2000() as i128) * PS_PER_S
+ (inst.tai_nanos() as i128) * PS_PER_NS;
Self::from_picoseconds(ps)
}
}
impl From<ExactBrightDate> for crate::BrightInstant {
fn from(value: ExactBrightDate) -> Self {
let ps = value.picoseconds;
let mut secs = ps.div_euclid(PS_PER_S);
let mut sub_ps = ps.rem_euclid(PS_PER_S);
if sub_ps < 0 {
secs -= 1;
sub_ps += PS_PER_S;
}
let nanos = (sub_ps / PS_PER_NS) as u32;
crate::BrightInstant::from_tai_components(secs as i64, nanos)
.expect("nanos < 1_000_000_000 by construction")
}
}
#[allow(dead_code)]
const _STATIC_INVARIANT: () = {
assert!(J2000_UNIX_MS_I64 == 946_727_935_816);
let _ = J2000_UTC_UNIX_MS;
let _ = MS_PER_DAY;
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_is_zero() {
assert_eq!(ExactBrightDate::epoch().picoseconds(), 0);
}
#[test]
fn unix_ms_roundtrip_is_bit_exact() {
for ms in [
0_i64,
J2000_UNIX_MS_I64,
1_700_000_000_000,
-1_000_000_000_000,
i64::MIN / 4,
i64::MAX / 4,
] {
let exact = ExactBrightDate::from_unix_ms(ms);
assert_eq!(exact.to_unix_ms(), ms, "ms = {ms}");
}
}
#[test]
fn at_j2000_is_zero() {
let e = ExactBrightDate::from_unix_ms(J2000_UNIX_MS_I64);
assert_eq!(e, ExactBrightDate::epoch());
}
#[test]
fn nanosecond_arithmetic_roundtrips() {
let a = ExactBrightDate::from_unix_ms(1_700_000_000_000);
let b = a.add_nanoseconds(123_456_789).subtract_nanoseconds(123_456_789);
assert_eq!(a, b);
}
#[test]
fn picosecond_arithmetic_is_lossless() {
let a = ExactBrightDate::epoch();
let b = a.add_picoseconds(1);
assert_eq!(b.picoseconds(), 1);
assert!(b.is_after(a));
assert!(a.is_before(b));
}
#[test]
fn encode_decode_roundtrip() {
let a = ExactBrightDate::from_picoseconds(-123_456_789_000_000);
let encoded = a.encode();
assert!(encoded.starts_with("EBD1:"));
let b = ExactBrightDate::decode(&encoded).unwrap();
assert_eq!(a, b);
}
#[test]
fn decode_rejects_bad_prefix() {
assert!(ExactBrightDate::decode("XBD1:123").is_err());
assert!(ExactBrightDate::decode("EBD1:notanint").is_err());
}
#[test]
fn be_bytes_roundtrip() {
let a = ExactBrightDate::from_picoseconds(-987_654_321_000);
let bytes = a.to_be_bytes();
assert_eq!(ExactBrightDate::from_be_bytes(bytes), a);
}
#[test]
fn bright_instant_roundtrip() {
let inst = crate::BrightInstant::from_tai_components(123_456, 789_000_000).unwrap();
let exact: ExactBrightDate = inst.into();
let back: crate::BrightInstant = exact.into();
assert_eq!(back, inst);
}
#[test]
fn brightdate_roundtrip_modern() {
let bd_ms = 1_700_000_000_000_i64;
let exact = ExactBrightDate::from_unix_ms(bd_ms);
let bd = exact.to_brightdate();
let back = ExactBrightDate::from_brightdate(bd).unwrap();
let diff = (exact.picoseconds() - back.picoseconds()).abs();
assert!(diff < PS_PER_US, "diff = {diff} ps");
}
#[test]
fn difference_milliseconds_truncates() {
let a = ExactBrightDate::epoch();
let b = a.add_picoseconds(2_500 * PS_PER_MS + 1); assert_eq!(b.difference_milliseconds(a), 2500);
}
#[test]
fn negative_picoseconds_brightdate_is_negative() {
let e = ExactBrightDate::from_picoseconds(-PS_PER_DAY);
assert_eq!(e.to_brightdate(), -1.0);
}
}