1use std::fmt;
10
11#[allow(dead_code)]
17pub struct DropFrameCalc;
18
19impl DropFrameCalc {
20 const FRAMES_PER_SEC: u64 = 30;
22 const DROP_PER_MIN: u64 = 2;
23 const FRAMES_PER_DROP_MIN: u64 = Self::FRAMES_PER_SEC * 60 - Self::DROP_PER_MIN; const FRAMES_PER_10_MIN: u64 = Self::FRAMES_PER_DROP_MIN * 9 + Self::FRAMES_PER_SEC * 60; const FRAMES_PER_HOUR: u64 = Self::FRAMES_PER_10_MIN * 6; #[must_use]
39 pub fn frame_count_to_df(frame_count: u64) -> (u8, u8, u8, u8) {
40 let d = frame_count % (Self::FRAMES_PER_HOUR * 24);
42
43 let d_ten = d / Self::FRAMES_PER_10_MIN;
45 let d_in_ten = d % Self::FRAMES_PER_10_MIN;
46
47 let (min_in_ten, d_in_min) = if d_in_ten < Self::FRAMES_PER_SEC * 60 {
51 (0u64, d_in_ten)
52 } else {
53 let d_after_first = d_in_ten - Self::FRAMES_PER_SEC * 60;
54 let extra_min = d_after_first / Self::FRAMES_PER_DROP_MIN;
55 let d_in_drop_min = d_after_first % Self::FRAMES_PER_DROP_MIN;
56 (extra_min + 1, d_in_drop_min)
57 };
58
59 let total_minutes = d_ten * 10 + min_in_ten;
60 let hh = (total_minutes / 60) as u8;
61 let mm = (total_minutes % 60) as u8;
62
63 let (ss, ff) = if min_in_ten > 0 {
67 let adjusted = d_in_min + Self::DROP_PER_MIN;
70 let ss = adjusted / Self::FRAMES_PER_SEC;
71 let ff = adjusted % Self::FRAMES_PER_SEC;
72 (ss as u8, ff as u8)
73 } else {
74 let ss = d_in_min / Self::FRAMES_PER_SEC;
76 let ff = d_in_min % Self::FRAMES_PER_SEC;
77 (ss as u8, ff as u8)
78 };
79
80 (hh, mm, ss, ff)
81 }
82
83 #[must_use]
91 pub fn df_to_frame_count(hh: u8, mm: u8, ss: u8, ff: u8) -> u64 {
92 let hh = u64::from(hh);
93 let mm = u64::from(mm);
94 let ss = u64::from(ss);
95 let ff = u64::from(ff);
96
97 let total_minutes = hh * 60 + mm;
98
99 let raw = hh * 108000 + mm * 1800 + ss * 30 + ff;
101
102 let dropped = Self::DROP_PER_MIN * (total_minutes - total_minutes / 10);
104
105 raw - dropped
106 }
107
108 #[must_use]
112 pub fn format_df(frame_count: u64) -> String {
113 let (hh, mm, ss, ff) = Self::frame_count_to_df(frame_count);
114 format!("{hh:02};{mm:02};{ss:02};{ff:02}")
115 }
116
117 #[must_use]
121 pub fn parse_df(tc: &str) -> Option<u64> {
122 let parts: Vec<&str> = tc.split(';').collect();
123 if parts.len() != 4 {
124 return None;
125 }
126
127 let hh: u8 = parts[0].parse().ok()?;
128 let mm: u8 = parts[1].parse().ok()?;
129 let ss: u8 = parts[2].parse().ok()?;
130 let ff: u8 = parts[3].parse().ok()?;
131
132 if hh > 23 || mm > 59 || ss > 59 || ff > 29 {
133 return None;
134 }
135
136 Some(Self::df_to_frame_count(hh, mm, ss, ff))
137 }
138
139 #[must_use]
143 pub fn is_dropped_frame(hh: u8, mm: u8, ss: u8, ff: u8) -> bool {
144 let _ = hh; ss == 0 && ff < 2 && !mm.is_multiple_of(10)
146 }
147}
148
149#[derive(Debug, Clone)]
151#[allow(dead_code)]
152pub struct TotalFrameCounter {
153 frame_count: u64,
155 drop_frame: bool,
157 fps: u8,
159}
160
161impl TotalFrameCounter {
162 #[must_use]
164 pub fn new_drop_frame() -> Self {
165 Self {
166 frame_count: 0,
167 drop_frame: true,
168 fps: 30,
169 }
170 }
171
172 #[must_use]
174 pub fn new_non_drop_frame(fps: u8) -> Self {
175 Self {
176 frame_count: 0,
177 drop_frame: false,
178 fps,
179 }
180 }
181
182 pub fn add_frames(&mut self, n: u64) {
184 self.frame_count = self.frame_count.wrapping_add(n);
185 }
186
187 #[must_use]
189 pub fn frame_count(&self) -> u64 {
190 self.frame_count
191 }
192
193 pub fn reset(&mut self) {
195 self.frame_count = 0;
196 }
197
198 #[must_use]
200 pub fn is_drop_frame(&self) -> bool {
201 self.drop_frame
202 }
203}
204
205impl fmt::Display for TotalFrameCounter {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 if self.drop_frame {
208 write!(f, "{}", DropFrameCalc::format_df(self.frame_count))
209 } else {
210 let fps = u64::from(self.fps);
212 let seconds_total = self.frame_count / fps;
213 let frames = self.frame_count % fps;
214 let seconds = seconds_total % 60;
215 let minutes_total = seconds_total / 60;
216 let minutes = minutes_total % 60;
217 let hours = (minutes_total / 60) % 24;
218 write!(f, "{hours:02}:{minutes:02}:{seconds:02}:{frames:02}")
219 }
220 }
221}
222
223fn build_df_29_97_drop_minute_lut() -> [bool; 60] {
234 let mut lut = [false; 60];
235 for (m, entry) in lut.iter_mut().enumerate() {
236 *entry = m % 10 != 0;
237 }
238 lut
239}
240
241#[cfg(test)]
244mod exact_df_tests {
245 use crate::{FrameRate, Timecode};
246
247 #[test]
249 fn test_drop_frame_from_frames_round_trip_1_million() {
250 let n: u64 = 1_000_000;
251 let tc = Timecode::from_frames(n, FrameRate::Fps2997DF).expect("from_frames must succeed");
252 let back = tc.to_frames();
253 assert_eq!(
254 back, n,
255 "round-trip failed: from_frames({n}) → {tc} → to_frames={back}"
256 );
257 }
258
259 #[test]
264 fn test_drop_frame_known_vector_29_97() {
265 let tc =
266 Timecode::from_frames(1800, FrameRate::Fps2997DF).expect("from_frames must succeed");
267 assert_eq!(tc.hours, 0);
268 assert_eq!(tc.minutes, 1);
269 assert_eq!(tc.seconds, 0);
270 assert_eq!(
271 tc.frames, 2,
272 "first frame after first-minute drop must be frame 2"
273 );
274 }
275
276 #[test]
279 fn test_drop_frame_exhaustive_10min_round_trip() {
280 const FRAMES_PER_10MIN_29_97: u64 = 17982;
282 for n in 0..FRAMES_PER_10MIN_29_97 {
283 let tc =
284 Timecode::from_frames(n, FrameRate::Fps2997DF).expect("from_frames must succeed");
285 let back = tc.to_frames();
286 assert_eq!(
287 back, n,
288 "exhaustive round-trip failed at frame {n}: got {back}"
289 );
290 }
291 }
292
293 #[test]
296 fn test_drop_frame_no_fp_at_midnight() {
297 let near_day_end: u64 = 23 * 107892 + 10000;
299 let tc = Timecode::from_frames(near_day_end, FrameRate::Fps2997DF)
300 .expect("from_frames must succeed");
301 let back = tc.to_frames();
302 assert_eq!(
303 back, near_day_end,
304 "near-midnight round-trip failed: {near_day_end} → {tc} → {back}"
305 );
306 }
307
308 #[test]
310 fn test_drop_frame_round_trip_5994_df() {
311 let n: u64 = 500_000;
312 let tc = Timecode::from_frames(n, FrameRate::Fps5994DF)
313 .expect("from_frames must succeed for 59.94 DF");
314 let back = tc.to_frames();
315 assert_eq!(back, n, "59.94 DF round-trip failed at frame {n}");
316 }
317
318 #[test]
320 fn test_drop_frame_round_trip_23976_df() {
321 let n: u64 = 200_000;
322 let tc = Timecode::from_frames(n, FrameRate::Fps23976DF)
323 .expect("from_frames must succeed for 23.976 DF");
324 let back = tc.to_frames();
325 assert_eq!(back, n, "23.976 DF round-trip failed at frame {n}");
326 }
327
328 #[test]
332 fn test_df_29_97_drop_minute_lut_correct() {
333 let lut = crate::drop_frame::build_df_29_97_drop_minute_lut();
334 assert_eq!(lut.len(), 60);
335
336 assert!(!lut[0], "minute 0 must be a keep-minute");
338 assert!(!lut[10], "minute 10 must be a keep-minute");
340 assert!(!lut[20], "minute 20 must be a keep-minute");
341 assert!(!lut[30], "minute 30 must be a keep-minute");
342 assert!(!lut[40], "minute 40 must be a keep-minute");
343 assert!(!lut[50], "minute 50 must be a keep-minute");
344
345 for m in 0..60usize {
347 if m % 10 != 0 {
348 assert!(lut[m], "minute {m} must be a drop-minute");
349 }
350 }
351 }
352
353 #[test]
357 fn test_lut_boundary_minute_1_skips_frames_0_1() {
358 let tc =
359 Timecode::from_frames(1800, FrameRate::Fps2997DF).expect("from_frames must succeed");
360 assert_eq!(tc.minutes, 1);
361 assert_eq!(tc.seconds, 0);
362 assert_eq!(
363 tc.frames, 2,
364 "boundary: first display frame in minute 1 must be 02"
365 );
366 }
367
368 #[test]
370 fn test_lut_boundary_minute_2() {
371 let tc =
374 Timecode::from_frames(3598, FrameRate::Fps2997DF).expect("from_frames must succeed");
375 assert_eq!(tc.minutes, 2);
376 assert_eq!(tc.seconds, 0);
377 assert_eq!(
378 tc.frames, 2,
379 "boundary: first display frame in minute 2 must be 02"
380 );
381 }
382
383 #[test]
385 fn test_lut_boundary_minute_10_no_drop() {
386 let tc =
389 Timecode::from_frames(17982, FrameRate::Fps2997DF).expect("from_frames must succeed");
390 assert_eq!(tc.minutes, 10);
391 assert_eq!(tc.seconds, 0);
392 assert_eq!(
393 tc.frames, 0,
394 "minute 10 is a keep-minute: display frame must be 00"
395 );
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_df_zero() {
405 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(0);
406 assert_eq!((hh, mm, ss, ff), (0, 0, 0, 0));
407 }
408
409 #[test]
410 fn test_df_one_frame() {
411 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(1);
412 assert_eq!((hh, mm, ss, ff), (0, 0, 0, 1));
413 }
414
415 #[test]
416 fn test_df_one_second() {
417 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(30);
418 assert_eq!((hh, mm, ss, ff), (0, 0, 1, 0));
419 }
420
421 #[test]
422 fn test_df_one_minute() {
423 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(1800);
426 assert_eq!(hh, 0);
427 assert_eq!(mm, 1);
428 assert_eq!(ss, 0);
429 assert_eq!(ff, 2); }
431
432 #[test]
433 fn test_df_ten_minutes() {
434 let frames = DropFrameCalc::df_to_frame_count(0, 10, 0, 0);
436 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(frames);
437 assert_eq!((hh, mm, ss, ff), (0, 10, 0, 0));
438 }
439
440 #[test]
441 fn test_df_roundtrip() {
442 let test_cases = [
443 (0u8, 0u8, 0u8, 0u8),
444 (0, 0, 0, 15),
445 (0, 0, 30, 0),
446 (0, 10, 0, 0), (1, 0, 0, 0),
448 ];
449
450 for (hh, mm, ss, ff) in test_cases {
451 let frame_count = DropFrameCalc::df_to_frame_count(hh, mm, ss, ff);
452 let (rhh, rmm, rss, rff) = DropFrameCalc::frame_count_to_df(frame_count);
453 assert_eq!(
454 (hh, mm, ss, ff),
455 (rhh, rmm, rss, rff),
456 "Roundtrip failed for {hh:02};{mm:02};{ss:02};{ff:02}"
457 );
458 }
459 }
460
461 #[test]
462 fn test_format_df() {
463 let s = DropFrameCalc::format_df(0);
464 assert_eq!(s, "00;00;00;00");
465 }
466
467 #[test]
468 fn test_parse_df_valid() {
469 let count = DropFrameCalc::parse_df("00;00;00;00").expect("should succeed");
470 assert_eq!(count, 0);
471 }
472
473 #[test]
474 fn test_parse_df_invalid() {
475 assert!(DropFrameCalc::parse_df("00:00:00:00").is_none()); assert!(DropFrameCalc::parse_df("25;00;00;00").is_none()); assert!(DropFrameCalc::parse_df("not;a;timecode;x").is_none());
478 assert!(DropFrameCalc::parse_df("").is_none());
479 }
480
481 #[test]
482 fn test_is_dropped_frame() {
483 assert!(DropFrameCalc::is_dropped_frame(0, 1, 0, 0));
485 assert!(DropFrameCalc::is_dropped_frame(0, 1, 0, 1));
486 assert!(!DropFrameCalc::is_dropped_frame(0, 1, 0, 2));
487
488 assert!(!DropFrameCalc::is_dropped_frame(0, 10, 0, 0));
490 assert!(!DropFrameCalc::is_dropped_frame(0, 10, 0, 1));
491
492 assert!(!DropFrameCalc::is_dropped_frame(0, 1, 1, 0));
494 }
495
496 #[test]
497 fn test_total_frame_counter_drop_frame() {
498 let mut counter = TotalFrameCounter::new_drop_frame();
499 assert!(counter.is_drop_frame());
500 counter.add_frames(100);
501 assert_eq!(counter.frame_count(), 100);
502 let s = counter.to_string();
503 assert!(s.contains(';')); }
505
506 #[test]
507 fn test_total_frame_counter_non_drop_frame() {
508 let mut counter = TotalFrameCounter::new_non_drop_frame(25);
509 assert!(!counter.is_drop_frame());
510 counter.add_frames(25); assert_eq!(counter.frame_count(), 25);
512 let s = counter.to_string();
513 assert!(s.contains(':'));
514 assert_eq!(s, "00:00:01:00");
515 }
516
517 #[test]
518 fn test_total_frame_counter_reset() {
519 let mut counter = TotalFrameCounter::new_drop_frame();
520 counter.add_frames(1000);
521 counter.reset();
522 assert_eq!(counter.frame_count(), 0);
523 }
524
525 #[test]
526 fn test_parse_format_roundtrip() {
527 let original = "01;05;30;15";
528 let frame_count = DropFrameCalc::parse_df(original).expect("should succeed");
529 let formatted = DropFrameCalc::format_df(frame_count);
530 assert_eq!(formatted, original);
531 }
532}