1#![allow(dead_code)]
8
9use crate::Timecode;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ValidationRule {
16 HoursInRange,
18 MinutesInRange,
20 SecondsInRange,
22 FramesInRange,
24 DropFramePositions,
26 WithinRange,
28}
29
30impl std::fmt::Display for ValidationRule {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::HoursInRange => write!(f, "hours-in-range"),
34 Self::MinutesInRange => write!(f, "minutes-in-range"),
35 Self::SecondsInRange => write!(f, "seconds-in-range"),
36 Self::FramesInRange => write!(f, "frames-in-range"),
37 Self::DropFramePositions => write!(f, "drop-frame-positions"),
38 Self::WithinRange => write!(f, "within-range"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TcViolation {
48 pub rule: ValidationRule,
50 pub message: String,
52}
53
54impl TcViolation {
55 pub fn new(rule: ValidationRule, message: impl Into<String>) -> Self {
57 Self {
58 rule,
59 message: message.into(),
60 }
61 }
62}
63
64impl std::fmt::Display for TcViolation {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "[{}] {}", self.rule, self.message)
67 }
68}
69
70#[derive(Debug, Clone)]
74pub struct ValidatorConfig {
75 pub rules: Vec<ValidationRule>,
77 pub allowed_range: Option<(u64, u64)>,
80}
81
82impl Default for ValidatorConfig {
83 fn default() -> Self {
84 Self {
85 rules: vec![
86 ValidationRule::HoursInRange,
87 ValidationRule::MinutesInRange,
88 ValidationRule::SecondsInRange,
89 ValidationRule::FramesInRange,
90 ValidationRule::DropFramePositions,
91 ],
92 allowed_range: None,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
112pub struct TimecodeValidator {
113 config: ValidatorConfig,
114}
115
116impl TimecodeValidator {
117 pub fn new(config: ValidatorConfig) -> Self {
119 Self { config }
120 }
121
122 pub fn default_validator() -> Self {
124 Self::new(ValidatorConfig::default())
125 }
126
127 pub fn validate(&self, tc: &Timecode) -> Vec<TcViolation> {
130 let mut violations = Vec::new();
131 for &rule in &self.config.rules {
132 match rule {
133 ValidationRule::HoursInRange => {
134 if tc.hours > 23 {
135 violations.push(TcViolation::new(
136 rule,
137 format!("hours {} exceeds maximum of 23", tc.hours),
138 ));
139 }
140 }
141 ValidationRule::MinutesInRange => {
142 if tc.minutes > 59 {
143 violations.push(TcViolation::new(
144 rule,
145 format!("minutes {} exceeds maximum of 59", tc.minutes),
146 ));
147 }
148 }
149 ValidationRule::SecondsInRange => {
150 if tc.seconds > 59 {
151 violations.push(TcViolation::new(
152 rule,
153 format!("seconds {} exceeds maximum of 59", tc.seconds),
154 ));
155 }
156 }
157 ValidationRule::FramesInRange => {
158 if tc.frames >= tc.frame_rate.fps {
159 violations.push(TcViolation::new(
160 rule,
161 format!("frames {} >= fps {}", tc.frames, tc.frame_rate.fps),
162 ));
163 }
164 }
165 ValidationRule::DropFramePositions => {
166 if tc.frame_rate.drop_frame
167 && tc.seconds == 0
168 && tc.frames < 2
169 && !tc.minutes.is_multiple_of(10)
170 {
171 violations.push(TcViolation::new(
172 rule,
173 format!(
174 "frames {f} at {m}:00 is an illegal drop-frame position",
175 f = tc.frames,
176 m = tc.minutes,
177 ),
178 ));
179 }
180 }
181 ValidationRule::WithinRange => {
182 if let Some((start, end)) = self.config.allowed_range {
183 let pos = tc.to_frames();
184 if pos < start || pos > end {
185 violations.push(TcViolation::new(
186 rule,
187 format!(
188 "frame position {pos} is outside allowed range [{start}, {end}]"
189 ),
190 ));
191 }
192 }
193 }
194 }
195 }
196 violations
197 }
198
199 pub fn validate_range(&self, timecodes: &[Timecode]) -> Vec<(usize, TcViolation)> {
202 let mut out = Vec::new();
203 for (i, tc) in timecodes.iter().enumerate() {
204 for v in self.validate(tc) {
205 out.push((i, v));
206 }
207 }
208 out
209 }
210
211 pub fn is_valid(&self, tc: &Timecode) -> bool {
213 self.validate(tc).is_empty()
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct NonMonotonicEvent {
224 pub frame_index: usize,
226 pub prev_tc: Timecode,
228 pub curr_tc: Timecode,
230 pub jump_frames: i64,
233}
234
235#[derive(Debug, Clone)]
257pub struct NonMonotonicDetector {
258 threshold_frames: i64,
263}
264
265impl NonMonotonicDetector {
266 pub fn new(threshold_frames: i64) -> Self {
271 Self { threshold_frames }
272 }
273
274 pub fn scan_sequence(self, timecodes: &[Timecode]) -> Vec<NonMonotonicEvent> {
279 let mut events = Vec::new();
280
281 for i in 1..timecodes.len() {
282 let prev = timecodes[i - 1];
283 let curr = timecodes[i];
284
285 let prev_f = prev.to_frames() as i64;
286 let curr_f = curr.to_frames() as i64;
287 let jump = curr_f - prev_f;
288
289 let deviation = (jump - 1).abs();
292 if deviation > self.threshold_frames {
293 events.push(NonMonotonicEvent {
294 frame_index: i,
295 prev_tc: prev,
296 curr_tc: curr,
297 jump_frames: jump,
298 });
299 }
300 }
301
302 events
303 }
304}
305
306fn raw_timecode(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop: bool) -> Timecode {
310 Timecode::from_raw_fields(hours, minutes, seconds, frames, fps, drop, 0)
311}
312
313#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::FrameRate;
319
320 fn valid_25fps() -> Timecode {
321 Timecode::new(1, 30, 0, 12, FrameRate::Fps25).expect("valid timecode")
322 }
323
324 #[test]
325 fn test_valid_timecode_no_violations() {
326 let v = TimecodeValidator::default_validator();
327 assert!(v.validate(&valid_25fps()).is_empty());
328 }
329
330 #[test]
331 fn test_is_valid_returns_true_for_good_tc() {
332 let v = TimecodeValidator::default_validator();
333 assert!(v.is_valid(&valid_25fps()));
334 }
335
336 #[test]
337 fn test_hours_out_of_range() {
338 let tc = raw_timecode(24, 0, 0, 0, 25, false);
339 let v = TimecodeValidator::default_validator();
340 let vios = v.validate(&tc);
341 assert!(vios.iter().any(|x| x.rule == ValidationRule::HoursInRange));
342 }
343
344 #[test]
345 fn test_minutes_out_of_range() {
346 let tc = raw_timecode(0, 60, 0, 0, 25, false);
347 let v = TimecodeValidator::default_validator();
348 let vios = v.validate(&tc);
349 assert!(vios
350 .iter()
351 .any(|x| x.rule == ValidationRule::MinutesInRange));
352 }
353
354 #[test]
355 fn test_seconds_out_of_range() {
356 let tc = raw_timecode(0, 0, 60, 0, 25, false);
357 let v = TimecodeValidator::default_validator();
358 let vios = v.validate(&tc);
359 assert!(vios
360 .iter()
361 .any(|x| x.rule == ValidationRule::SecondsInRange));
362 }
363
364 #[test]
365 fn test_frames_out_of_range() {
366 let tc = raw_timecode(0, 0, 0, 25, 25, false);
367 let v = TimecodeValidator::default_validator();
368 let vios = v.validate(&tc);
369 assert!(vios.iter().any(|x| x.rule == ValidationRule::FramesInRange));
370 }
371
372 #[test]
373 fn test_drop_frame_illegal_position_detected() {
374 let tc = raw_timecode(0, 1, 0, 0, 30, true);
376 let v = TimecodeValidator::default_validator();
377 let vios = v.validate(&tc);
378 assert!(vios
379 .iter()
380 .any(|x| x.rule == ValidationRule::DropFramePositions));
381 }
382
383 #[test]
384 fn test_drop_frame_tenth_minute_is_ok() {
385 let tc = raw_timecode(0, 10, 0, 0, 30, true);
387 let v = TimecodeValidator::default_validator();
388 let vios = v.validate(&tc);
389 assert!(!vios
390 .iter()
391 .any(|x| x.rule == ValidationRule::DropFramePositions));
392 }
393
394 #[test]
395 fn test_within_range_pass() {
396 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
398 rules: vec![ValidationRule::WithinRange],
399 allowed_range: Some((0, 100)),
400 };
401 let v = TimecodeValidator::new(cfg);
402 assert!(v.validate(&tc).is_empty());
403 }
404
405 #[test]
406 fn test_within_range_fail() {
407 let tc = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
409 rules: vec![ValidationRule::WithinRange],
410 allowed_range: Some((0, 100)),
411 };
412 let v = TimecodeValidator::new(cfg);
413 let vios = v.validate(&tc);
414 assert!(vios.iter().any(|x| x.rule == ValidationRule::WithinRange));
415 }
416
417 #[test]
418 fn test_validate_range_empty_slice() {
419 let v = TimecodeValidator::default_validator();
420 assert!(v.validate_range(&[]).is_empty());
421 }
422
423 #[test]
424 fn test_validate_range_all_valid() {
425 let tcs: Vec<Timecode> = (0u8..5)
426 .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid timecode"))
427 .collect();
428 let v = TimecodeValidator::default_validator();
429 assert!(v.validate_range(&tcs).is_empty());
430 }
431
432 #[test]
433 fn test_validate_range_with_violation() {
434 let good = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
435 let bad = raw_timecode(0, 0, 0, 25, 25, false); let v = TimecodeValidator::default_validator();
437 let results = v.validate_range(&[good, bad]);
438 assert!(!results.is_empty());
439 assert_eq!(results[0].0, 1); }
441
442 #[test]
443 fn test_rule_display() {
444 assert_eq!(ValidationRule::HoursInRange.to_string(), "hours-in-range");
445 assert_eq!(
446 ValidationRule::DropFramePositions.to_string(),
447 "drop-frame-positions"
448 );
449 assert_eq!(ValidationRule::WithinRange.to_string(), "within-range");
450 }
451
452 #[test]
453 fn test_violation_display() {
454 let v = TcViolation::new(ValidationRule::FramesInRange, "frames 30 >= fps 30");
455 let s = v.to_string();
456 assert!(s.contains("frames-in-range"));
457 assert!(s.contains("frames 30"));
458 }
459
460 #[test]
461 fn test_no_rules_produces_no_violations() {
462 let tc = raw_timecode(99, 99, 99, 99, 25, false); let cfg = ValidatorConfig {
464 rules: vec![],
465 allowed_range: None,
466 };
467 let v = TimecodeValidator::new(cfg);
468 assert!(v.validate(&tc).is_empty());
469 }
470
471 #[test]
472 fn test_multiple_violations_accumulate() {
473 let tc = raw_timecode(24, 60, 60, 25, 25, false);
474 let v = TimecodeValidator::default_validator();
475 let vios = v.validate(&tc);
476 assert!(vios.len() >= 4);
478 }
479
480 #[test]
483 fn test_non_monotonic_empty_slice() {
484 let events = NonMonotonicDetector::new(0).scan_sequence(&[]);
485 assert!(events.is_empty());
486 }
487
488 #[test]
489 fn test_non_monotonic_single_element() {
490 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
491 let events = NonMonotonicDetector::new(0).scan_sequence(&[tc]);
492 assert!(events.is_empty());
493 }
494
495 #[test]
496 fn test_non_monotonic_normal_sequence_no_events() {
497 let tcs: Vec<Timecode> = (0u8..25)
499 .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid"))
500 .collect();
501 let events = NonMonotonicDetector::new(0).scan_sequence(&tcs);
502 assert!(
503 events.is_empty(),
504 "sequential sequence should produce no events, got: {:?}",
505 events
506 );
507 }
508
509 #[test]
510 fn test_non_monotonic_2_second_jump_detected() {
511 let tc0 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
513 let tc1 = Timecode::new(0, 0, 2, 0, FrameRate::Fps25).expect("valid");
514 let events = NonMonotonicDetector::new(1).scan_sequence(&[tc0, tc1]);
515 assert_eq!(events.len(), 1);
516 assert_eq!(events[0].frame_index, 1);
517 assert_eq!(events[0].jump_frames, 50);
518 }
519
520 #[test]
521 fn test_non_monotonic_backwards_detected() {
522 let tc0 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid");
523 let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid"); let events = NonMonotonicDetector::new(0).scan_sequence(&[tc0, tc1]);
525 assert_eq!(events.len(), 1);
526 assert!(events[0].jump_frames < 0);
527 }
528
529 #[test]
530 fn test_non_monotonic_threshold_filters_small_jumps() {
531 let tc0 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
533 let tc1 = Timecode::new(0, 0, 0, 2, FrameRate::Fps25).expect("valid"); let events = NonMonotonicDetector::new(1).scan_sequence(&[tc0, tc1]);
536 assert!(
537 events.is_empty(),
538 "jump of 2 should be filtered by threshold=1"
539 );
540
541 let tc0b = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
543 let tc1b = Timecode::new(0, 0, 0, 2, FrameRate::Fps25).expect("valid");
544 let events2 = NonMonotonicDetector::new(0).scan_sequence(&[tc0b, tc1b]);
545 assert_eq!(events2.len(), 1);
546 }
547
548 #[test]
549 fn test_non_monotonic_multiple_events() {
550 let tcs = vec![
552 Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid"),
553 Timecode::new(0, 0, 0, 1, FrameRate::Fps25).expect("valid"), Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid"), Timecode::new(0, 0, 1, 1, FrameRate::Fps25).expect("valid"), Timecode::new(0, 0, 0, 5, FrameRate::Fps25).expect("valid"), ];
558 let events = NonMonotonicDetector::new(1).scan_sequence(&tcs);
559 assert_eq!(events.len(), 2);
561 assert_eq!(events[0].frame_index, 2);
562 assert_eq!(events[1].frame_index, 4);
563 }
564}