Skip to main content

use_media_duration/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive media duration helpers.
3//!
4//! These helpers keep durations explicit without requiring a broader time
5//! framework.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_media_duration::{MediaDuration, duration_from_samples, format_duration_hms};
11//!
12//! let duration = MediaDuration::new(90.5).unwrap();
13//!
14//! assert_eq!(duration.millis(), 90_500);
15//! assert_eq!(format_duration_hms(3661.0).unwrap(), "01:01:01");
16//! assert!((duration_from_samples(96_000, 48_000.0).unwrap() - 2.0).abs() < 1.0e-12);
17//! ```
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct MediaDuration {
21    seconds: f64,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MediaDurationError {
26    InvalidSeconds,
27    InvalidSampleRate,
28}
29
30fn validate_seconds(seconds: f64) -> Result<f64, MediaDurationError> {
31    if !seconds.is_finite() || seconds < 0.0 {
32        Err(MediaDurationError::InvalidSeconds)
33    } else {
34        Ok(seconds)
35    }
36}
37
38fn validate_sample_rate(sample_rate_hz: f64) -> Result<f64, MediaDurationError> {
39    if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 {
40        Err(MediaDurationError::InvalidSampleRate)
41    } else {
42        Ok(sample_rate_hz)
43    }
44}
45
46impl MediaDuration {
47    pub fn new(seconds: f64) -> Result<Self, MediaDurationError> {
48        Ok(Self {
49            seconds: validate_seconds(seconds)?,
50        })
51    }
52
53    #[must_use]
54    pub fn seconds(&self) -> f64 {
55        self.seconds
56    }
57
58    #[must_use]
59    pub fn millis(&self) -> u64 {
60        (self.seconds * 1_000.0).round() as u64
61    }
62
63    #[must_use]
64    pub fn minutes(&self) -> f64 {
65        self.seconds / 60.0
66    }
67
68    #[must_use]
69    pub fn hours(&self) -> f64 {
70        self.seconds / 3_600.0
71    }
72}
73
74pub fn seconds_to_millis(seconds: f64) -> Result<u64, MediaDurationError> {
75    Ok((validate_seconds(seconds)? * 1_000.0).round() as u64)
76}
77
78#[must_use]
79pub fn millis_to_seconds(millis: u64) -> f64 {
80    millis as f64 / 1_000.0
81}
82
83pub fn format_duration_hms(seconds: f64) -> Result<String, MediaDurationError> {
84    let total_seconds = validate_seconds(seconds)?.floor() as u64;
85    let hours = total_seconds / 3_600;
86    let minutes = (total_seconds % 3_600) / 60;
87    let seconds = total_seconds % 60;
88
89    Ok(format!("{hours:02}:{minutes:02}:{seconds:02}"))
90}
91
92pub fn duration_from_samples(
93    sample_count: usize,
94    sample_rate_hz: f64,
95) -> Result<f64, MediaDurationError> {
96    Ok(sample_count as f64 / validate_sample_rate(sample_rate_hz)?)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::{
102        MediaDuration, MediaDurationError, duration_from_samples, format_duration_hms,
103        millis_to_seconds, seconds_to_millis,
104    };
105
106    #[test]
107    fn computes_duration_helpers() {
108        let duration = MediaDuration::new(90.5).unwrap();
109
110        assert_eq!(duration.seconds(), 90.5);
111        assert_eq!(duration.millis(), 90_500);
112        assert!((duration.minutes() - 1.508_333_333_333_333_3).abs() < 1.0e-12);
113        assert!((duration.hours() - 0.025_138_888_888_888_89).abs() < 1.0e-12);
114        assert_eq!(seconds_to_millis(1.5).unwrap(), 1_500);
115        assert_eq!(millis_to_seconds(1_500), 1.5);
116    }
117
118    #[test]
119    fn formats_duration_and_samples() {
120        assert_eq!(format_duration_hms(3661.0).unwrap(), "01:01:01");
121        assert!((duration_from_samples(96_000, 48_000.0).unwrap() - 2.0).abs() < 1.0e-12);
122        assert_eq!(duration_from_samples(0, 48_000.0).unwrap(), 0.0);
123    }
124
125    #[test]
126    fn rejects_invalid_duration_inputs() {
127        assert_eq!(
128            MediaDuration::new(-1.0),
129            Err(MediaDurationError::InvalidSeconds)
130        );
131        assert_eq!(
132            seconds_to_millis(f64::NAN),
133            Err(MediaDurationError::InvalidSeconds)
134        );
135        assert_eq!(
136            format_duration_hms(-0.1),
137            Err(MediaDurationError::InvalidSeconds)
138        );
139        assert_eq!(
140            duration_from_samples(10, 0.0),
141            Err(MediaDurationError::InvalidSampleRate)
142        );
143    }
144}