use chrono::{DateTime, Duration, TimeZone, Utc};
const OSCILLATIONS_PER_SECOND: u64 = 1_420_407_826;
#[derive(Debug, Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub enum EtType {
u(u64), i(i64), f5(f32), 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;
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::u(v as u64),
VsfType::u3(v) => EtType::u(v as u64),
VsfType::u4(v) => EtType::u(v as u64),
VsfType::u5(v) => EtType::u(v as u64),
VsfType::u6(v) => EtType::u(v as u64),
VsfType::i(v) => EtType::i(v as i64),
VsfType::i3(v) => EtType::i(v as i64),
VsfType::i4(v) => EtType::i(v as i64),
VsfType::i5(v) => EtType::i(v as i64),
VsfType::i6(v) => EtType::i(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: u64) -> Self {
EagleTime {
et_seconds: EtType::u(count),
}
}
pub fn from_oscillations_signed(count: i64) -> Self {
EagleTime {
et_seconds: EtType::i(count),
}
}
pub fn from_seconds_f64(seconds: f64) -> Self {
let oscillations = (seconds * OSCILLATIONS_PER_SECOND as f64).round() as u64;
EagleTime {
et_seconds: EtType::u(oscillations),
}
}
pub fn from_seconds_f32(seconds: f32) -> Self {
let oscillations = (seconds * OSCILLATIONS_PER_SECOND as f32).round() as u64;
EagleTime {
et_seconds: EtType::u(oscillations),
}
}
pub fn to_vsf_type(&self) -> crate::types::VsfType {
use crate::types::VsfType;
match self.et_seconds {
EtType::f5(v) => VsfType::f5(v),
EtType::f6(v) => VsfType::f6(v),
EtType::u(v) => VsfType::u6(v),
EtType::i(v) => VsfType::i6(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 duration = Duration::from_std(std::time::Duration::from_secs_f64(seconds)).ok()?;
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 {
match self.et_seconds {
EtType::u(oscillations) => oscillations as f64 / OSCILLATIONS_PER_SECOND as f64,
EtType::i(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 {
match self.et_seconds {
EtType::u(oscillations) => oscillations as f32 / OSCILLATIONS_PER_SECOND as f32,
EtType::i(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> {
match self.et_seconds {
EtType::u(v) => Some(v as i64),
EtType::i(v) => Some(v),
EtType::f5(_) | EtType::f6(_) => None,
}
}
pub fn oscillations_u64(&self) -> Option<u64> {
match self.et_seconds {
EtType::u(v) => Some(v),
_ => None,
}
}
pub fn picoseconds(&self) -> Option<i128> {
self.oscillations()
.map(|osc| (osc as i128 * 704_032) / 1000)
}
}
impl PartialEq for EagleTime {
fn eq(&self, other: &Self) -> bool {
match (&self.et_seconds, &other.et_seconds) {
(EtType::u(a), EtType::u(b)) => a == b,
(EtType::i(a), EtType::i(b)) => a == b,
(EtType::u(a), EtType::i(b)) | (EtType::i(b), EtType::u(a)) => *a as i64 == *b,
_ => self.to_seconds_f64() == other.to_seconds_f64(),
}
}
}
impl Eq for EagleTime {}
impl PartialOrd for EagleTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for EagleTime {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (&self.et_seconds, &other.et_seconds) {
(EtType::u(a), EtType::u(b)) => a.cmp(b),
(EtType::i(a), EtType::i(b)) => a.cmp(b),
(EtType::u(a), EtType::i(b)) => (*a as i64).cmp(b),
(EtType::i(a), EtType::u(b)) => a.cmp(&(*b as i64)),
_ => self
.to_seconds_f64()
.partial_cmp(&other.to_seconds_f64())
.unwrap_or(std::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 u64;
EagleTime::from_oscillations(oscillations)
}
pub fn eagle_time_now() -> EagleTime {
datetime_to_eagle_time(Utc::now())
}
pub fn eagle_time_oscillations() -> u64 {
eagle_time_now().oscillations().unwrap_or(0) as u64
}
#[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 u64);
assert_eq!(one_second.to_seconds_f64(), 1.0);
let hundred_seconds =
EagleTime::from_oscillations(OSCILLATIONS_PER_SECOND * 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);
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_signed(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_range_limits() {
let year_2380 = Utc.with_ymd_and_hms(2380, 1, 1, 0, 0, 0).unwrap();
let et = datetime_to_eagle_time(year_2380);
if let Some(osc) = et.oscillations_u64() {
assert!(osc > 0);
}
}
#[test]
fn test_signed_oscillations() {
let before_epoch = EagleTime::from_oscillations_signed(-1000);
assert_eq!(before_epoch.oscillations(), Some(-1000));
let after_epoch = EagleTime::from_oscillations(500);
assert!(before_epoch < after_epoch);
}
}