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 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 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 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 let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps24);
306 assert_eq!(tc.to_string(), "00:00:00:12");
307 }
308}