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}