1use std::cmp::Ordering;
4use std::fmt;
5use std::time::Duration;
6
7pub const MAX_FRAMES_PER_SECOND: u32 = 1_000_000;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
12#[non_exhaustive]
13pub enum RateError {
14 #[error("frame rate numerator and denominator must be non-zero")]
16 Zero,
17 #[error("frame rate must not exceed {MAX_FRAMES_PER_SECOND} frames per second")]
19 TooLarge,
20 #[error("frame rate must be finite and positive")]
22 InvalidFloat,
23}
24
25#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
27pub struct Rate {
28 numerator: u32,
29 denominator: u32,
30}
31
32impl Rate {
33 pub fn new(numerator: u32, denominator: u32) -> Result<Self, RateError> {
35 if numerator == 0 || denominator == 0 {
36 return Err(RateError::Zero);
37 }
38 if u64::from(numerator) > u64::from(MAX_FRAMES_PER_SECOND) * u64::from(denominator) {
39 return Err(RateError::TooLarge);
40 }
41 let divisor = gcd(numerator, denominator);
42 Ok(Self {
43 numerator: numerator / divisor,
44 denominator: denominator / divisor,
45 })
46 }
47
48 pub fn from_f64(value: f64) -> Result<Self, RateError> {
50 if !value.is_finite() || value <= 0.0 {
51 return Err(RateError::InvalidFloat);
52 }
53 if value > f64::from(MAX_FRAMES_PER_SECOND) {
54 return Err(RateError::TooLarge);
55 }
56 let (mut input, mut n0, mut d0, mut n1, mut d1) = (value, 0u64, 1u64, 1u64, 0u64);
59 loop {
60 let whole = input.floor() as u64;
61 let Some(n2) = whole.checked_mul(n1).and_then(|v| v.checked_add(n0)) else {
62 break;
63 };
64 let Some(d2) = whole.checked_mul(d1).and_then(|v| v.checked_add(d0)) else {
65 break;
66 };
67 if n2 > u64::from(u32::MAX) || d2 > u64::from(u32::MAX) {
68 break;
69 }
70 (n0, d0, n1, d1) = (n1, d1, n2, d2);
71 let fraction = input - whole as f64;
72 if fraction < 1e-12 || (n1 as f64 / d1 as f64 - value).abs() < 1e-12 {
73 break;
74 }
75 input = 1.0 / fraction;
76 }
77 Self::new(n1 as u32, d1 as u32)
78 }
79
80 pub fn numerator(self) -> u32 {
82 self.numerator
83 }
84
85 pub fn denominator(self) -> u32 {
87 self.denominator
88 }
89
90 pub fn as_f64(self) -> f64 {
92 f64::from(self.numerator) / f64::from(self.denominator)
93 }
94
95 pub fn rounded(self) -> u32 {
97 let numerator = u64::from(self.numerator);
98 let denominator = u64::from(self.denominator);
99 u32::try_from((numerator + denominator / 2) / denominator)
100 .unwrap_or(MAX_FRAMES_PER_SECOND)
101 .max(1)
102 }
103
104 pub fn frames(self, duration: Duration) -> u32 {
106 let nanos = duration.as_nanos();
107 let scaled = nanos.saturating_mul(u128::from(self.numerator));
108 let divisor = 1_000_000_000u128 * u128::from(self.denominator);
109 u32::try_from((scaled + divisor / 2) / divisor).unwrap_or(u32::MAX)
110 }
111
112 #[cfg(feature = "capture")]
113 pub(crate) const fn integer(value: u32) -> Self {
114 Self {
115 numerator: value,
116 denominator: 1,
117 }
118 }
119}
120
121impl Ord for Rate {
122 fn cmp(&self, other: &Self) -> Ordering {
123 (u64::from(self.numerator) * u64::from(other.denominator))
124 .cmp(&(u64::from(other.numerator) * u64::from(self.denominator)))
125 }
126}
127
128impl PartialOrd for Rate {
129 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
130 Some(self.cmp(other))
131 }
132}
133
134impl fmt::Display for Rate {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{}/{}", self.numerator, self.denominator)
137 }
138}
139
140const fn gcd(mut left: u32, mut right: u32) -> u32 {
141 while right != 0 {
142 let remainder = left % right;
143 left = right;
144 right = remainder;
145 }
146 left
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn preserves_broadcast_rates() {
155 assert_eq!(
156 Rate::new(30_000, 1_001).unwrap(),
157 Rate::from_f64(30_000.0 / 1_001.0).unwrap()
158 );
159 assert_eq!(
160 Rate::new(60_000, 1_001).unwrap(),
161 Rate::from_f64(60_000.0 / 1_001.0).unwrap()
162 );
163 }
164
165 #[test]
166 fn validates_components_and_range() {
167 assert_eq!(Rate::new(30, 0), Err(RateError::Zero));
168 assert_eq!(Rate::new(0, 1), Err(RateError::Zero));
169 assert_eq!(Rate::new(MAX_FRAMES_PER_SECOND + 1, 1), Err(RateError::TooLarge));
170 }
171
172 #[test]
173 fn rejects_invalid_floats() {
174 assert_eq!(Rate::from_f64(f64::NAN), Err(RateError::InvalidFloat));
175 assert_eq!(Rate::from_f64(f64::INFINITY), Err(RateError::InvalidFloat));
176 assert_eq!(Rate::from_f64(f64::NEG_INFINITY), Err(RateError::InvalidFloat));
177 assert_eq!(Rate::from_f64(0.0), Err(RateError::InvalidFloat));
178 assert_eq!(Rate::from_f64(-30.0), Err(RateError::InvalidFloat));
179 assert_eq!(
180 Rate::from_f64(f64::from(MAX_FRAMES_PER_SECOND) + 1.0),
181 Err(RateError::TooLarge)
182 );
183 }
184}