use std::cmp::Ordering;
use std::fmt;
use std::time::Duration;
pub const MAX_FRAMES_PER_SECOND: u32 = 1_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RateError {
#[error("frame rate numerator and denominator must be non-zero")]
Zero,
#[error("frame rate must not exceed {MAX_FRAMES_PER_SECOND} frames per second")]
TooLarge,
#[error("frame rate must be finite and positive")]
InvalidFloat,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct Rate {
numerator: u32,
denominator: u32,
}
impl Rate {
pub fn new(numerator: u32, denominator: u32) -> Result<Self, RateError> {
if numerator == 0 || denominator == 0 {
return Err(RateError::Zero);
}
if u64::from(numerator) > u64::from(MAX_FRAMES_PER_SECOND) * u64::from(denominator) {
return Err(RateError::TooLarge);
}
let divisor = gcd(numerator, denominator);
Ok(Self {
numerator: numerator / divisor,
denominator: denominator / divisor,
})
}
pub fn from_f64(value: f64) -> Result<Self, RateError> {
if !value.is_finite() || value <= 0.0 {
return Err(RateError::InvalidFloat);
}
if value > f64::from(MAX_FRAMES_PER_SECOND) {
return Err(RateError::TooLarge);
}
let (mut input, mut n0, mut d0, mut n1, mut d1) = (value, 0u64, 1u64, 1u64, 0u64);
loop {
let whole = input.floor() as u64;
let Some(n2) = whole.checked_mul(n1).and_then(|v| v.checked_add(n0)) else {
break;
};
let Some(d2) = whole.checked_mul(d1).and_then(|v| v.checked_add(d0)) else {
break;
};
if n2 > u64::from(u32::MAX) || d2 > u64::from(u32::MAX) {
break;
}
(n0, d0, n1, d1) = (n1, d1, n2, d2);
let fraction = input - whole as f64;
if fraction < 1e-12 || (n1 as f64 / d1 as f64 - value).abs() < 1e-12 {
break;
}
input = 1.0 / fraction;
}
Self::new(n1 as u32, d1 as u32)
}
pub fn numerator(self) -> u32 {
self.numerator
}
pub fn denominator(self) -> u32 {
self.denominator
}
pub fn as_f64(self) -> f64 {
f64::from(self.numerator) / f64::from(self.denominator)
}
pub fn rounded(self) -> u32 {
let numerator = u64::from(self.numerator);
let denominator = u64::from(self.denominator);
u32::try_from((numerator + denominator / 2) / denominator)
.unwrap_or(MAX_FRAMES_PER_SECOND)
.max(1)
}
pub fn frames(self, duration: Duration) -> u32 {
let nanos = duration.as_nanos();
let scaled = nanos.saturating_mul(u128::from(self.numerator));
let divisor = 1_000_000_000u128 * u128::from(self.denominator);
u32::try_from((scaled + divisor / 2) / divisor).unwrap_or(u32::MAX)
}
#[cfg(feature = "capture")]
pub(crate) const fn integer(value: u32) -> Self {
Self {
numerator: value,
denominator: 1,
}
}
}
impl Ord for Rate {
fn cmp(&self, other: &Self) -> Ordering {
(u64::from(self.numerator) * u64::from(other.denominator))
.cmp(&(u64::from(other.numerator) * u64::from(self.denominator)))
}
}
impl PartialOrd for Rate {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Rate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.numerator, self.denominator)
}
}
const fn gcd(mut left: u32, mut right: u32) -> u32 {
while right != 0 {
let remainder = left % right;
left = right;
right = remainder;
}
left
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_broadcast_rates() {
assert_eq!(
Rate::new(30_000, 1_001).unwrap(),
Rate::from_f64(30_000.0 / 1_001.0).unwrap()
);
assert_eq!(
Rate::new(60_000, 1_001).unwrap(),
Rate::from_f64(60_000.0 / 1_001.0).unwrap()
);
}
#[test]
fn validates_components_and_range() {
assert_eq!(Rate::new(30, 0), Err(RateError::Zero));
assert_eq!(Rate::new(0, 1), Err(RateError::Zero));
assert_eq!(Rate::new(MAX_FRAMES_PER_SECOND + 1, 1), Err(RateError::TooLarge));
}
#[test]
fn rejects_invalid_floats() {
assert_eq!(Rate::from_f64(f64::NAN), Err(RateError::InvalidFloat));
assert_eq!(Rate::from_f64(f64::INFINITY), Err(RateError::InvalidFloat));
assert_eq!(Rate::from_f64(f64::NEG_INFINITY), Err(RateError::InvalidFloat));
assert_eq!(Rate::from_f64(0.0), Err(RateError::InvalidFloat));
assert_eq!(Rate::from_f64(-30.0), Err(RateError::InvalidFloat));
assert_eq!(
Rate::from_f64(f64::from(MAX_FRAMES_PER_SECOND) + 1.0),
Err(RateError::TooLarge)
);
}
}