Skip to main content

ff_format/
time.rs

1//! Time primitives for video/audio processing.
2//!
3//! This module provides [`Rational`] for representing fractions (like time bases
4//! and frame rates) and [`Timestamp`] for representing media timestamps with
5//! their associated time base.
6//!
7//! # Examples
8//!
9//! ```
10//! use ff_format::{Rational, Timestamp};
11//! use std::time::Duration;
12//!
13//! // Create a rational number (e.g., 1/90000 time base)
14//! let time_base = Rational::new(1, 90000);
15//! assert_eq!(time_base.as_f64(), 1.0 / 90000.0);
16//!
17//! // Create a timestamp at 1 second (90000 * 1/90000)
18//! let ts = Timestamp::new(90000, time_base);
19//! assert!((ts.as_secs_f64() - 1.0).abs() < 0.0001);
20//!
21//! // Convert to Duration
22//! let duration = ts.as_duration();
23//! assert_eq!(duration.as_secs(), 1);
24//! ```
25
26// These casts are intentional for media timestamp arithmetic.
27// The values involved (PTS, time bases, frame rates) are well within
28// the safe ranges for these conversions in practical video/audio scenarios.
29#![allow(
30    clippy::cast_possible_truncation,
31    clippy::cast_possible_wrap,
32    clippy::cast_precision_loss,
33    clippy::cast_sign_loss
34)]
35
36use std::cmp::Ordering;
37use std::fmt;
38use std::ops::{Add, Div, Mul, Neg, Sub};
39use std::time::Duration;
40
41/// A rational number represented as a fraction (numerator / denominator).
42///
43/// This type is commonly used to represent:
44/// - Time bases (e.g., 1/90000 for MPEG-TS, 1/1000 for milliseconds)
45/// - Frame rates (e.g., 30000/1001 for 29.97 fps)
46/// - Aspect ratios (e.g., 16/9)
47///
48/// # Invariants
49///
50/// - Denominator is always positive (sign is in numerator)
51/// - Zero denominator is handled gracefully (returns infinity/NaN for conversions)
52///
53/// # Examples
54///
55/// ```
56/// use ff_format::Rational;
57///
58/// // Common time base for MPEG-TS
59/// let time_base = Rational::new(1, 90000);
60///
61/// // 29.97 fps (NTSC)
62/// let fps = Rational::new(30000, 1001);
63/// assert!((fps.as_f64() - 29.97).abs() < 0.01);
64///
65/// // Invert to get frame duration
66/// let frame_duration = fps.invert();
67/// assert_eq!(frame_duration.num(), 1001);
68/// assert_eq!(frame_duration.den(), 30000);
69/// ```
70#[derive(Debug, Clone, Copy)]
71pub struct Rational {
72    num: i32,
73    den: i32,
74}
75
76impl PartialEq for Rational {
77    fn eq(&self, other: &Self) -> bool {
78        // a/b == c/d iff a*d == b*c (cross-multiplication)
79        // Use i64 to avoid overflow
80        i64::from(self.num) * i64::from(other.den) == i64::from(other.num) * i64::from(self.den)
81    }
82}
83
84impl Eq for Rational {}
85
86impl std::hash::Hash for Rational {
87    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
88        // Hash the reduced form to ensure equal values have equal hashes
89        let reduced = self.reduce();
90        reduced.num.hash(state);
91        reduced.den.hash(state);
92    }
93}
94
95impl Rational {
96    /// Creates a new rational number.
97    ///
98    /// The denominator is normalized to always be positive (the sign is moved
99    /// to the numerator).
100    ///
101    /// # Panics
102    ///
103    /// Does not panic. A zero denominator is allowed but will result in
104    /// infinity or NaN when converted to floating-point.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use ff_format::Rational;
110    ///
111    /// let r = Rational::new(1, 2);
112    /// assert_eq!(r.num(), 1);
113    /// assert_eq!(r.den(), 2);
114    ///
115    /// // Negative denominator is normalized
116    /// let r = Rational::new(1, -2);
117    /// assert_eq!(r.num(), -1);
118    /// assert_eq!(r.den(), 2);
119    /// ```
120    #[must_use]
121    pub const fn new(num: i32, den: i32) -> Self {
122        // Normalize: denominator should always be positive
123        if den < 0 {
124            Self {
125                num: -num,
126                den: -den,
127            }
128        } else {
129            Self { num, den }
130        }
131    }
132
133    /// Creates a rational number representing zero (0/1).
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use ff_format::Rational;
139    ///
140    /// let zero = Rational::zero();
141    /// assert_eq!(zero.as_f64(), 0.0);
142    /// assert!(zero.is_zero());
143    /// ```
144    #[must_use]
145    pub const fn zero() -> Self {
146        Self { num: 0, den: 1 }
147    }
148
149    /// Creates a rational number representing one (1/1).
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use ff_format::Rational;
155    ///
156    /// let one = Rational::one();
157    /// assert_eq!(one.as_f64(), 1.0);
158    /// ```
159    #[must_use]
160    pub const fn one() -> Self {
161        Self { num: 1, den: 1 }
162    }
163
164    /// Returns the numerator.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use ff_format::Rational;
170    ///
171    /// let r = Rational::new(3, 4);
172    /// assert_eq!(r.num(), 3);
173    /// ```
174    #[must_use]
175    #[inline]
176    pub const fn num(&self) -> i32 {
177        self.num
178    }
179
180    /// Returns the denominator.
181    ///
182    /// The denominator is always non-negative.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use ff_format::Rational;
188    ///
189    /// let r = Rational::new(3, 4);
190    /// assert_eq!(r.den(), 4);
191    /// ```
192    #[must_use]
193    #[inline]
194    pub const fn den(&self) -> i32 {
195        self.den
196    }
197
198    /// Converts the rational number to a floating-point value.
199    ///
200    /// Returns `f64::INFINITY`, `f64::NEG_INFINITY`, or `f64::NAN` for
201    /// edge cases (division by zero).
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use ff_format::Rational;
207    ///
208    /// let r = Rational::new(1, 4);
209    /// assert_eq!(r.as_f64(), 0.25);
210    ///
211    /// let r = Rational::new(1, 3);
212    /// assert!((r.as_f64() - 0.333333).abs() < 0.001);
213    /// ```
214    #[must_use]
215    #[inline]
216    pub fn as_f64(self) -> f64 {
217        if self.den == 0 {
218            match self.num.cmp(&0) {
219                Ordering::Greater => f64::INFINITY,
220                Ordering::Less => f64::NEG_INFINITY,
221                Ordering::Equal => f64::NAN,
222            }
223        } else {
224            f64::from(self.num) / f64::from(self.den)
225        }
226    }
227
228    /// Converts the rational number to a single-precision floating-point value.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use ff_format::Rational;
234    ///
235    /// let r = Rational::new(1, 2);
236    /// assert_eq!(r.as_f32(), 0.5);
237    /// ```
238    #[must_use]
239    #[inline]
240    pub fn as_f32(self) -> f32 {
241        self.as_f64() as f32
242    }
243
244    /// Returns the inverse (reciprocal) of this rational number.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use ff_format::Rational;
250    ///
251    /// let r = Rational::new(3, 4);
252    /// let inv = r.invert();
253    /// assert_eq!(inv.num(), 4);
254    /// assert_eq!(inv.den(), 3);
255    ///
256    /// // Negative values
257    /// let r = Rational::new(-3, 4);
258    /// let inv = r.invert();
259    /// assert_eq!(inv.num(), -4);
260    /// assert_eq!(inv.den(), 3);
261    /// ```
262    #[must_use]
263    pub const fn invert(self) -> Self {
264        // Handle sign normalization when inverting
265        if self.num < 0 {
266            Self {
267                num: -self.den,
268                den: -self.num,
269            }
270        } else {
271            Self {
272                num: self.den,
273                den: self.num,
274            }
275        }
276    }
277
278    /// Returns true if this rational number is zero.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use ff_format::Rational;
284    ///
285    /// assert!(Rational::new(0, 1).is_zero());
286    /// assert!(Rational::new(0, 100).is_zero());
287    /// assert!(!Rational::new(1, 100).is_zero());
288    /// ```
289    #[must_use]
290    #[inline]
291    pub const fn is_zero(self) -> bool {
292        self.num == 0
293    }
294
295    /// Returns true if this rational number is positive.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use ff_format::Rational;
301    ///
302    /// assert!(Rational::new(1, 2).is_positive());
303    /// assert!(!Rational::new(-1, 2).is_positive());
304    /// assert!(!Rational::new(0, 1).is_positive());
305    /// ```
306    #[must_use]
307    #[inline]
308    pub const fn is_positive(self) -> bool {
309        self.num > 0 && self.den > 0
310    }
311
312    /// Returns true if this rational number is negative.
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// use ff_format::Rational;
318    ///
319    /// assert!(Rational::new(-1, 2).is_negative());
320    /// assert!(!Rational::new(1, 2).is_negative());
321    /// assert!(!Rational::new(0, 1).is_negative());
322    /// ```
323    #[must_use]
324    #[inline]
325    pub const fn is_negative(self) -> bool {
326        self.num < 0 && self.den > 0
327    }
328
329    /// Returns the absolute value of this rational number.
330    ///
331    /// # Examples
332    ///
333    /// ```
334    /// use ff_format::Rational;
335    ///
336    /// assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
337    /// assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
338    /// ```
339    #[must_use]
340    pub const fn abs(self) -> Self {
341        Self {
342            num: if self.num < 0 { -self.num } else { self.num },
343            den: self.den,
344        }
345    }
346
347    /// Reduces the rational to its lowest terms using GCD.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use ff_format::Rational;
353    ///
354    /// let r = Rational::new(4, 8);
355    /// let reduced = r.reduce();
356    /// assert_eq!(reduced.num(), 1);
357    /// assert_eq!(reduced.den(), 2);
358    /// ```
359    #[must_use]
360    pub fn reduce(self) -> Self {
361        if self.num == 0 {
362            return Self::new(0, 1);
363        }
364        let g = gcd(self.num.unsigned_abs(), self.den.unsigned_abs());
365        Self {
366            num: self.num / g as i32,
367            den: self.den / g as i32,
368        }
369    }
370}
371
372/// Computes the greatest common divisor using Euclidean algorithm.
373fn gcd(mut a: u32, mut b: u32) -> u32 {
374    while b != 0 {
375        let temp = b;
376        b = a % b;
377        a = temp;
378    }
379    a
380}
381
382impl Default for Rational {
383    /// Returns the default rational number (1/1).
384    fn default() -> Self {
385        Self::one()
386    }
387}
388
389impl fmt::Display for Rational {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        write!(f, "{}/{}", self.num, self.den)
392    }
393}
394
395impl From<i32> for Rational {
396    fn from(n: i32) -> Self {
397        Self::new(n, 1)
398    }
399}
400
401impl From<(i32, i32)> for Rational {
402    fn from((num, den): (i32, i32)) -> Self {
403        Self::new(num, den)
404    }
405}
406
407// Arithmetic operations for Rational
408
409impl Add for Rational {
410    type Output = Self;
411
412    fn add(self, rhs: Self) -> Self::Output {
413        // a/b + c/d = (ad + bc) / bd
414        let num =
415            i64::from(self.num) * i64::from(rhs.den) + i64::from(rhs.num) * i64::from(self.den);
416        let den = i64::from(self.den) * i64::from(rhs.den);
417
418        // Try to reduce to fit in i32
419        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
420        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
421    }
422}
423
424impl Sub for Rational {
425    type Output = Self;
426
427    fn sub(self, rhs: Self) -> Self::Output {
428        // a/b - c/d = (ad - bc) / bd
429        let num =
430            i64::from(self.num) * i64::from(rhs.den) - i64::from(rhs.num) * i64::from(self.den);
431        let den = i64::from(self.den) * i64::from(rhs.den);
432
433        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
434        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
435    }
436}
437
438impl Mul for Rational {
439    type Output = Self;
440
441    fn mul(self, rhs: Self) -> Self::Output {
442        // a/b * c/d = ac / bd
443        let num = i64::from(self.num) * i64::from(rhs.num);
444        let den = i64::from(self.den) * i64::from(rhs.den);
445
446        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
447        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
448    }
449}
450
451impl Div for Rational {
452    type Output = Self;
453
454    #[allow(clippy::suspicious_arithmetic_impl)]
455    fn div(self, rhs: Self) -> Self::Output {
456        // a/b / c/d = a/b * d/c = ad / bc
457        // Using multiplication by inverse is mathematically correct for rational division
458        self * rhs.invert()
459    }
460}
461
462impl Mul<i32> for Rational {
463    type Output = Self;
464
465    fn mul(self, rhs: i32) -> Self::Output {
466        let num = i64::from(self.num) * i64::from(rhs);
467        let g = gcd(num.unsigned_abs() as u32, self.den.unsigned_abs());
468        Self::new((num / i64::from(g)) as i32, self.den / g as i32)
469    }
470}
471
472impl Div<i32> for Rational {
473    type Output = Self;
474
475    fn div(self, rhs: i32) -> Self::Output {
476        let den = i64::from(self.den) * i64::from(rhs);
477        let g = gcd(self.num.unsigned_abs(), den.unsigned_abs() as u32);
478        Self::new(self.num / g as i32, (den / i64::from(g)) as i32)
479    }
480}
481
482impl Neg for Rational {
483    type Output = Self;
484
485    fn neg(self) -> Self::Output {
486        Self::new(-self.num, self.den)
487    }
488}
489
490impl PartialOrd for Rational {
491    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
492        Some(self.cmp(other))
493    }
494}
495
496impl Ord for Rational {
497    fn cmp(&self, other: &Self) -> Ordering {
498        // Compare a/b with c/d by comparing ad with bc
499        let left = i64::from(self.num) * i64::from(other.den);
500        let right = i64::from(other.num) * i64::from(self.den);
501        left.cmp(&right)
502    }
503}
504
505// ============================================================================
506// Timestamp
507// ============================================================================
508
509/// A timestamp representing a point in time within a media stream.
510///
511/// Timestamps are represented as a presentation timestamp (PTS) value
512/// combined with a time base that defines the unit of measurement.
513///
514/// # Time Base
515///
516/// The time base is a rational number that represents the duration of
517/// one timestamp unit. For example:
518/// - `1/90000`: Each PTS unit is 1/90000 of a second (MPEG-TS)
519/// - `1/1000`: Each PTS unit is 1 millisecond
520/// - `1/48000`: Each PTS unit is one audio sample at 48kHz
521///
522/// # Examples
523///
524/// ```
525/// use ff_format::{Rational, Timestamp};
526/// use std::time::Duration;
527///
528/// // Create a timestamp at 1 second using 90kHz time base
529/// let time_base = Rational::new(1, 90000);
530/// let ts = Timestamp::new(90000, time_base);
531///
532/// assert!((ts.as_secs_f64() - 1.0).abs() < 0.0001);
533/// assert_eq!(ts.as_millis(), 1000);
534///
535/// // Convert from Duration
536/// let ts2 = Timestamp::from_duration(Duration::from_secs(1), time_base);
537/// assert_eq!(ts2.pts(), 90000);
538/// ```
539#[derive(Debug, Clone, Copy)]
540pub struct Timestamp {
541    pts: i64,
542    time_base: Rational,
543}
544
545impl Timestamp {
546    /// Creates a new timestamp with the given PTS value and time base.
547    ///
548    /// # Arguments
549    ///
550    /// * `pts` - The presentation timestamp value
551    /// * `time_base` - The time base (duration of one PTS unit)
552    ///
553    /// # Examples
554    ///
555    /// ```
556    /// use ff_format::{Rational, Timestamp};
557    ///
558    /// let time_base = Rational::new(1, 1000);  // milliseconds
559    /// let ts = Timestamp::new(500, time_base);  // 500ms
560    /// assert_eq!(ts.as_millis(), 500);
561    /// ```
562    #[must_use]
563    pub const fn new(pts: i64, time_base: Rational) -> Self {
564        Self { pts, time_base }
565    }
566
567    /// Creates a timestamp representing zero (0 PTS).
568    ///
569    /// # Examples
570    ///
571    /// ```
572    /// use ff_format::{Rational, Timestamp};
573    ///
574    /// let time_base = Rational::new(1, 90000);
575    /// let zero = Timestamp::zero(time_base);
576    /// assert_eq!(zero.pts(), 0);
577    /// assert_eq!(zero.as_secs_f64(), 0.0);
578    /// ```
579    #[must_use]
580    pub const fn zero(time_base: Rational) -> Self {
581        Self { pts: 0, time_base }
582    }
583
584    /// Creates a timestamp from a Duration value.
585    ///
586    /// # Arguments
587    ///
588    /// * `duration` - The duration to convert
589    /// * `time_base` - The target time base for the resulting timestamp
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// use ff_format::{Rational, Timestamp};
595    /// use std::time::Duration;
596    ///
597    /// let time_base = Rational::new(1, 90000);
598    /// let ts = Timestamp::from_duration(Duration::from_millis(1000), time_base);
599    /// assert_eq!(ts.pts(), 90000);
600    /// ```
601    #[must_use]
602    pub fn from_duration(duration: Duration, time_base: Rational) -> Self {
603        let secs = duration.as_secs_f64();
604        let pts = (secs / time_base.as_f64()).round() as i64;
605        Self { pts, time_base }
606    }
607
608    /// Creates a timestamp from a seconds value.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// use ff_format::{Rational, Timestamp};
614    ///
615    /// let time_base = Rational::new(1, 1000);
616    /// let ts = Timestamp::from_secs_f64(1.5, time_base);
617    /// assert_eq!(ts.pts(), 1500);
618    /// ```
619    #[must_use]
620    pub fn from_secs_f64(secs: f64, time_base: Rational) -> Self {
621        let pts = (secs / time_base.as_f64()).round() as i64;
622        Self { pts, time_base }
623    }
624
625    /// Creates a timestamp from milliseconds.
626    ///
627    /// # Examples
628    ///
629    /// ```
630    /// use ff_format::{Rational, Timestamp};
631    ///
632    /// let time_base = Rational::new(1, 90000);
633    /// let ts = Timestamp::from_millis(1000, time_base);
634    /// assert_eq!(ts.pts(), 90000);
635    /// ```
636    #[must_use]
637    pub fn from_millis(millis: i64, time_base: Rational) -> Self {
638        let secs = millis as f64 / 1000.0;
639        Self::from_secs_f64(secs, time_base)
640    }
641
642    /// Returns the presentation timestamp value.
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use ff_format::{Rational, Timestamp};
648    ///
649    /// let ts = Timestamp::new(12345, Rational::new(1, 90000));
650    /// assert_eq!(ts.pts(), 12345);
651    /// ```
652    #[must_use]
653    #[inline]
654    pub const fn pts(&self) -> i64 {
655        self.pts
656    }
657
658    /// Returns the time base.
659    ///
660    /// # Examples
661    ///
662    /// ```
663    /// use ff_format::{Rational, Timestamp};
664    ///
665    /// let time_base = Rational::new(1, 90000);
666    /// let ts = Timestamp::new(100, time_base);
667    /// assert_eq!(ts.time_base(), time_base);
668    /// ```
669    #[must_use]
670    #[inline]
671    pub const fn time_base(&self) -> Rational {
672        self.time_base
673    }
674
675    /// Converts the timestamp to a Duration.
676    ///
677    /// Note: Negative timestamps will be clamped to zero Duration.
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// use ff_format::{Rational, Timestamp};
683    /// use std::time::Duration;
684    ///
685    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
686    /// let duration = ts.as_duration();
687    /// assert_eq!(duration, Duration::from_secs(1));
688    /// ```
689    #[must_use]
690    pub fn as_duration(&self) -> Duration {
691        let secs = self.as_secs_f64();
692        if secs < 0.0 {
693            log::warn!(
694                "timestamp is negative, clamping to zero \
695                 secs={secs} fallback=Duration::ZERO"
696            );
697            Duration::ZERO
698        } else {
699            Duration::from_secs_f64(secs)
700        }
701    }
702
703    /// Converts the timestamp to seconds as a floating-point value.
704    ///
705    /// # Examples
706    ///
707    /// ```
708    /// use ff_format::{Rational, Timestamp};
709    ///
710    /// let ts = Timestamp::new(45000, Rational::new(1, 90000));
711    /// assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
712    /// ```
713    #[must_use]
714    #[inline]
715    pub fn as_secs_f64(&self) -> f64 {
716        self.pts as f64 * self.time_base.as_f64()
717    }
718
719    /// Converts the timestamp to milliseconds.
720    ///
721    /// # Examples
722    ///
723    /// ```
724    /// use ff_format::{Rational, Timestamp};
725    ///
726    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
727    /// assert_eq!(ts.as_millis(), 1000);
728    /// ```
729    #[must_use]
730    #[inline]
731    pub fn as_millis(&self) -> i64 {
732        (self.as_secs_f64() * 1000.0).round() as i64
733    }
734
735    /// Converts the timestamp to microseconds.
736    ///
737    /// # Examples
738    ///
739    /// ```
740    /// use ff_format::{Rational, Timestamp};
741    ///
742    /// let ts = Timestamp::new(90, Rational::new(1, 90000));
743    /// assert_eq!(ts.as_micros(), 1000);  // 90/90000 = 0.001 sec = 1000 microseconds
744    /// ```
745    #[must_use]
746    #[inline]
747    pub fn as_micros(&self) -> i64 {
748        (self.as_secs_f64() * 1_000_000.0).round() as i64
749    }
750
751    /// Converts the timestamp to a frame number at the given frame rate.
752    ///
753    /// # Arguments
754    ///
755    /// * `fps` - The frame rate (frames per second)
756    ///
757    /// # Examples
758    ///
759    /// ```
760    /// use ff_format::{Rational, Timestamp};
761    ///
762    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));  // 1 second
763    /// assert_eq!(ts.as_frame_number(30.0), 30);  // 30 fps
764    /// assert_eq!(ts.as_frame_number(60.0), 60);  // 60 fps
765    /// ```
766    #[must_use]
767    #[inline]
768    pub fn as_frame_number(&self, fps: f64) -> u64 {
769        let secs = self.as_secs_f64();
770        if secs < 0.0 {
771            log::warn!(
772                "timestamp is negative, returning frame 0 \
773                 secs={secs} fps={fps} fallback=0"
774            );
775            0
776        } else {
777            (secs * fps).round() as u64
778        }
779    }
780
781    /// Converts the timestamp to a frame number using a rational frame rate.
782    ///
783    /// # Arguments
784    ///
785    /// * `fps` - The frame rate as a rational number
786    ///
787    /// # Examples
788    ///
789    /// ```
790    /// use ff_format::{Rational, Timestamp};
791    ///
792    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));  // 1 second
793    /// let fps = Rational::new(30000, 1001);  // 29.97 fps
794    /// let frame = ts.as_frame_number_rational(fps);
795    /// assert!(frame == 29 || frame == 30);  // Should be approximately 30
796    /// ```
797    #[must_use]
798    pub fn as_frame_number_rational(&self, fps: Rational) -> u64 {
799        self.as_frame_number(fps.as_f64())
800    }
801
802    /// Rescales this timestamp to a different time base.
803    ///
804    /// # Arguments
805    ///
806    /// * `new_time_base` - The target time base
807    ///
808    /// # Examples
809    ///
810    /// ```
811    /// use ff_format::{Rational, Timestamp};
812    ///
813    /// let ts = Timestamp::new(1000, Rational::new(1, 1000));  // 1 second
814    /// let rescaled = ts.rescale(Rational::new(1, 90000));
815    /// assert_eq!(rescaled.pts(), 90000);
816    /// ```
817    #[must_use]
818    pub fn rescale(&self, new_time_base: Rational) -> Self {
819        let secs = self.as_secs_f64();
820        Self::from_secs_f64(secs, new_time_base)
821    }
822
823    /// Returns true if this timestamp is zero.
824    ///
825    /// # Examples
826    ///
827    /// ```
828    /// use ff_format::{Rational, Timestamp};
829    ///
830    /// let zero = Timestamp::zero(Rational::new(1, 90000));
831    /// assert!(zero.is_zero());
832    ///
833    /// let non_zero = Timestamp::new(100, Rational::new(1, 90000));
834    /// assert!(!non_zero.is_zero());
835    /// ```
836    #[must_use]
837    #[inline]
838    pub const fn is_zero(&self) -> bool {
839        self.pts == 0
840    }
841
842    /// Returns true if this timestamp is negative.
843    ///
844    /// # Examples
845    ///
846    /// ```
847    /// use ff_format::{Rational, Timestamp};
848    ///
849    /// let negative = Timestamp::new(-100, Rational::new(1, 90000));
850    /// assert!(negative.is_negative());
851    /// ```
852    #[must_use]
853    #[inline]
854    pub const fn is_negative(&self) -> bool {
855        self.pts < 0
856    }
857}
858
859impl Default for Timestamp {
860    /// Returns a default timestamp (0 with 1/90000 time base).
861    fn default() -> Self {
862        Self::new(0, Rational::new(1, 90000))
863    }
864}
865
866impl fmt::Display for Timestamp {
867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868        let secs = self.as_secs_f64();
869        let hours = (secs / 3600.0).floor() as u32;
870        let mins = ((secs % 3600.0) / 60.0).floor() as u32;
871        let secs_remainder = secs % 60.0;
872        write!(f, "{hours:02}:{mins:02}:{secs_remainder:06.3}")
873    }
874}
875
876impl PartialEq for Timestamp {
877    fn eq(&self, other: &Self) -> bool {
878        // Compare by converting to common representation (seconds)
879        (self.as_secs_f64() - other.as_secs_f64()).abs() < 1e-9
880    }
881}
882
883impl Eq for Timestamp {}
884
885impl PartialOrd for Timestamp {
886    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
887        Some(self.cmp(other))
888    }
889}
890
891impl Ord for Timestamp {
892    fn cmp(&self, other: &Self) -> Ordering {
893        self.as_secs_f64()
894            .partial_cmp(&other.as_secs_f64())
895            .unwrap_or_else(|| {
896                log::warn!(
897                    "NaN timestamp comparison, treating as equal \
898                     self_pts={} other_pts={} fallback=Ordering::Equal",
899                    self.pts,
900                    other.pts
901                );
902                Ordering::Equal
903            })
904    }
905}
906
907impl Add for Timestamp {
908    type Output = Self;
909
910    fn add(self, rhs: Self) -> Self::Output {
911        let secs = self.as_secs_f64() + rhs.as_secs_f64();
912        Self::from_secs_f64(secs, self.time_base)
913    }
914}
915
916impl Sub for Timestamp {
917    type Output = Self;
918
919    fn sub(self, rhs: Self) -> Self::Output {
920        let secs = self.as_secs_f64() - rhs.as_secs_f64();
921        Self::from_secs_f64(secs, self.time_base)
922    }
923}
924
925impl Add<Duration> for Timestamp {
926    type Output = Self;
927
928    fn add(self, rhs: Duration) -> Self::Output {
929        let secs = self.as_secs_f64() + rhs.as_secs_f64();
930        Self::from_secs_f64(secs, self.time_base)
931    }
932}
933
934impl Sub<Duration> for Timestamp {
935    type Output = Self;
936
937    fn sub(self, rhs: Duration) -> Self::Output {
938        let secs = self.as_secs_f64() - rhs.as_secs_f64();
939        Self::from_secs_f64(secs, self.time_base)
940    }
941}
942
943#[cfg(test)]
944#[allow(
945    clippy::unwrap_used,
946    clippy::float_cmp,
947    clippy::similar_names,
948    clippy::redundant_closure_for_method_calls
949)]
950mod tests {
951    use super::*;
952
953    /// Helper for approximate float comparison in tests
954    fn approx_eq(a: f64, b: f64) -> bool {
955        (a - b).abs() < 1e-9
956    }
957
958    // ==================== Rational Tests ====================
959
960    mod rational_tests {
961        use super::*;
962
963        #[test]
964        fn test_new() {
965            let r = Rational::new(1, 2);
966            assert_eq!(r.num(), 1);
967            assert_eq!(r.den(), 2);
968        }
969
970        #[test]
971        fn test_new_negative_denominator() {
972            // Negative denominator should be normalized
973            let r = Rational::new(1, -2);
974            assert_eq!(r.num(), -1);
975            assert_eq!(r.den(), 2);
976
977            let r = Rational::new(-1, -2);
978            assert_eq!(r.num(), 1);
979            assert_eq!(r.den(), 2);
980        }
981
982        #[test]
983        fn test_zero_and_one() {
984            let zero = Rational::zero();
985            assert!(zero.is_zero());
986            assert!(approx_eq(zero.as_f64(), 0.0));
987
988            let one = Rational::one();
989            assert!(approx_eq(one.as_f64(), 1.0));
990            assert!(!one.is_zero());
991        }
992
993        #[test]
994        fn test_as_f64() {
995            assert!(approx_eq(Rational::new(1, 2).as_f64(), 0.5));
996            assert!(approx_eq(Rational::new(1, 4).as_f64(), 0.25));
997            assert!((Rational::new(1, 3).as_f64() - 0.333_333).abs() < 0.001);
998            assert!(approx_eq(Rational::new(-1, 2).as_f64(), -0.5));
999        }
1000
1001        #[test]
1002        fn test_as_f64_division_by_zero() {
1003            assert!(Rational::new(1, 0).as_f64().is_infinite());
1004            assert!(Rational::new(1, 0).as_f64().is_sign_positive());
1005            assert!(Rational::new(-1, 0).as_f64().is_infinite());
1006            assert!(Rational::new(-1, 0).as_f64().is_sign_negative());
1007            assert!(Rational::new(0, 0).as_f64().is_nan());
1008        }
1009
1010        #[test]
1011        fn test_as_f32() {
1012            assert_eq!(Rational::new(1, 2).as_f32(), 0.5);
1013        }
1014
1015        #[test]
1016        fn test_invert() {
1017            let r = Rational::new(3, 4);
1018            let inv = r.invert();
1019            assert_eq!(inv.num(), 4);
1020            assert_eq!(inv.den(), 3);
1021
1022            // Negative value
1023            let r = Rational::new(-3, 4);
1024            let inv = r.invert();
1025            assert_eq!(inv.num(), -4);
1026            assert_eq!(inv.den(), 3);
1027        }
1028
1029        #[test]
1030        fn test_is_positive_negative() {
1031            assert!(Rational::new(1, 2).is_positive());
1032            assert!(!Rational::new(-1, 2).is_positive());
1033            assert!(!Rational::new(0, 1).is_positive());
1034
1035            assert!(Rational::new(-1, 2).is_negative());
1036            assert!(!Rational::new(1, 2).is_negative());
1037            assert!(!Rational::new(0, 1).is_negative());
1038        }
1039
1040        #[test]
1041        fn test_abs() {
1042            assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
1043            assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
1044            assert_eq!(Rational::new(0, 4).abs(), Rational::new(0, 4));
1045        }
1046
1047        #[test]
1048        fn test_reduce() {
1049            let r = Rational::new(4, 8);
1050            let reduced = r.reduce();
1051            assert_eq!(reduced.num(), 1);
1052            assert_eq!(reduced.den(), 2);
1053
1054            let r = Rational::new(6, 9);
1055            let reduced = r.reduce();
1056            assert_eq!(reduced.num(), 2);
1057            assert_eq!(reduced.den(), 3);
1058
1059            let r = Rational::new(0, 5);
1060            let reduced = r.reduce();
1061            assert_eq!(reduced.num(), 0);
1062            assert_eq!(reduced.den(), 1);
1063        }
1064
1065        #[test]
1066        fn test_add() {
1067            let a = Rational::new(1, 2);
1068            let b = Rational::new(1, 4);
1069            let result = a + b;
1070            assert!((result.as_f64() - 0.75).abs() < 0.0001);
1071        }
1072
1073        #[test]
1074        fn test_sub() {
1075            let a = Rational::new(1, 2);
1076            let b = Rational::new(1, 4);
1077            let result = a - b;
1078            assert!((result.as_f64() - 0.25).abs() < 0.0001);
1079        }
1080
1081        #[test]
1082        fn test_mul() {
1083            let a = Rational::new(1, 2);
1084            let b = Rational::new(2, 3);
1085            let result = a * b;
1086            assert!((result.as_f64() - (1.0 / 3.0)).abs() < 0.0001);
1087        }
1088
1089        #[test]
1090        fn test_div() {
1091            let a = Rational::new(1, 2);
1092            let b = Rational::new(2, 3);
1093            let result = a / b;
1094            assert!((result.as_f64() - 0.75).abs() < 0.0001);
1095        }
1096
1097        #[test]
1098        fn test_mul_i32() {
1099            let r = Rational::new(1, 4);
1100            let result = r * 2;
1101            assert!((result.as_f64() - 0.5).abs() < 0.0001);
1102        }
1103
1104        #[test]
1105        fn test_div_i32() {
1106            let r = Rational::new(1, 2);
1107            let result = r / 2;
1108            assert!((result.as_f64() - 0.25).abs() < 0.0001);
1109        }
1110
1111        #[test]
1112        fn test_neg() {
1113            let r = Rational::new(1, 2);
1114            let neg = -r;
1115            assert_eq!(neg.num(), -1);
1116            assert_eq!(neg.den(), 2);
1117        }
1118
1119        #[test]
1120        fn test_ord() {
1121            let a = Rational::new(1, 2);
1122            let b = Rational::new(1, 3);
1123            let c = Rational::new(2, 4);
1124
1125            assert!(a > b);
1126            assert!(b < a);
1127            assert_eq!(a, c);
1128            assert!(a >= c);
1129            assert!(a <= c);
1130        }
1131
1132        #[test]
1133        fn test_from_i32() {
1134            let r: Rational = 5.into();
1135            assert_eq!(r.num(), 5);
1136            assert_eq!(r.den(), 1);
1137        }
1138
1139        #[test]
1140        fn test_from_tuple() {
1141            let r: Rational = (3, 4).into();
1142            assert_eq!(r.num(), 3);
1143            assert_eq!(r.den(), 4);
1144        }
1145
1146        #[test]
1147        fn test_display() {
1148            assert_eq!(format!("{}", Rational::new(1, 2)), "1/2");
1149            assert_eq!(format!("{}", Rational::new(-3, 4)), "-3/4");
1150        }
1151
1152        #[test]
1153        fn test_default() {
1154            assert_eq!(Rational::default(), Rational::one());
1155        }
1156
1157        #[test]
1158        fn test_common_frame_rates() {
1159            // 23.976 fps (film)
1160            let fps = Rational::new(24000, 1001);
1161            assert!((fps.as_f64() - 23.976).abs() < 0.001);
1162
1163            // 29.97 fps (NTSC)
1164            let fps = Rational::new(30000, 1001);
1165            assert!((fps.as_f64() - 29.97).abs() < 0.01);
1166
1167            // 59.94 fps (NTSC interlaced as progressive)
1168            let fps = Rational::new(60000, 1001);
1169            assert!((fps.as_f64() - 59.94).abs() < 0.01);
1170        }
1171    }
1172
1173    // ==================== Timestamp Tests ====================
1174
1175    mod timestamp_tests {
1176        use super::*;
1177
1178        fn time_base_90k() -> Rational {
1179            Rational::new(1, 90000)
1180        }
1181
1182        fn time_base_1k() -> Rational {
1183            Rational::new(1, 1000)
1184        }
1185
1186        #[test]
1187        fn test_new() {
1188            let ts = Timestamp::new(90000, time_base_90k());
1189            assert_eq!(ts.pts(), 90000);
1190            assert_eq!(ts.time_base(), time_base_90k());
1191        }
1192
1193        #[test]
1194        fn test_zero() {
1195            let ts = Timestamp::zero(time_base_90k());
1196            assert_eq!(ts.pts(), 0);
1197            assert!(ts.is_zero());
1198            assert!(approx_eq(ts.as_secs_f64(), 0.0));
1199        }
1200
1201        #[test]
1202        fn test_from_duration() {
1203            let ts = Timestamp::from_duration(Duration::from_secs(1), time_base_90k());
1204            assert_eq!(ts.pts(), 90000);
1205
1206            let ts = Timestamp::from_duration(Duration::from_millis(500), time_base_90k());
1207            assert_eq!(ts.pts(), 45000);
1208        }
1209
1210        #[test]
1211        fn test_from_secs_f64() {
1212            let ts = Timestamp::from_secs_f64(1.5, time_base_1k());
1213            assert_eq!(ts.pts(), 1500);
1214        }
1215
1216        #[test]
1217        fn test_from_millis() {
1218            let ts = Timestamp::from_millis(1000, time_base_90k());
1219            assert_eq!(ts.pts(), 90000);
1220
1221            let ts = Timestamp::from_millis(500, time_base_1k());
1222            assert_eq!(ts.pts(), 500);
1223        }
1224
1225        #[test]
1226        fn test_as_duration() {
1227            let ts = Timestamp::new(90000, time_base_90k());
1228            let duration = ts.as_duration();
1229            assert_eq!(duration, Duration::from_secs(1));
1230
1231            // Negative timestamp clamps to zero
1232            let ts = Timestamp::new(-100, time_base_90k());
1233            assert_eq!(ts.as_duration(), Duration::ZERO);
1234        }
1235
1236        #[test]
1237        fn test_as_secs_f64() {
1238            let ts = Timestamp::new(45000, time_base_90k());
1239            assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
1240        }
1241
1242        #[test]
1243        fn test_as_millis() {
1244            let ts = Timestamp::new(90000, time_base_90k());
1245            assert_eq!(ts.as_millis(), 1000);
1246
1247            let ts = Timestamp::new(45000, time_base_90k());
1248            assert_eq!(ts.as_millis(), 500);
1249        }
1250
1251        #[test]
1252        fn test_as_micros() {
1253            let ts = Timestamp::new(90, time_base_90k());
1254            assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 us
1255        }
1256
1257        #[test]
1258        fn test_as_frame_number() {
1259            let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1260            assert_eq!(ts.as_frame_number(30.0), 30);
1261            assert_eq!(ts.as_frame_number(60.0), 60);
1262            assert_eq!(ts.as_frame_number(24.0), 24);
1263
1264            // Negative timestamp
1265            let ts = Timestamp::new(-90000, time_base_90k());
1266            assert_eq!(ts.as_frame_number(30.0), 0);
1267        }
1268
1269        #[test]
1270        fn test_as_frame_number_rational() {
1271            let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1272            let fps = Rational::new(30, 1);
1273            assert_eq!(ts.as_frame_number_rational(fps), 30);
1274        }
1275
1276        #[test]
1277        fn test_rescale() {
1278            let ts = Timestamp::new(1000, time_base_1k()); // 1 second
1279            let rescaled = ts.rescale(time_base_90k());
1280            assert_eq!(rescaled.pts(), 90000);
1281        }
1282
1283        #[test]
1284        fn test_is_zero() {
1285            assert!(Timestamp::zero(time_base_90k()).is_zero());
1286            assert!(!Timestamp::new(1, time_base_90k()).is_zero());
1287        }
1288
1289        #[test]
1290        fn test_is_negative() {
1291            assert!(Timestamp::new(-100, time_base_90k()).is_negative());
1292            assert!(!Timestamp::new(100, time_base_90k()).is_negative());
1293            assert!(!Timestamp::new(0, time_base_90k()).is_negative());
1294        }
1295
1296        #[test]
1297        fn test_display() {
1298            // 1 hour, 2 minutes, 3.456 seconds
1299            let secs = 3600.0 + 120.0 + 3.456;
1300            let ts = Timestamp::from_secs_f64(secs, time_base_90k());
1301            let display = format!("{ts}");
1302            assert!(display.starts_with("01:02:03"));
1303        }
1304
1305        #[test]
1306        fn test_eq() {
1307            let ts1 = Timestamp::new(90000, time_base_90k());
1308            let ts2 = Timestamp::new(1000, time_base_1k());
1309            assert_eq!(ts1, ts2); // Both are 1 second
1310        }
1311
1312        #[test]
1313        fn test_ord() {
1314            let ts1 = Timestamp::new(45000, time_base_90k()); // 0.5 sec
1315            let ts2 = Timestamp::new(90000, time_base_90k()); // 1.0 sec
1316            assert!(ts1 < ts2);
1317            assert!(ts2 > ts1);
1318        }
1319
1320        #[test]
1321        fn test_add() {
1322            let ts1 = Timestamp::new(45000, time_base_90k());
1323            let ts2 = Timestamp::new(45000, time_base_90k());
1324            let sum = ts1 + ts2;
1325            assert_eq!(sum.pts(), 90000);
1326        }
1327
1328        #[test]
1329        fn test_sub() {
1330            let ts1 = Timestamp::new(90000, time_base_90k());
1331            let ts2 = Timestamp::new(45000, time_base_90k());
1332            let diff = ts1 - ts2;
1333            assert_eq!(diff.pts(), 45000);
1334        }
1335
1336        #[test]
1337        fn test_add_duration() {
1338            let ts = Timestamp::new(45000, time_base_90k());
1339            let result = ts + Duration::from_millis(500);
1340            assert_eq!(result.pts(), 90000);
1341        }
1342
1343        #[test]
1344        fn test_sub_duration() {
1345            let ts = Timestamp::new(90000, time_base_90k());
1346            let result = ts - Duration::from_millis(500);
1347            assert_eq!(result.pts(), 45000);
1348        }
1349
1350        #[test]
1351        fn test_default() {
1352            let ts = Timestamp::default();
1353            assert_eq!(ts.pts(), 0);
1354            assert_eq!(ts.time_base(), Rational::new(1, 90000));
1355        }
1356
1357        #[test]
1358        fn test_video_timestamps() {
1359            // Common video time base: 1/90000 (MPEG-TS)
1360            let time_base = Rational::new(1, 90000);
1361
1362            // At 30 fps, each frame is 3000 PTS units
1363            let frame_duration_pts = 90000 / 30;
1364            assert_eq!(frame_duration_pts, 3000);
1365
1366            // Frame 0
1367            let frame0 = Timestamp::new(0, time_base);
1368            assert_eq!(frame0.as_frame_number(30.0), 0);
1369
1370            // Frame 30 (1 second)
1371            let frame30 = Timestamp::new(90000, time_base);
1372            assert_eq!(frame30.as_frame_number(30.0), 30);
1373        }
1374
1375        #[test]
1376        fn test_audio_timestamps() {
1377            // Audio at 48kHz - each sample is 1/48000 seconds
1378            let time_base = Rational::new(1, 48000);
1379
1380            // 1024 samples (common audio frame size)
1381            let ts = Timestamp::new(1024, time_base);
1382            let ms = ts.as_secs_f64() * 1000.0;
1383            assert!((ms - 21.333).abs() < 0.01); // ~21.33 ms
1384        }
1385    }
1386
1387    // ==================== GCD Tests ====================
1388
1389    #[test]
1390    fn test_gcd() {
1391        assert_eq!(gcd(12, 8), 4);
1392        assert_eq!(gcd(17, 13), 1);
1393        assert_eq!(gcd(100, 25), 25);
1394        assert_eq!(gcd(0, 5), 5);
1395        assert_eq!(gcd(5, 0), 5);
1396    }
1397}