Skip to main content

dspk_core/time/
rational.rs

1use num_rational::Ratio;
2use num_traits::CheckedAdd;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::time::Duration;
6use crate::error::{DspError, DspResult};
7use super::rate::{decimal_ratio, SampleRate};
8
9fn gcd(mut a: u128, mut b: u128) -> u128 {
10    while b != 0 {
11        (a, b) = (b, a % b);
12    }
13    a.max(1)
14}
15
16/// Exact rational representation of time (seconds as numerator/denominator).
17/// Prevents floating-point accumulation drift over long recordings.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19pub struct RationalTime {
20    seconds: Ratio<u64>,
21}
22
23impl RationalTime {
24    pub const ZERO: Self = Self {
25        seconds: Ratio::new_raw(0, 1),
26    };
27
28    pub fn new(numerator: u64, denominator: u64) -> DspResult<Self> {
29        if denominator == 0 {
30            return Err(DspError::InvalidConfig("Denominator cannot be 0".into()));
31        }
32        Ok(Self {
33            seconds: Ratio::new(numerator, denominator),
34        })
35    }
36
37    /// Time from a decimal number of seconds, exact for its shortest decimal form (`12.5` → `25/2`).
38    pub fn from_seconds_f64(seconds: f64) -> DspResult<Self> {
39        if seconds < 0.0 || !seconds.is_finite() {
40            return Err(DspError::InvalidConfig(format!("time {seconds} s must be finite and non-negative")));
41        }
42        Ok(Self { seconds: decimal_ratio(seconds) })
43    }
44
45    /// `self + other`, or `None` on `u64` overflow.
46    pub fn checked_add(&self, other: &Self) -> Option<Self> {
47        self.seconds.checked_add(&other.seconds).map(|seconds| Self { seconds })
48    }
49
50    /// Exact time of `sample_index` at `rate` (fractional rates included, no drift).
51    pub fn from_samples(sample_index: u64, rate: SampleRate) -> DspResult<Self> {
52        let r = rate.as_ratio();
53        // seconds = sample · denom / numer, reduced in 128-bit arithmetic
54        let (n, d) = (sample_index as u128 * *r.denom() as u128, *r.numer() as u128);
55        let g = gcd(n, d);
56        let (n, d) = (n / g, d / g);
57        match (u64::try_from(n), u64::try_from(d)) {
58            (Ok(n), Ok(d)) => Self::new(n, d),
59            _ => Err(DspError::InvalidConfig(format!("time of sample {sample_index} at {} Hz overflows", rate.rate_hz()))),
60        }
61    }
62
63    /// The time as an exact fraction of seconds.
64    pub fn as_ratio(&self) -> Ratio<u64> {
65        self.seconds
66    }
67
68    pub fn as_seconds_f64(&self) -> f64 {
69        *self.seconds.numer() as f64 / *self.seconds.denom() as f64
70    }
71
72    pub fn as_duration(&self) -> Duration {
73        Duration::from_secs_f64(self.as_seconds_f64())
74    }
75
76    /// Index of the sample at or before this time at `rate` (exact).
77    pub fn to_sample_index(&self, rate: SampleRate) -> u64 {
78        let r = rate.as_ratio();
79        let n = *self.seconds.numer() as u128 * *r.numer() as u128;
80        let d = *self.seconds.denom() as u128 * *r.denom() as u128;
81        (n / d) as u64
82    }
83}
84
85impl fmt::Display for RationalTime {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(
88            f,
89            "{:.6}s ({}/{})",
90            self.as_seconds_f64(),
91            self.seconds.numer(),
92            self.seconds.denom()
93        )
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn fractional_rates_round_trip_exactly() {
103        for hz in [24_414.0625, 30_000.12, 2_500.0, 30_000.0] {
104            let rate = SampleRate::new(hz).unwrap();
105            let one_hour = (hz * 3600.0).round() as u64;
106            for s in [0, 1, 12_345, one_hour - 1, one_hour, 10 * one_hour + 7] {
107                let t = RationalTime::from_samples(s, rate).unwrap();
108                assert_eq!(t.to_sample_index(rate), s, "{hz} Hz sample {s}");
109            }
110        }
111        // 1 h at the TDT rate is exactly 3600 s (the old integer rate drifted ~9 ms).
112        let tdt = SampleRate::new(24_414.0625).unwrap();
113        assert_eq!(RationalTime::from_samples(87_890_625, tdt).unwrap(), RationalTime::new(3600, 1).unwrap());
114        assert_eq!(SampleRate::new(30_000.12).unwrap().as_ratio(), Ratio::new(750_003, 25));
115    }
116}