use chrono::{DateTime, Duration, TimeZone, Utc};
#[cfg(not(feature = "std"))]
use num_traits::Float;
pub const OSCILLATIONS_PER_SECOND: u64 = 1_420_407_826;
#[derive(Debug, Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub enum EtType {
e5(i32),
e6(i64),
e7(i128),
#[deprecated(since = "0.3.5", note = "Use EtType::e6 (oscillation count)")]
f5(f32),
#[deprecated(since = "0.3.5", note = "Use EtType::e6 (oscillation count)")]
f6(f64),
}
#[derive(Debug, Clone)]
pub struct EagleTime {
et_seconds: EtType,
}
impl EagleTime {
pub fn new_from_vsf(value: crate::types::VsfType) -> Self {
use crate::types::VsfType;
#[allow(deprecated)]
let et_seconds = match value {
VsfType::e(et) => et,
VsfType::f5(v) => EtType::f5(v),
VsfType::f6(v) => EtType::f6(v),
VsfType::u(v, false) => EtType::e6(v as i64),
VsfType::u3(v) => EtType::e6(v as i64),
VsfType::u4(v) => EtType::e6(v as i64),
VsfType::u5(v) => EtType::e6(v as i64),
VsfType::u6(v) => EtType::e6(v as i64),
VsfType::i(v) => EtType::e6(v as i64),
VsfType::i3(v) => EtType::e6(v as i64),
VsfType::i4(v) => EtType::e6(v as i64),
VsfType::i5(v) => EtType::e6(v as i64),
VsfType::i6(v) => EtType::e6(v as i64),
_ => panic!("EagleTime must be created with a valid numeric VsfType variant"),
};
EagleTime { et_seconds }
}
pub fn new(et_seconds: EtType) -> Self {
EagleTime { et_seconds }
}
pub fn from_oscillations(count: i64) -> Self {
EagleTime {
et_seconds: EtType::e6(count),
}
}
pub fn from_seconds_f64(seconds: f64) -> Self {
let oscillations = (seconds * OSCILLATIONS_PER_SECOND as f64).round() as i64;
EagleTime {
et_seconds: EtType::e6(oscillations),
}
}
pub fn from_seconds_f32(seconds: f32) -> Self {
let oscillations = (seconds * OSCILLATIONS_PER_SECOND as f32).round() as i64;
EagleTime {
et_seconds: EtType::e6(oscillations),
}
}
pub fn to_vsf_type(&self) -> crate::types::VsfType {
use crate::types::VsfType;
#[allow(deprecated)]
match self.et_seconds {
EtType::e5(v) => VsfType::i5(v),
EtType::e6(v) => VsfType::i6(v),
EtType::e7(v) => VsfType::i6(v as i64), EtType::f5(v) => VsfType::f5(v),
EtType::f6(v) => VsfType::f6(v),
}
}
pub fn to_datetime_opt(&self) -> Option<DateTime<Utc>> {
let eagle_epoch = Utc.with_ymd_and_hms(1969, 7, 20, 20, 17, 40).unwrap();
let seconds = self.to_seconds_f64();
let abs = seconds.abs();
let int_secs = abs.trunc() as i64;
let frac_nanos = (abs.fract() * 1_000_000_000.0).round() as i64;
let duration =
Duration::seconds(int_secs).checked_add(&Duration::nanoseconds(frac_nanos))?;
if seconds >= 0.0 {
Some(eagle_epoch + duration)
} else {
Some(eagle_epoch - duration)
}
}
pub fn to_datetime(&self) -> DateTime<Utc> {
self.to_datetime_opt().unwrap_or_else(|| {
panic!(
"Timestamp outside representable range: {:?}",
self.et_seconds
)
})
}
pub fn et_type(&self) -> &EtType {
&self.et_seconds
}
pub fn to_seconds_f64(&self) -> f64 {
#[allow(deprecated)]
match self.et_seconds {
EtType::e5(oscillations) => oscillations as f64 / OSCILLATIONS_PER_SECOND as f64,
EtType::e6(oscillations) => oscillations as f64 / OSCILLATIONS_PER_SECOND as f64,
EtType::e7(oscillations) => oscillations as f64 / OSCILLATIONS_PER_SECOND as f64,
EtType::f5(seconds) => seconds as f64,
EtType::f6(seconds) => seconds,
}
}
pub fn to_seconds_f32(&self) -> f32 {
#[allow(deprecated)]
match self.et_seconds {
EtType::e5(oscillations) => oscillations as f32 / OSCILLATIONS_PER_SECOND as f32,
EtType::e6(oscillations) => oscillations as f32 / OSCILLATIONS_PER_SECOND as f32,
EtType::e7(oscillations) => oscillations as f32 / OSCILLATIONS_PER_SECOND as f32,
EtType::f5(seconds) => seconds,
EtType::f6(seconds) => seconds as f32,
}
}
pub fn oscillations(&self) -> Option<i64> {
#[allow(deprecated)]
match self.et_seconds {
EtType::e5(v) => Some(v as i64),
EtType::e6(v) => Some(v),
EtType::e7(v) => Some(v as i64), EtType::f5(_) | EtType::f6(_) => None,
}
}
pub fn oscillations_i128(&self) -> Option<i128> {
#[allow(deprecated)]
match self.et_seconds {
EtType::e5(v) => Some(v as i128),
EtType::e6(v) => Some(v as i128),
EtType::e7(v) => Some(v),
EtType::f5(_) | EtType::f6(_) => None,
}
}
pub fn picoseconds(&self) -> Option<i128> {
self.oscillations_i128().map(|osc| (osc * 704_032) / 1000)
}
}
impl PartialEq for EagleTime {
fn eq(&self, other: &Self) -> bool {
#[allow(deprecated)]
match (&self.et_seconds, &other.et_seconds) {
(EtType::e5(a), EtType::e5(b)) => a == b,
(EtType::e6(a), EtType::e6(b)) => a == b,
(EtType::e7(a), EtType::e7(b)) => a == b,
(a, b)
if matches!(a, EtType::e5(_) | EtType::e6(_) | EtType::e7(_))
&& matches!(b, EtType::e5(_) | EtType::e6(_) | EtType::e7(_)) =>
{
self.oscillations_i128() == other.oscillations_i128()
}
_ => self.to_seconds_f64() == other.to_seconds_f64(),
}
}
}
impl Eq for EagleTime {}
impl PartialOrd for EagleTime {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for EagleTime {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
#[allow(deprecated)]
match (&self.et_seconds, &other.et_seconds) {
(EtType::e6(a), EtType::e6(b)) => a.cmp(b),
_ if self.oscillations_i128().is_some() && other.oscillations_i128().is_some() => {
self.oscillations_i128().cmp(&other.oscillations_i128())
}
_ => self
.to_seconds_f64()
.partial_cmp(&other.to_seconds_f64())
.unwrap_or(core::cmp::Ordering::Equal),
}
}
}
pub fn datetime_to_eagle_time(dt: DateTime<Utc>) -> EagleTime {
let eagle_epoch = Utc.with_ymd_and_hms(1969, 7, 20, 20, 17, 40).unwrap();
let duration = dt - eagle_epoch;
let total_seconds =
duration.num_seconds() as f64 + duration.subsec_nanos() as f64 / 1_000_000_000.0;
let oscillations = (total_seconds * OSCILLATIONS_PER_SECOND as f64).round() as i64;
EagleTime::from_oscillations(oscillations)
}
#[cfg(feature = "std")]
pub fn eagle_time_now() -> EagleTime {
datetime_to_eagle_time(Utc::now())
}
#[cfg(feature = "std")]
pub fn eagle_time_oscillations() -> i64 {
eagle_time_now().oscillations().unwrap_or(0)
}
#[cfg(feature = "std")]
#[deprecated(note = "Use eagle_time_oscillations() for integer timestamps")]
pub fn eagle_time_nanos() -> f64 {
eagle_time_now().to_seconds_f64()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_oscillations_per_second_constant() {
assert_eq!(OSCILLATIONS_PER_SECOND, 1_420_407_826);
}
#[test]
fn test_eagle_epoch() {
let epoch = Utc.with_ymd_and_hms(1969, 7, 20, 20, 17, 40).unwrap();
let et = datetime_to_eagle_time(epoch);
assert_eq!(et.oscillations(), Some(0));
let back = et.to_datetime();
assert_eq!(epoch, back);
}
#[test]
fn test_oscillation_counting() {
let one_second = EagleTime::from_oscillations(OSCILLATIONS_PER_SECOND as i64);
assert_eq!(one_second.to_seconds_f64(), 1.0);
let hundred_seconds = EagleTime::from_oscillations(OSCILLATIONS_PER_SECOND as i64 * 100);
assert_eq!(hundred_seconds.to_seconds_f64(), 100.0);
}
#[test]
fn test_picosecond_precision() {
let one_osc = EagleTime::from_oscillations(1);
let ps = one_osc.picoseconds().unwrap();
assert_eq!(ps, 704);
let ten_k = EagleTime::from_oscillations(10_000);
let ps = ten_k.picoseconds().unwrap();
assert_eq!(ps, 7_040_320);
}
#[test]
fn test_float_to_oscillation_conversion() {
let et = EagleTime::from_seconds_f64(1.0);
assert_eq!(et.oscillations(), Some(OSCILLATIONS_PER_SECOND as i64));
let seconds = et.to_seconds_f64();
assert!((seconds - 1.0).abs() < 1e-9);
}
#[test]
fn test_eagle_time_positive() {
let future = Utc.with_ymd_and_hms(2025, 10, 25, 0, 0, 0).unwrap();
let et = datetime_to_eagle_time(future);
let back = et.to_datetime();
assert_eq!((future - back).num_seconds().abs(), 0);
}
#[test]
fn test_eagle_time_comparison() {
let time1 = EagleTime::from_oscillations(1000);
let time2 = EagleTime::from_oscillations(2000);
let time3 = EagleTime::from_oscillations(1000);
assert!(time1 < time2);
assert!(time2 > time1);
assert_eq!(time1, time3);
#[allow(deprecated)]
let time_f = EagleTime::new(EtType::f6(1000.0 / OSCILLATIONS_PER_SECOND as f64));
assert_eq!(time1, time_f);
}
#[test]
fn test_eagle_time_sorting() {
let mut times = vec![
EagleTime::from_oscillations(3000),
EagleTime::from_oscillations(1000),
EagleTime::from_oscillations(2000),
EagleTime::from_oscillations(500),
];
times.sort();
assert_eq!(times[0].oscillations(), Some(500));
assert_eq!(times[1].oscillations(), Some(1000));
assert_eq!(times[2].oscillations(), Some(2000));
assert_eq!(times[3].oscillations(), Some(3000));
}
#[test]
fn test_negative_oscillations() {
let before = EagleTime::from_oscillations(-1000);
assert_eq!(before.oscillations(), Some(-1000));
let after = EagleTime::from_oscillations(500);
assert!(before < after);
}
#[test]
fn test_pre_epoch_datetime() {
let pre_epoch = Utc.with_ymd_and_hms(1960, 1, 1, 0, 0, 0).unwrap();
let et = datetime_to_eagle_time(pre_epoch);
assert!(et.oscillations().unwrap() < 0);
let back = et.to_datetime();
assert_eq!((pre_epoch - back).num_seconds().abs(), 0);
}
}