#![allow(dead_code)]
use crate::types::{Rational, Timestamp};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimestampArithError {
Overflow,
ZeroDenominator,
}
impl std::fmt::Display for TimestampArithError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Overflow => write!(f, "timestamp arithmetic overflow"),
Self::ZeroDenominator => write!(f, "zero denominator in timestamp rational"),
}
}
}
impl std::error::Error for TimestampArithError {}
fn rescale_ticks(ticks: i64, src: Rational, dst: Rational) -> Option<i64> {
if src.den == 0 || dst.den == 0 || dst.num == 0 {
return None;
}
if src == dst {
return Some(ticks);
}
let num = i128::from(src.num).checked_mul(i128::from(dst.den))?;
let den = i128::from(src.den).checked_mul(i128::from(dst.num))?;
if den == 0 {
return None;
}
let half = den / 2;
let result = i128::from(ticks)
.checked_mul(num)?
.checked_add(half)?
.checked_div(den)?;
i64::try_from(result).ok()
}
fn rescale_ts(ts: &Timestamp, dst: Rational) -> Option<Timestamp> {
let pts = rescale_ticks(ts.pts, ts.timebase, dst)?;
let dts = match ts.dts {
Some(d) => Some(rescale_ticks(d, ts.timebase, dst)?),
None => None,
};
let duration = match ts.duration {
Some(d) => Some(rescale_ticks(d, ts.timebase, dst)?),
None => None,
};
Some(Timestamp::with_dts(pts, dts, dst, duration))
}
pub struct TimestampArith;
impl TimestampArith {
#[must_use]
pub fn add(lhs: &Timestamp, rhs: &Timestamp) -> Option<Timestamp> {
let rhs_rebased = rescale_ts(rhs, lhs.timebase)?;
let pts = lhs.pts.checked_add(rhs_rebased.pts)?;
let dts = match (lhs.dts, rhs_rebased.dts) {
(Some(a), Some(b)) => Some(a.checked_add(b)?),
(Some(a), None) => Some(a.checked_add(rhs_rebased.pts)?),
(None, Some(b)) => Some(lhs.pts.checked_add(b)?),
(None, None) => None,
};
let duration = match (lhs.duration, rhs_rebased.duration) {
(Some(a), Some(b)) => Some(a.checked_add(b)?),
(other, None) | (None, other) => other,
};
Some(Timestamp::with_dts(pts, dts, lhs.timebase, duration))
}
#[must_use]
pub fn sub(lhs: &Timestamp, rhs: &Timestamp) -> Option<Timestamp> {
let rhs_rebased = rescale_ts(rhs, lhs.timebase)?;
let pts = lhs.pts.checked_sub(rhs_rebased.pts)?;
let dts = match (lhs.dts, rhs_rebased.dts) {
(Some(a), Some(b)) => Some(a.checked_sub(b)?),
(Some(a), None) => Some(a.checked_sub(rhs_rebased.pts)?),
(None, Some(b)) => Some(lhs.pts.checked_sub(b)?),
(None, None) => None,
};
Some(Timestamp::with_dts(pts, dts, lhs.timebase, lhs.duration))
}
#[must_use]
pub fn scale(ts: &Timestamp, factor: Rational) -> Option<Timestamp> {
if factor.den == 0 {
return None;
}
let scale_tick = |v: i64| -> Option<i64> {
let num128 = i128::from(v).checked_mul(i128::from(factor.num))?;
let half = i128::from(factor.den) / 2;
let result = num128
.checked_add(half)?
.checked_div(i128::from(factor.den))?;
i64::try_from(result).ok()
};
let pts = scale_tick(ts.pts)?;
let dts = match ts.dts {
Some(d) => Some(scale_tick(d)?),
None => None,
};
let duration = match ts.duration {
Some(d) => Some(scale_tick(d)?),
None => None,
};
Some(Timestamp::with_dts(pts, dts, ts.timebase, duration))
}
#[must_use]
pub fn rescale(ts: &Timestamp, new_base: Rational) -> Option<Timestamp> {
rescale_ts(ts, new_base)
}
#[must_use]
pub fn clamp(ts: &Timestamp, lo: &Timestamp, hi: &Timestamp) -> Option<Timestamp> {
let lo_rebased = rescale_ts(lo, ts.timebase)?;
let hi_rebased = rescale_ts(hi, ts.timebase)?;
let pts = ts.pts.clamp(lo_rebased.pts, hi_rebased.pts);
let dts = ts.dts.map(|d| d.clamp(lo_rebased.pts, hi_rebased.pts));
Some(Timestamp::with_dts(pts, dts, ts.timebase, ts.duration))
}
#[must_use]
pub fn cmp_pts(a: &Timestamp, b: &Timestamp) -> std::cmp::Ordering {
if a.timebase == b.timebase {
a.pts.cmp(&b.pts)
} else {
let a_secs = a.to_seconds();
let b_secs = b.to_seconds();
a_secs
.partial_cmp(&b_secs)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Rational, Timestamp};
fn ms_tb() -> Rational {
Rational::new(1, 1_000)
}
fn tb_90k() -> Rational {
Rational::new(1, 90_000)
}
#[test]
fn test_add_same_base() {
let tb = ms_tb();
let a = Timestamp::new(1_000, tb);
let b = Timestamp::new(500, tb);
let sum = TimestampArith::add(&a, &b).expect("add ok");
assert_eq!(sum.pts, 1_500);
assert_eq!(sum.timebase, tb);
}
#[test]
fn test_sub_same_base() {
let tb = ms_tb();
let a = Timestamp::new(2_000, tb);
let b = Timestamp::new(750, tb);
let diff = TimestampArith::sub(&a, &b).expect("sub ok");
assert_eq!(diff.pts, 1_250);
}
#[test]
fn test_scale_double() {
let tb = ms_tb();
let t = Timestamp::new(1_000, tb);
let doubled = TimestampArith::scale(&t, Rational::new(2, 1)).expect("scale ok");
assert_eq!(doubled.pts, 2_000);
}
#[test]
fn test_scale_half() {
let tb = ms_tb();
let t = Timestamp::new(3_000, tb);
let halved = TimestampArith::scale(&t, Rational::new(1, 2)).expect("scale ok");
assert_eq!(halved.pts, 1_500);
}
#[test]
fn test_rescale_1k_to_90k() {
let ts = Timestamp::new(1_000, ms_tb());
let r = TimestampArith::rescale(&ts, tb_90k()).expect("rescale ok");
assert_eq!(r.pts, 90_000);
assert_eq!(r.timebase, tb_90k());
}
#[test]
fn test_rescale_preserves_dts() {
let ts = Timestamp::with_dts(1_000, Some(900), ms_tb(), Some(33));
let r = TimestampArith::rescale(&ts, tb_90k()).expect("rescale ok");
assert_eq!(r.pts, 90_000);
assert_eq!(r.dts, Some(81_000));
assert_eq!(r.duration, Some(2_970));
}
#[test]
fn test_add_cross_base() {
let a = Timestamp::new(1_000, ms_tb()); let b = Timestamp::new(45_000, tb_90k()); let sum = TimestampArith::add(&a, &b).expect("add ok");
assert_eq!(sum.timebase, ms_tb());
assert_eq!(sum.pts, 1_500);
}
#[test]
fn test_sub_cross_base() {
let a = Timestamp::new(2_000, ms_tb()); let b = Timestamp::new(90_000, tb_90k()); let diff = TimestampArith::sub(&a, &b).expect("sub ok");
assert_eq!(diff.timebase, ms_tb());
assert_eq!(diff.pts, 1_000);
}
#[test]
fn test_add_overflow() {
let tb = ms_tb();
let max_ts = Timestamp::new(i64::MAX, tb);
let one = Timestamp::new(1, tb);
assert!(TimestampArith::add(&max_ts, &one).is_none());
}
#[test]
fn test_sub_underflow() {
let tb = ms_tb();
let min_ts = Timestamp::new(i64::MIN, tb);
let one = Timestamp::new(1, tb);
assert!(TimestampArith::sub(&min_ts, &one).is_none());
}
#[test]
fn test_scale_zero_denominator() {
let tb = ms_tb();
let ts = Timestamp::new(1_000, tb);
let zero_den = Rational { num: 1, den: 0 };
assert!(TimestampArith::scale(&ts, zero_den).is_none());
}
#[test]
fn test_clamp_above_hi() {
let tb = ms_tb();
let ts = Timestamp::new(5_000, tb);
let lo = Timestamp::new(0, tb);
let hi = Timestamp::new(3_000, tb);
let c = TimestampArith::clamp(&ts, &lo, &hi).expect("clamp ok");
assert_eq!(c.pts, 3_000);
}
#[test]
fn test_clamp_below_lo() {
let tb = ms_tb();
let ts = Timestamp::new(-500, tb);
let lo = Timestamp::new(0, tb);
let hi = Timestamp::new(3_000, tb);
let c = TimestampArith::clamp(&ts, &lo, &hi).expect("clamp ok");
assert_eq!(c.pts, 0);
}
#[test]
fn test_clamp_within_range() {
let tb = ms_tb();
let ts = Timestamp::new(1_500, tb);
let lo = Timestamp::new(0, tb);
let hi = Timestamp::new(3_000, tb);
let c = TimestampArith::clamp(&ts, &lo, &hi).expect("clamp ok");
assert_eq!(c.pts, 1_500);
}
#[test]
fn test_cmp_pts_same_base() {
use std::cmp::Ordering;
let tb = ms_tb();
let a = Timestamp::new(1_000, tb);
let b = Timestamp::new(2_000, tb);
assert_eq!(TimestampArith::cmp_pts(&a, &b), Ordering::Less);
assert_eq!(TimestampArith::cmp_pts(&b, &a), Ordering::Greater);
assert_eq!(TimestampArith::cmp_pts(&a, &a), Ordering::Equal);
}
#[test]
fn test_cmp_pts_cross_base_equal() {
use std::cmp::Ordering;
let a = Timestamp::new(1_000, ms_tb()); let b = Timestamp::new(90_000, tb_90k()); assert_eq!(TimestampArith::cmp_pts(&a, &b), Ordering::Equal);
}
#[test]
fn test_add_dts_summed() {
let tb = ms_tb();
let a = Timestamp::with_dts(1_000, Some(900), tb, None);
let b = Timestamp::with_dts(500, Some(450), tb, None);
let sum = TimestampArith::add(&a, &b).expect("add ok");
assert_eq!(sum.pts, 1_500);
assert_eq!(sum.dts, Some(1_350));
}
#[test]
fn test_rescale_noop_same_base() {
let tb = ms_tb();
let ts = Timestamp::new(42_000, tb);
let r = TimestampArith::rescale(&ts, tb).expect("rescale ok");
assert_eq!(r.pts, 42_000);
assert_eq!(r.timebase, tb);
}
#[test]
fn test_scale_identity() {
let tb = ms_tb();
let ts = Timestamp::with_dts(3_000, Some(2_700), tb, Some(33));
let same = TimestampArith::scale(&ts, Rational::new(1, 1)).expect("scale ok");
assert_eq!(same.pts, 3_000);
assert_eq!(same.dts, Some(2_700));
assert_eq!(same.duration, Some(33));
}
#[test]
fn test_error_display() {
assert!(format!("{}", TimestampArithError::Overflow).contains("overflow"));
assert!(format!("{}", TimestampArithError::ZeroDenominator).contains("zero"));
}
#[test]
fn test_rescale_90k_to_48k() {
let ts = Timestamp::new(90_000, tb_90k());
let audio_tb = Rational::new(1, 48_000);
let r = TimestampArith::rescale(&ts, audio_tb).expect("rescale ok");
assert_eq!(r.pts, 48_000);
assert_eq!(r.timebase, audio_tb);
}
#[test]
fn test_scale_three_halves() {
let tb = ms_tb();
let ts = Timestamp::new(2_000, tb); let scaled = TimestampArith::scale(&ts, Rational::new(3, 2)).expect("scale ok");
assert_eq!(scaled.pts, 3_000);
}
#[test]
fn test_add_zero_is_identity() {
let tb = ms_tb();
let ts = Timestamp::new(5_432, tb);
let zero = Timestamp::new(0, tb);
let sum = TimestampArith::add(&ts, &zero).expect("add ok");
assert_eq!(sum.pts, 5_432);
}
#[test]
fn test_sub_self_yields_zero() {
let tb = ms_tb();
let ts = Timestamp::new(10_000, tb);
let diff = TimestampArith::sub(&ts, &ts).expect("sub ok");
assert_eq!(diff.pts, 0);
}
#[test]
fn test_clamp_cross_base() {
let ts = Timestamp::new(5_000, ms_tb());
let lo = Timestamp::new(90_000, tb_90k()); let hi = Timestamp::new(270_000, tb_90k()); let c = TimestampArith::clamp(&ts, &lo, &hi).expect("clamp ok");
assert_eq!(c.timebase, ms_tb());
assert_eq!(c.pts, 3_000);
}
#[test]
fn test_scale_duration_field() {
let tb = ms_tb();
let ts = Timestamp::with_dts(1_000, None, tb, Some(100)); let half = TimestampArith::scale(&ts, Rational::new(1, 2)).expect("scale ok");
assert_eq!(half.duration, Some(50));
}
#[test]
fn test_add_duration_summed() {
let tb = ms_tb();
let a = Timestamp::with_dts(0, None, tb, Some(500));
let b = Timestamp::with_dts(500, None, tb, Some(300));
let sum = TimestampArith::add(&a, &b).expect("add ok");
assert_eq!(sum.pts, 500);
assert_eq!(sum.duration, Some(800));
}
#[test]
fn test_rescale_48k_to_1k() {
let audio_tb = Rational::new(1, 48_000);
let ts = Timestamp::new(48_000, audio_tb);
let r = TimestampArith::rescale(&ts, ms_tb()).expect("rescale ok");
assert_eq!(r.pts, 1_000);
}
}