dspk_core/time/
rational.rs1use 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#[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 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 pub fn checked_add(&self, other: &Self) -> Option<Self> {
47 self.seconds.checked_add(&other.seconds).map(|seconds| Self { seconds })
48 }
49
50 pub fn from_samples(sample_index: u64, rate: SampleRate) -> DspResult<Self> {
52 let r = rate.as_ratio();
53 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 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 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 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}