Skip to main content

talw_timecode/
timecode.rs

1use core::fmt;
2use core::ops::{Add, Sub};
3
4use crate::convert::convert_frames;
5use crate::dropframe::{components_to_frames, frames_to_components};
6use crate::error::TimecodeError;
7use crate::format::format_timecode;
8use crate::framerate::{FrameRate, Rational};
9use crate::parse::parse_timecode;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct Timecode {
13    total_frames: i64,
14    rate: FrameRate,
15}
16
17impl Timecode {
18    pub fn new(h: u8, m: u8, s: u8, f: u8, rate: FrameRate) -> Result<Self, TimecodeError> {
19        let max_frames = rate.nominal() as u8;
20        if h > 23 {
21            return Err(TimecodeError::InvalidHours(h));
22        }
23        if m > 59 {
24            return Err(TimecodeError::InvalidMinutes(m));
25        }
26        if s > 59 {
27            return Err(TimecodeError::InvalidSeconds(s));
28        }
29        if f >= max_frames {
30            return Err(TimecodeError::InvalidFrames(f, max_frames));
31        }
32
33        Ok(Self {
34            total_frames: components_to_frames(h, m, s, f, rate),
35            rate,
36        })
37    }
38
39    pub const fn from_frames(total_frames: i64, rate: FrameRate) -> Self {
40        Self { total_frames, rate }
41    }
42
43    pub fn from_seconds(seconds: f64, rate: FrameRate) -> Self {
44        let r = rate.rational();
45        let frames = (seconds * r.num as f64 / r.den as f64).round() as i64;
46        Self {
47            total_frames: frames,
48            rate,
49        }
50    }
51
52    pub fn from_milliseconds(ms: f64, rate: FrameRate) -> Self {
53        Self::from_seconds(ms / 1000.0, rate)
54    }
55
56    pub fn parse(s: &str, rate: FrameRate) -> Result<Self, TimecodeError> {
57        let (h, m, s_val, f) = parse_timecode(s, rate)?;
58        Ok(Self {
59            total_frames: components_to_frames(h, m, s_val, f, rate),
60            rate,
61        })
62    }
63
64    pub fn validate(s: &str, rate: FrameRate) -> bool {
65        parse_timecode(s, rate).is_ok()
66    }
67
68    pub fn hours(&self) -> u8 {
69        frames_to_components(self.total_frames, self.rate).0
70    }
71
72    pub fn minutes(&self) -> u8 {
73        frames_to_components(self.total_frames, self.rate).1
74    }
75
76    pub fn seconds(&self) -> u8 {
77        frames_to_components(self.total_frames, self.rate).2
78    }
79
80    pub fn frames(&self) -> u8 {
81        frames_to_components(self.total_frames, self.rate).3
82    }
83
84    pub fn components(&self) -> (u8, u8, u8, u8) {
85        frames_to_components(self.total_frames, self.rate)
86    }
87
88    pub const fn total_frames(&self) -> i64 {
89        self.total_frames
90    }
91
92    pub const fn rate(&self) -> FrameRate {
93        self.rate
94    }
95
96    pub fn to_seconds(&self) -> f64 {
97        let r = self.rate.rational();
98        self.total_frames as f64 * r.den as f64 / r.num as f64
99    }
100
101    pub fn to_milliseconds(&self) -> f64 {
102        self.to_seconds() * 1000.0
103    }
104
105    pub fn to_rational(&self) -> (i64, Rational) {
106        (self.total_frames, self.rate.rational())
107    }
108
109    pub fn convert_to(&self, target_rate: FrameRate) -> Self {
110        Self {
111            total_frames: convert_frames(self.total_frames, self.rate, target_rate),
112            rate: target_rate,
113        }
114    }
115
116    pub fn frame_diff(&self, other: &Self) -> Result<i64, TimecodeError> {
117        if self.rate != other.rate {
118            return Err(TimecodeError::MismatchedRates);
119        }
120        Ok(self.total_frames - other.total_frames)
121    }
122}
123
124impl fmt::Display for Timecode {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        let bytes = format_timecode(self.total_frames, self.rate);
127        let s = core::str::from_utf8(&bytes).unwrap();
128        f.write_str(s)
129    }
130}
131
132impl PartialOrd for Timecode {
133    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
134        if self.rate != other.rate {
135            return None;
136        }
137        Some(self.total_frames.cmp(&other.total_frames))
138    }
139}
140
141impl Add<i64> for Timecode {
142    type Output = Self;
143
144    fn add(self, frames: i64) -> Self {
145        Self {
146            total_frames: self.total_frames + frames,
147            rate: self.rate,
148        }
149    }
150}
151
152impl Sub<i64> for Timecode {
153    type Output = Self;
154
155    fn sub(self, frames: i64) -> Self {
156        Self {
157            total_frames: self.total_frames - frames,
158            rate: self.rate,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn new_and_display() {
169        let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps24).unwrap();
170        assert_eq!(tc.to_string(), "01:23:45:12");
171    }
172
173    #[test]
174    fn from_frames() {
175        let tc = Timecode::from_frames(86400, FrameRate::Fps24);
176        assert_eq!(tc.to_string(), "01:00:00:00");
177    }
178
179    #[test]
180    fn from_seconds() {
181        let tc = Timecode::from_seconds(0.5, FrameRate::Fps24);
182        assert_eq!(tc.to_string(), "00:00:00:12");
183    }
184
185    #[test]
186    fn from_milliseconds() {
187        let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps24);
188        assert_eq!(tc.to_string(), "00:00:00:12");
189    }
190
191    #[test]
192    fn parse_roundtrip() {
193        let tc = Timecode::parse("01:23:45:12", FrameRate::Fps24).unwrap();
194        assert_eq!(tc.to_string(), "01:23:45:12");
195    }
196
197    #[test]
198    fn to_seconds() {
199        let tc = Timecode::from_frames(24, FrameRate::Fps24);
200        assert!((tc.to_seconds() - 1.0).abs() < 0.001);
201    }
202
203    #[test]
204    fn to_milliseconds() {
205        let tc = Timecode::from_frames(12, FrameRate::Fps24);
206        assert!((tc.to_milliseconds() - 500.0).abs() < 1.0);
207    }
208
209    #[test]
210    fn add_frames() {
211        let tc = Timecode::from_frames(0, FrameRate::Fps24);
212        let tc2 = tc + 48;
213        assert_eq!(tc2.to_string(), "00:00:02:00");
214    }
215
216    #[test]
217    fn sub_frames() {
218        let tc = Timecode::from_frames(48, FrameRate::Fps24);
219        let tc2 = tc - 24;
220        assert_eq!(tc2.to_string(), "00:00:01:00");
221    }
222
223    #[test]
224    fn frame_diff() {
225        let a = Timecode::from_frames(100, FrameRate::Fps24);
226        let b = Timecode::from_frames(50, FrameRate::Fps24);
227        assert_eq!(a.frame_diff(&b).unwrap(), 50);
228    }
229
230    #[test]
231    fn frame_diff_mismatched_rates() {
232        let a = Timecode::from_frames(100, FrameRate::Fps24);
233        let b = Timecode::from_frames(100, FrameRate::Fps30);
234        assert!(a.frame_diff(&b).is_err());
235    }
236
237    #[test]
238    fn convert_24_to_30() {
239        let tc = Timecode::from_frames(24, FrameRate::Fps24);
240        let converted = tc.convert_to(FrameRate::Fps30);
241        assert_eq!(converted.total_frames(), 30);
242    }
243
244    #[test]
245    fn ordering() {
246        let a = Timecode::from_frames(10, FrameRate::Fps24);
247        let b = Timecode::from_frames(20, FrameRate::Fps24);
248        assert!(a < b);
249    }
250
251    #[test]
252    fn ordering_different_rates_is_none() {
253        let a = Timecode::from_frames(10, FrameRate::Fps24);
254        let b = Timecode::from_frames(10, FrameRate::Fps30);
255        assert!(a.partial_cmp(&b).is_none());
256    }
257
258    #[test]
259    fn drop_frame_display() {
260        let tc = Timecode::from_frames(1800, FrameRate::Fps29_97Df);
261        assert_eq!(tc.to_string(), "00:01:00;02");
262    }
263
264    #[test]
265    fn components() {
266        let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps24).unwrap();
267        assert_eq!(tc.hours(), 1);
268        assert_eq!(tc.minutes(), 23);
269        assert_eq!(tc.seconds(), 45);
270        assert_eq!(tc.frames(), 12);
271    }
272
273    #[test]
274    fn ms_roundtrip_matches_python() {
275        // Port of Python test: 5025000.0 ms at 24fps
276        let tc = Timecode::from_milliseconds(5025000.0, FrameRate::Fps24);
277        let back = tc.to_milliseconds();
278        assert!((back - 5025000.0).abs() < 50.0);
279    }
280
281    #[test]
282    fn validate() {
283        assert!(Timecode::validate("01:23:45:12", FrameRate::Fps24));
284        assert!(!Timecode::validate("01:23:45", FrameRate::Fps24));
285        assert!(!Timecode::validate("25:00:00:00", FrameRate::Fps24));
286    }
287
288    #[test]
289    fn rust_nexus_ms_to_smpte_basic() {
290        // Port of Nexus dvr.rs test: 3723000ms -> "01:02:03:00" at 30fps
291        let tc = Timecode::from_milliseconds(3723000.0, FrameRate::Fps30);
292        assert_eq!(tc.to_string(), "01:02:03:00");
293    }
294
295    #[test]
296    fn rust_nexus_ms_to_smpte_with_frames() {
297        // 500ms at 30fps -> 15 frames
298        let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps30);
299        assert_eq!(tc.to_string(), "00:00:00:15");
300    }
301
302    #[test]
303    fn rust_nexus_ms_to_smpte_24fps() {
304        // 500ms at 24fps -> 12 frames
305        let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps24);
306        assert_eq!(tc.to_string(), "00:00:00:12");
307    }
308}