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 Duration::ZERO
694 } else {
695 Duration::from_secs_f64(secs)
696 }
697 }
698
699 /// Converts the timestamp to seconds as a floating-point value.
700 ///
701 /// # Examples
702 ///
703 /// ```
704 /// use ff_format::{Rational, Timestamp};
705 ///
706 /// let ts = Timestamp::new(45000, Rational::new(1, 90000));
707 /// assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
708 /// ```
709 #[must_use]
710 #[inline]
711 pub fn as_secs_f64(&self) -> f64 {
712 self.pts as f64 * self.time_base.as_f64()
713 }
714
715 /// Converts the timestamp to milliseconds.
716 ///
717 /// # Examples
718 ///
719 /// ```
720 /// use ff_format::{Rational, Timestamp};
721 ///
722 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
723 /// assert_eq!(ts.as_millis(), 1000);
724 /// ```
725 #[must_use]
726 #[inline]
727 pub fn as_millis(&self) -> i64 {
728 (self.as_secs_f64() * 1000.0).round() as i64
729 }
730
731 /// Converts the timestamp to microseconds.
732 ///
733 /// # Examples
734 ///
735 /// ```
736 /// use ff_format::{Rational, Timestamp};
737 ///
738 /// let ts = Timestamp::new(90, Rational::new(1, 90000));
739 /// assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 microseconds
740 /// ```
741 #[must_use]
742 #[inline]
743 pub fn as_micros(&self) -> i64 {
744 (self.as_secs_f64() * 1_000_000.0).round() as i64
745 }
746
747 /// Converts the timestamp to a frame number at the given frame rate.
748 ///
749 /// # Arguments
750 ///
751 /// * `fps` - The frame rate (frames per second)
752 ///
753 /// # Examples
754 ///
755 /// ```
756 /// use ff_format::{Rational, Timestamp};
757 ///
758 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
759 /// assert_eq!(ts.as_frame_number(30.0), 30); // 30 fps
760 /// assert_eq!(ts.as_frame_number(60.0), 60); // 60 fps
761 /// ```
762 #[must_use]
763 #[inline]
764 pub fn as_frame_number(&self, fps: f64) -> u64 {
765 let secs = self.as_secs_f64();
766 if secs < 0.0 {
767 0
768 } else {
769 (secs * fps).round() as u64
770 }
771 }
772
773 /// Converts the timestamp to a frame number using a rational frame rate.
774 ///
775 /// # Arguments
776 ///
777 /// * `fps` - The frame rate as a rational number
778 ///
779 /// # Examples
780 ///
781 /// ```
782 /// use ff_format::{Rational, Timestamp};
783 ///
784 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
785 /// let fps = Rational::new(30000, 1001); // 29.97 fps
786 /// let frame = ts.as_frame_number_rational(fps);
787 /// assert!(frame == 29 || frame == 30); // Should be approximately 30
788 /// ```
789 #[must_use]
790 pub fn as_frame_number_rational(&self, fps: Rational) -> u64 {
791 self.as_frame_number(fps.as_f64())
792 }
793
794 /// Rescales this timestamp to a different time base.
795 ///
796 /// # Arguments
797 ///
798 /// * `new_time_base` - The target time base
799 ///
800 /// # Examples
801 ///
802 /// ```
803 /// use ff_format::{Rational, Timestamp};
804 ///
805 /// let ts = Timestamp::new(1000, Rational::new(1, 1000)); // 1 second
806 /// let rescaled = ts.rescale(Rational::new(1, 90000));
807 /// assert_eq!(rescaled.pts(), 90000);
808 /// ```
809 #[must_use]
810 pub fn rescale(&self, new_time_base: Rational) -> Self {
811 let secs = self.as_secs_f64();
812 Self::from_secs_f64(secs, new_time_base)
813 }
814
815 /// Returns true if this timestamp is zero.
816 ///
817 /// # Examples
818 ///
819 /// ```
820 /// use ff_format::{Rational, Timestamp};
821 ///
822 /// let zero = Timestamp::zero(Rational::new(1, 90000));
823 /// assert!(zero.is_zero());
824 ///
825 /// let non_zero = Timestamp::new(100, Rational::new(1, 90000));
826 /// assert!(!non_zero.is_zero());
827 /// ```
828 #[must_use]
829 #[inline]
830 pub const fn is_zero(&self) -> bool {
831 self.pts == 0
832 }
833
834 /// Returns true if this timestamp is negative.
835 ///
836 /// # Examples
837 ///
838 /// ```
839 /// use ff_format::{Rational, Timestamp};
840 ///
841 /// let negative = Timestamp::new(-100, Rational::new(1, 90000));
842 /// assert!(negative.is_negative());
843 /// ```
844 #[must_use]
845 #[inline]
846 pub const fn is_negative(&self) -> bool {
847 self.pts < 0
848 }
849}
850
851impl Default for Timestamp {
852 /// Returns a default timestamp (0 with 1/90000 time base).
853 fn default() -> Self {
854 Self::new(0, Rational::new(1, 90000))
855 }
856}
857
858impl fmt::Display for Timestamp {
859 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
860 let secs = self.as_secs_f64();
861 let hours = (secs / 3600.0).floor() as u32;
862 let mins = ((secs % 3600.0) / 60.0).floor() as u32;
863 let secs_remainder = secs % 60.0;
864 write!(f, "{hours:02}:{mins:02}:{secs_remainder:06.3}")
865 }
866}
867
868impl PartialEq for Timestamp {
869 fn eq(&self, other: &Self) -> bool {
870 // Compare by converting to common representation (seconds)
871 (self.as_secs_f64() - other.as_secs_f64()).abs() < 1e-9
872 }
873}
874
875impl Eq for Timestamp {}
876
877impl PartialOrd for Timestamp {
878 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
879 Some(self.cmp(other))
880 }
881}
882
883impl Ord for Timestamp {
884 fn cmp(&self, other: &Self) -> Ordering {
885 self.as_secs_f64()
886 .partial_cmp(&other.as_secs_f64())
887 .unwrap_or(Ordering::Equal)
888 }
889}
890
891impl Add for Timestamp {
892 type Output = Self;
893
894 fn add(self, rhs: Self) -> Self::Output {
895 let secs = self.as_secs_f64() + rhs.as_secs_f64();
896 Self::from_secs_f64(secs, self.time_base)
897 }
898}
899
900impl Sub for Timestamp {
901 type Output = Self;
902
903 fn sub(self, rhs: Self) -> Self::Output {
904 let secs = self.as_secs_f64() - rhs.as_secs_f64();
905 Self::from_secs_f64(secs, self.time_base)
906 }
907}
908
909impl Add<Duration> for Timestamp {
910 type Output = Self;
911
912 fn add(self, rhs: Duration) -> Self::Output {
913 let secs = self.as_secs_f64() + rhs.as_secs_f64();
914 Self::from_secs_f64(secs, self.time_base)
915 }
916}
917
918impl Sub<Duration> for Timestamp {
919 type Output = Self;
920
921 fn sub(self, rhs: Duration) -> Self::Output {
922 let secs = self.as_secs_f64() - rhs.as_secs_f64();
923 Self::from_secs_f64(secs, self.time_base)
924 }
925}
926
927#[cfg(test)]
928#[allow(
929 clippy::unwrap_used,
930 clippy::float_cmp,
931 clippy::similar_names,
932 clippy::redundant_closure_for_method_calls
933)]
934mod tests {
935 use super::*;
936
937 /// Helper for approximate float comparison in tests
938 fn approx_eq(a: f64, b: f64) -> bool {
939 (a - b).abs() < 1e-9
940 }
941
942 // ==================== Rational Tests ====================
943
944 mod rational_tests {
945 use super::*;
946
947 #[test]
948 fn test_new() {
949 let r = Rational::new(1, 2);
950 assert_eq!(r.num(), 1);
951 assert_eq!(r.den(), 2);
952 }
953
954 #[test]
955 fn test_new_negative_denominator() {
956 // Negative denominator should be normalized
957 let r = Rational::new(1, -2);
958 assert_eq!(r.num(), -1);
959 assert_eq!(r.den(), 2);
960
961 let r = Rational::new(-1, -2);
962 assert_eq!(r.num(), 1);
963 assert_eq!(r.den(), 2);
964 }
965
966 #[test]
967 fn test_zero_and_one() {
968 let zero = Rational::zero();
969 assert!(zero.is_zero());
970 assert!(approx_eq(zero.as_f64(), 0.0));
971
972 let one = Rational::one();
973 assert!(approx_eq(one.as_f64(), 1.0));
974 assert!(!one.is_zero());
975 }
976
977 #[test]
978 fn test_as_f64() {
979 assert!(approx_eq(Rational::new(1, 2).as_f64(), 0.5));
980 assert!(approx_eq(Rational::new(1, 4).as_f64(), 0.25));
981 assert!((Rational::new(1, 3).as_f64() - 0.333_333).abs() < 0.001);
982 assert!(approx_eq(Rational::new(-1, 2).as_f64(), -0.5));
983 }
984
985 #[test]
986 fn test_as_f64_division_by_zero() {
987 assert!(Rational::new(1, 0).as_f64().is_infinite());
988 assert!(Rational::new(1, 0).as_f64().is_sign_positive());
989 assert!(Rational::new(-1, 0).as_f64().is_infinite());
990 assert!(Rational::new(-1, 0).as_f64().is_sign_negative());
991 assert!(Rational::new(0, 0).as_f64().is_nan());
992 }
993
994 #[test]
995 fn test_as_f32() {
996 assert_eq!(Rational::new(1, 2).as_f32(), 0.5);
997 }
998
999 #[test]
1000 fn test_invert() {
1001 let r = Rational::new(3, 4);
1002 let inv = r.invert();
1003 assert_eq!(inv.num(), 4);
1004 assert_eq!(inv.den(), 3);
1005
1006 // Negative value
1007 let r = Rational::new(-3, 4);
1008 let inv = r.invert();
1009 assert_eq!(inv.num(), -4);
1010 assert_eq!(inv.den(), 3);
1011 }
1012
1013 #[test]
1014 fn test_is_positive_negative() {
1015 assert!(Rational::new(1, 2).is_positive());
1016 assert!(!Rational::new(-1, 2).is_positive());
1017 assert!(!Rational::new(0, 1).is_positive());
1018
1019 assert!(Rational::new(-1, 2).is_negative());
1020 assert!(!Rational::new(1, 2).is_negative());
1021 assert!(!Rational::new(0, 1).is_negative());
1022 }
1023
1024 #[test]
1025 fn test_abs() {
1026 assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
1027 assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
1028 assert_eq!(Rational::new(0, 4).abs(), Rational::new(0, 4));
1029 }
1030
1031 #[test]
1032 fn test_reduce() {
1033 let r = Rational::new(4, 8);
1034 let reduced = r.reduce();
1035 assert_eq!(reduced.num(), 1);
1036 assert_eq!(reduced.den(), 2);
1037
1038 let r = Rational::new(6, 9);
1039 let reduced = r.reduce();
1040 assert_eq!(reduced.num(), 2);
1041 assert_eq!(reduced.den(), 3);
1042
1043 let r = Rational::new(0, 5);
1044 let reduced = r.reduce();
1045 assert_eq!(reduced.num(), 0);
1046 assert_eq!(reduced.den(), 1);
1047 }
1048
1049 #[test]
1050 fn test_add() {
1051 let a = Rational::new(1, 2);
1052 let b = Rational::new(1, 4);
1053 let result = a + b;
1054 assert!((result.as_f64() - 0.75).abs() < 0.0001);
1055 }
1056
1057 #[test]
1058 fn test_sub() {
1059 let a = Rational::new(1, 2);
1060 let b = Rational::new(1, 4);
1061 let result = a - b;
1062 assert!((result.as_f64() - 0.25).abs() < 0.0001);
1063 }
1064
1065 #[test]
1066 fn test_mul() {
1067 let a = Rational::new(1, 2);
1068 let b = Rational::new(2, 3);
1069 let result = a * b;
1070 assert!((result.as_f64() - (1.0 / 3.0)).abs() < 0.0001);
1071 }
1072
1073 #[test]
1074 fn test_div() {
1075 let a = Rational::new(1, 2);
1076 let b = Rational::new(2, 3);
1077 let result = a / b;
1078 assert!((result.as_f64() - 0.75).abs() < 0.0001);
1079 }
1080
1081 #[test]
1082 fn test_mul_i32() {
1083 let r = Rational::new(1, 4);
1084 let result = r * 2;
1085 assert!((result.as_f64() - 0.5).abs() < 0.0001);
1086 }
1087
1088 #[test]
1089 fn test_div_i32() {
1090 let r = Rational::new(1, 2);
1091 let result = r / 2;
1092 assert!((result.as_f64() - 0.25).abs() < 0.0001);
1093 }
1094
1095 #[test]
1096 fn test_neg() {
1097 let r = Rational::new(1, 2);
1098 let neg = -r;
1099 assert_eq!(neg.num(), -1);
1100 assert_eq!(neg.den(), 2);
1101 }
1102
1103 #[test]
1104 fn test_ord() {
1105 let a = Rational::new(1, 2);
1106 let b = Rational::new(1, 3);
1107 let c = Rational::new(2, 4);
1108
1109 assert!(a > b);
1110 assert!(b < a);
1111 assert_eq!(a, c);
1112 assert!(a >= c);
1113 assert!(a <= c);
1114 }
1115
1116 #[test]
1117 fn test_from_i32() {
1118 let r: Rational = 5.into();
1119 assert_eq!(r.num(), 5);
1120 assert_eq!(r.den(), 1);
1121 }
1122
1123 #[test]
1124 fn test_from_tuple() {
1125 let r: Rational = (3, 4).into();
1126 assert_eq!(r.num(), 3);
1127 assert_eq!(r.den(), 4);
1128 }
1129
1130 #[test]
1131 fn test_display() {
1132 assert_eq!(format!("{}", Rational::new(1, 2)), "1/2");
1133 assert_eq!(format!("{}", Rational::new(-3, 4)), "-3/4");
1134 }
1135
1136 #[test]
1137 fn test_default() {
1138 assert_eq!(Rational::default(), Rational::one());
1139 }
1140
1141 #[test]
1142 fn test_common_frame_rates() {
1143 // 23.976 fps (film)
1144 let fps = Rational::new(24000, 1001);
1145 assert!((fps.as_f64() - 23.976).abs() < 0.001);
1146
1147 // 29.97 fps (NTSC)
1148 let fps = Rational::new(30000, 1001);
1149 assert!((fps.as_f64() - 29.97).abs() < 0.01);
1150
1151 // 59.94 fps (NTSC interlaced as progressive)
1152 let fps = Rational::new(60000, 1001);
1153 assert!((fps.as_f64() - 59.94).abs() < 0.01);
1154 }
1155 }
1156
1157 // ==================== Timestamp Tests ====================
1158
1159 mod timestamp_tests {
1160 use super::*;
1161
1162 fn time_base_90k() -> Rational {
1163 Rational::new(1, 90000)
1164 }
1165
1166 fn time_base_1k() -> Rational {
1167 Rational::new(1, 1000)
1168 }
1169
1170 #[test]
1171 fn test_new() {
1172 let ts = Timestamp::new(90000, time_base_90k());
1173 assert_eq!(ts.pts(), 90000);
1174 assert_eq!(ts.time_base(), time_base_90k());
1175 }
1176
1177 #[test]
1178 fn test_zero() {
1179 let ts = Timestamp::zero(time_base_90k());
1180 assert_eq!(ts.pts(), 0);
1181 assert!(ts.is_zero());
1182 assert!(approx_eq(ts.as_secs_f64(), 0.0));
1183 }
1184
1185 #[test]
1186 fn test_from_duration() {
1187 let ts = Timestamp::from_duration(Duration::from_secs(1), time_base_90k());
1188 assert_eq!(ts.pts(), 90000);
1189
1190 let ts = Timestamp::from_duration(Duration::from_millis(500), time_base_90k());
1191 assert_eq!(ts.pts(), 45000);
1192 }
1193
1194 #[test]
1195 fn test_from_secs_f64() {
1196 let ts = Timestamp::from_secs_f64(1.5, time_base_1k());
1197 assert_eq!(ts.pts(), 1500);
1198 }
1199
1200 #[test]
1201 fn test_from_millis() {
1202 let ts = Timestamp::from_millis(1000, time_base_90k());
1203 assert_eq!(ts.pts(), 90000);
1204
1205 let ts = Timestamp::from_millis(500, time_base_1k());
1206 assert_eq!(ts.pts(), 500);
1207 }
1208
1209 #[test]
1210 fn test_as_duration() {
1211 let ts = Timestamp::new(90000, time_base_90k());
1212 let duration = ts.as_duration();
1213 assert_eq!(duration, Duration::from_secs(1));
1214
1215 // Negative timestamp clamps to zero
1216 let ts = Timestamp::new(-100, time_base_90k());
1217 assert_eq!(ts.as_duration(), Duration::ZERO);
1218 }
1219
1220 #[test]
1221 fn test_as_secs_f64() {
1222 let ts = Timestamp::new(45000, time_base_90k());
1223 assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
1224 }
1225
1226 #[test]
1227 fn test_as_millis() {
1228 let ts = Timestamp::new(90000, time_base_90k());
1229 assert_eq!(ts.as_millis(), 1000);
1230
1231 let ts = Timestamp::new(45000, time_base_90k());
1232 assert_eq!(ts.as_millis(), 500);
1233 }
1234
1235 #[test]
1236 fn test_as_micros() {
1237 let ts = Timestamp::new(90, time_base_90k());
1238 assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 us
1239 }
1240
1241 #[test]
1242 fn test_as_frame_number() {
1243 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1244 assert_eq!(ts.as_frame_number(30.0), 30);
1245 assert_eq!(ts.as_frame_number(60.0), 60);
1246 assert_eq!(ts.as_frame_number(24.0), 24);
1247
1248 // Negative timestamp
1249 let ts = Timestamp::new(-90000, time_base_90k());
1250 assert_eq!(ts.as_frame_number(30.0), 0);
1251 }
1252
1253 #[test]
1254 fn test_as_frame_number_rational() {
1255 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1256 let fps = Rational::new(30, 1);
1257 assert_eq!(ts.as_frame_number_rational(fps), 30);
1258 }
1259
1260 #[test]
1261 fn test_rescale() {
1262 let ts = Timestamp::new(1000, time_base_1k()); // 1 second
1263 let rescaled = ts.rescale(time_base_90k());
1264 assert_eq!(rescaled.pts(), 90000);
1265 }
1266
1267 #[test]
1268 fn test_is_zero() {
1269 assert!(Timestamp::zero(time_base_90k()).is_zero());
1270 assert!(!Timestamp::new(1, time_base_90k()).is_zero());
1271 }
1272
1273 #[test]
1274 fn test_is_negative() {
1275 assert!(Timestamp::new(-100, time_base_90k()).is_negative());
1276 assert!(!Timestamp::new(100, time_base_90k()).is_negative());
1277 assert!(!Timestamp::new(0, time_base_90k()).is_negative());
1278 }
1279
1280 #[test]
1281 fn test_display() {
1282 // 1 hour, 2 minutes, 3.456 seconds
1283 let secs = 3600.0 + 120.0 + 3.456;
1284 let ts = Timestamp::from_secs_f64(secs, time_base_90k());
1285 let display = format!("{ts}");
1286 assert!(display.starts_with("01:02:03"));
1287 }
1288
1289 #[test]
1290 fn test_eq() {
1291 let ts1 = Timestamp::new(90000, time_base_90k());
1292 let ts2 = Timestamp::new(1000, time_base_1k());
1293 assert_eq!(ts1, ts2); // Both are 1 second
1294 }
1295
1296 #[test]
1297 fn test_ord() {
1298 let ts1 = Timestamp::new(45000, time_base_90k()); // 0.5 sec
1299 let ts2 = Timestamp::new(90000, time_base_90k()); // 1.0 sec
1300 assert!(ts1 < ts2);
1301 assert!(ts2 > ts1);
1302 }
1303
1304 #[test]
1305 fn test_add() {
1306 let ts1 = Timestamp::new(45000, time_base_90k());
1307 let ts2 = Timestamp::new(45000, time_base_90k());
1308 let sum = ts1 + ts2;
1309 assert_eq!(sum.pts(), 90000);
1310 }
1311
1312 #[test]
1313 fn test_sub() {
1314 let ts1 = Timestamp::new(90000, time_base_90k());
1315 let ts2 = Timestamp::new(45000, time_base_90k());
1316 let diff = ts1 - ts2;
1317 assert_eq!(diff.pts(), 45000);
1318 }
1319
1320 #[test]
1321 fn test_add_duration() {
1322 let ts = Timestamp::new(45000, time_base_90k());
1323 let result = ts + Duration::from_millis(500);
1324 assert_eq!(result.pts(), 90000);
1325 }
1326
1327 #[test]
1328 fn test_sub_duration() {
1329 let ts = Timestamp::new(90000, time_base_90k());
1330 let result = ts - Duration::from_millis(500);
1331 assert_eq!(result.pts(), 45000);
1332 }
1333
1334 #[test]
1335 fn test_default() {
1336 let ts = Timestamp::default();
1337 assert_eq!(ts.pts(), 0);
1338 assert_eq!(ts.time_base(), Rational::new(1, 90000));
1339 }
1340
1341 #[test]
1342 fn test_video_timestamps() {
1343 // Common video time base: 1/90000 (MPEG-TS)
1344 let time_base = Rational::new(1, 90000);
1345
1346 // At 30 fps, each frame is 3000 PTS units
1347 let frame_duration_pts = 90000 / 30;
1348 assert_eq!(frame_duration_pts, 3000);
1349
1350 // Frame 0
1351 let frame0 = Timestamp::new(0, time_base);
1352 assert_eq!(frame0.as_frame_number(30.0), 0);
1353
1354 // Frame 30 (1 second)
1355 let frame30 = Timestamp::new(90000, time_base);
1356 assert_eq!(frame30.as_frame_number(30.0), 30);
1357 }
1358
1359 #[test]
1360 fn test_audio_timestamps() {
1361 // Audio at 48kHz - each sample is 1/48000 seconds
1362 let time_base = Rational::new(1, 48000);
1363
1364 // 1024 samples (common audio frame size)
1365 let ts = Timestamp::new(1024, time_base);
1366 let ms = ts.as_secs_f64() * 1000.0;
1367 assert!((ms - 21.333).abs() < 0.01); // ~21.33 ms
1368 }
1369 }
1370
1371 // ==================== GCD Tests ====================
1372
1373 #[test]
1374 fn test_gcd() {
1375 assert_eq!(gcd(12, 8), 4);
1376 assert_eq!(gcd(17, 13), 1);
1377 assert_eq!(gcd(100, 25), 25);
1378 assert_eq!(gcd(0, 5), 5);
1379 assert_eq!(gcd(5, 0), 5);
1380 }
1381}