1use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10#[allow(dead_code)]
11pub struct TimecodeValue {
12 pub hh: u8,
14 pub mm: u8,
16 pub ss: u8,
18 pub ff: u8,
20 pub fps: f32,
22 pub drop_frame: bool,
24}
25
26impl TimecodeValue {
27 #[must_use]
29 pub fn new(hh: u8, mm: u8, ss: u8, ff: u8, fps: f32, drop_frame: bool) -> Self {
30 Self {
31 hh,
32 mm,
33 ss,
34 ff,
35 fps,
36 drop_frame,
37 }
38 }
39
40 #[must_use]
42 fn fps_int(&self) -> u64 {
43 self.fps.ceil() as u64
44 }
45
46 #[must_use]
48 fn frames_per_day(&self) -> u64 {
49 self.fps_int() * 3600 * 24
50 }
51
52 #[must_use]
56 pub fn to_frame_count(&self) -> u64 {
57 let fps = self.fps_int();
58 let hh = u64::from(self.hh);
59 let mm = u64::from(self.mm);
60 let ss = u64::from(self.ss);
61 let ff = u64::from(self.ff);
62
63 let raw = hh * 3600 * fps + mm * 60 * fps + ss * fps + ff;
64
65 if self.drop_frame {
66 let total_minutes = hh * 60 + mm;
67 let dropped = 2 * (total_minutes - total_minutes / 10);
68 raw - dropped
69 } else {
70 raw
71 }
72 }
73
74 #[must_use]
76 pub fn from_frame_count(frames: u64, fps: f32, drop_frame: bool) -> Self {
77 let fps_int = fps.ceil() as u64;
78 let mut remaining = frames;
79
80 if drop_frame {
82 let frames_per_min = fps_int * 60 - 2;
83 let frames_per_10_min = frames_per_min * 9 + fps_int * 60;
84
85 let ten_min_blocks = remaining / frames_per_10_min;
86 remaining += ten_min_blocks * 18;
87
88 let remaining_in_block = remaining % frames_per_10_min;
89 if remaining_in_block >= fps_int * 60 {
90 let extra_minutes = (remaining_in_block - fps_int * 60) / frames_per_min;
91 remaining += (extra_minutes + 1) * 2;
92 }
93 }
94
95 let hh = ((remaining / (fps_int * 3600)) % 24) as u8;
96 remaining %= fps_int * 3600;
97 let mm = (remaining / (fps_int * 60)) as u8;
98 remaining %= fps_int * 60;
99 let ss = (remaining / fps_int) as u8;
100 let ff = (remaining % fps_int) as u8;
101
102 Self::new(hh, mm, ss, ff, fps, drop_frame)
103 }
104
105 #[must_use]
107 pub fn add_frames(&self, frames: i64) -> Self {
108 let total = self.to_frame_count() as i64;
109 let frames_per_day = self.frames_per_day() as i64;
110
111 let new_total = ((total + frames) % frames_per_day + frames_per_day) % frames_per_day;
113
114 Self::from_frame_count(new_total as u64, self.fps, self.drop_frame)
115 }
116
117 #[must_use]
121 pub fn subtract(&self, other: &Self) -> i64 {
122 self.to_frame_count() as i64 - other.to_frame_count() as i64
123 }
124
125 #[must_use]
129 pub fn to_string_formatted(&self) -> String {
130 let sep = if self.drop_frame { ';' } else { ':' };
131 format!(
132 "{:02}:{:02}:{:02}{}{:02}",
133 self.hh, self.mm, self.ss, sep, self.ff
134 )
135 }
136
137 #[must_use]
142 pub fn parse(s: &str, fps: f32) -> Option<Self> {
143 let drop_frame = s.contains(';');
145
146 let normalized = s.replace(';', ":");
148 let parts: Vec<&str> = normalized.split(':').collect();
149
150 if parts.len() != 4 {
151 return None;
152 }
153
154 let hh: u8 = parts[0].parse().ok()?;
155 let mm: u8 = parts[1].parse().ok()?;
156 let ss: u8 = parts[2].parse().ok()?;
157 let ff: u8 = parts[3].parse().ok()?;
158
159 if hh > 23 || mm > 59 || ss > 59 {
160 return None;
161 }
162
163 Some(Self::new(hh, mm, ss, ff, fps, drop_frame))
164 }
165}
166
167impl fmt::Display for TimecodeValue {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 write!(f, "{}", self.to_string_formatted())
170 }
171}
172
173#[allow(dead_code)]
175pub struct Duration;
176
177impl Duration {
178 #[must_use]
182 pub fn from_timecode(tc: &TimecodeValue) -> f64 {
183 let frame_count = tc.to_frame_count();
184 f64::from(frame_count as u32) / f64::from(tc.fps)
185 }
186
187 #[must_use]
189 pub fn to_timecode(seconds: f64, fps: f32, drop_frame: bool) -> TimecodeValue {
190 let total_frames = (seconds * f64::from(fps)).round() as u64;
191 TimecodeValue::from_frame_count(total_frames, fps, drop_frame)
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_timecode_value_new() {
201 let tc = TimecodeValue::new(1, 2, 3, 4, 25.0, false);
202 assert_eq!(tc.hh, 1);
203 assert_eq!(tc.mm, 2);
204 assert_eq!(tc.ss, 3);
205 assert_eq!(tc.ff, 4);
206 assert!((tc.fps - 25.0).abs() < f32::EPSILON);
207 assert!(!tc.drop_frame);
208 }
209
210 #[test]
211 fn test_to_frame_count_ndf() {
212 let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
213 assert_eq!(tc.to_frame_count(), 25);
214 }
215
216 #[test]
217 fn test_to_frame_count_one_hour() {
218 let tc = TimecodeValue::new(1, 0, 0, 0, 30.0, false);
219 assert_eq!(tc.to_frame_count(), 3600 * 30);
220 }
221
222 #[test]
223 fn test_from_frame_count_ndf() {
224 let tc = TimecodeValue::from_frame_count(25, 25.0, false);
225 assert_eq!(tc.hh, 0);
226 assert_eq!(tc.mm, 0);
227 assert_eq!(tc.ss, 1);
228 assert_eq!(tc.ff, 0);
229 }
230
231 #[test]
232 fn test_frame_count_roundtrip_ndf() {
233 let original = TimecodeValue::new(1, 30, 45, 12, 25.0, false);
234 let frames = original.to_frame_count();
235 let recovered = TimecodeValue::from_frame_count(frames, 25.0, false);
236 assert_eq!(original, recovered);
237 }
238
239 #[test]
240 fn test_add_frames_forward() {
241 let tc = TimecodeValue::new(0, 0, 0, 0, 25.0, false);
242 let tc2 = tc.add_frames(25);
243 assert_eq!(tc2.ss, 1);
244 assert_eq!(tc2.ff, 0);
245 }
246
247 #[test]
248 fn test_add_frames_backward() {
249 let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
250 let tc2 = tc.add_frames(-25);
251 assert_eq!(tc2.hh, 0);
252 assert_eq!(tc2.mm, 0);
253 assert_eq!(tc2.ss, 0);
254 assert_eq!(tc2.ff, 0);
255 }
256
257 #[test]
258 fn test_add_frames_wrap_at_24h() {
259 let tc = TimecodeValue::new(23, 59, 59, 24, 25.0, false);
261 let tc2 = tc.add_frames(1); assert_eq!(tc2.hh, 0);
263 assert_eq!(tc2.mm, 0);
264 assert_eq!(tc2.ss, 0);
265 assert_eq!(tc2.ff, 0);
266 }
267
268 #[test]
269 fn test_subtract() {
270 let tc1 = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
271 let tc2 = TimecodeValue::new(0, 0, 0, 0, 25.0, false);
272 assert_eq!(tc1.subtract(&tc2), 25);
273 assert_eq!(tc2.subtract(&tc1), -25);
274 }
275
276 #[test]
277 fn test_display_ndf() {
278 let tc = TimecodeValue::new(1, 2, 3, 4, 25.0, false);
279 assert_eq!(tc.to_string(), "01:02:03:04");
280 }
281
282 #[test]
283 fn test_display_df() {
284 let tc = TimecodeValue::new(1, 2, 3, 4, 29.97, true);
285 assert_eq!(tc.to_string(), "01:02:03;04");
286 }
287
288 #[test]
289 fn test_parse_ndf() {
290 let tc = TimecodeValue::parse("01:02:03:04", 25.0).expect("valid timecode value");
291 assert_eq!(tc.hh, 1);
292 assert_eq!(tc.mm, 2);
293 assert_eq!(tc.ss, 3);
294 assert_eq!(tc.ff, 4);
295 assert!(!tc.drop_frame);
296 }
297
298 #[test]
299 fn test_parse_df() {
300 let tc = TimecodeValue::parse("01:02:03;04", 29.97).expect("valid timecode value");
301 assert_eq!(tc.hh, 1);
302 assert_eq!(tc.mm, 2);
303 assert_eq!(tc.ss, 3);
304 assert_eq!(tc.ff, 4);
305 assert!(tc.drop_frame);
306 }
307
308 #[test]
309 fn test_parse_invalid() {
310 assert!(TimecodeValue::parse("invalid", 25.0).is_none());
311 assert!(TimecodeValue::parse("25:00:00:00", 25.0).is_none()); assert!(TimecodeValue::parse("", 25.0).is_none());
313 }
314
315 #[test]
316 fn test_duration_from_timecode() {
317 let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
318 let secs = Duration::from_timecode(&tc);
319 assert!((secs - 1.0).abs() < 0.01);
320 }
321
322 #[test]
323 fn test_duration_to_timecode() {
324 let tc = Duration::to_timecode(1.0, 25.0, false);
325 assert_eq!(tc.ss, 1);
326 assert_eq!(tc.ff, 0);
327 }
328
329 #[test]
330 fn test_parse_display_roundtrip() {
331 let original = "01:30:45:12";
332 let tc = TimecodeValue::parse(original, 25.0).expect("valid timecode value");
333 assert_eq!(tc.to_string(), original);
334 }
335}