#![allow(dead_code)]
use crate::Timecode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationRule {
HoursInRange,
MinutesInRange,
SecondsInRange,
FramesInRange,
DropFramePositions,
WithinRange,
}
impl std::fmt::Display for ValidationRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HoursInRange => write!(f, "hours-in-range"),
Self::MinutesInRange => write!(f, "minutes-in-range"),
Self::SecondsInRange => write!(f, "seconds-in-range"),
Self::FramesInRange => write!(f, "frames-in-range"),
Self::DropFramePositions => write!(f, "drop-frame-positions"),
Self::WithinRange => write!(f, "within-range"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TcViolation {
pub rule: ValidationRule,
pub message: String,
}
impl TcViolation {
pub fn new(rule: ValidationRule, message: impl Into<String>) -> Self {
Self {
rule,
message: message.into(),
}
}
}
impl std::fmt::Display for TcViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.rule, self.message)
}
}
#[derive(Debug, Clone)]
pub struct ValidatorConfig {
pub rules: Vec<ValidationRule>,
pub allowed_range: Option<(u64, u64)>,
}
impl Default for ValidatorConfig {
fn default() -> Self {
Self {
rules: vec![
ValidationRule::HoursInRange,
ValidationRule::MinutesInRange,
ValidationRule::SecondsInRange,
ValidationRule::FramesInRange,
ValidationRule::DropFramePositions,
],
allowed_range: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TimecodeValidator {
config: ValidatorConfig,
}
impl TimecodeValidator {
pub fn new(config: ValidatorConfig) -> Self {
Self { config }
}
pub fn default_validator() -> Self {
Self::new(ValidatorConfig::default())
}
pub fn validate(&self, tc: &Timecode) -> Vec<TcViolation> {
let mut violations = Vec::new();
for &rule in &self.config.rules {
match rule {
ValidationRule::HoursInRange => {
if tc.hours > 23 {
violations.push(TcViolation::new(
rule,
format!("hours {} exceeds maximum of 23", tc.hours),
));
}
}
ValidationRule::MinutesInRange => {
if tc.minutes > 59 {
violations.push(TcViolation::new(
rule,
format!("minutes {} exceeds maximum of 59", tc.minutes),
));
}
}
ValidationRule::SecondsInRange => {
if tc.seconds > 59 {
violations.push(TcViolation::new(
rule,
format!("seconds {} exceeds maximum of 59", tc.seconds),
));
}
}
ValidationRule::FramesInRange => {
if tc.frames >= tc.frame_rate.fps {
violations.push(TcViolation::new(
rule,
format!("frames {} >= fps {}", tc.frames, tc.frame_rate.fps),
));
}
}
ValidationRule::DropFramePositions => {
if tc.frame_rate.drop_frame
&& tc.seconds == 0
&& tc.frames < 2
&& !tc.minutes.is_multiple_of(10)
{
violations.push(TcViolation::new(
rule,
format!(
"frames {f} at {m}:00 is an illegal drop-frame position",
f = tc.frames,
m = tc.minutes,
),
));
}
}
ValidationRule::WithinRange => {
if let Some((start, end)) = self.config.allowed_range {
let pos = tc.to_frames();
if pos < start || pos > end {
violations.push(TcViolation::new(
rule,
format!(
"frame position {pos} is outside allowed range [{start}, {end}]"
),
));
}
}
}
}
}
violations
}
pub fn validate_range(&self, timecodes: &[Timecode]) -> Vec<(usize, TcViolation)> {
let mut out = Vec::new();
for (i, tc) in timecodes.iter().enumerate() {
for v in self.validate(tc) {
out.push((i, v));
}
}
out
}
pub fn is_valid(&self, tc: &Timecode) -> bool {
self.validate(tc).is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonMonotonicEvent {
pub frame_index: usize,
pub prev_tc: Timecode,
pub curr_tc: Timecode,
pub jump_frames: i64,
}
#[derive(Debug, Clone)]
pub struct NonMonotonicDetector {
threshold_frames: i64,
}
impl NonMonotonicDetector {
pub fn new(threshold_frames: i64) -> Self {
Self { threshold_frames }
}
pub fn scan_sequence(self, timecodes: &[Timecode]) -> Vec<NonMonotonicEvent> {
let mut events = Vec::new();
for i in 1..timecodes.len() {
let prev = timecodes[i - 1];
let curr = timecodes[i];
let prev_f = prev.to_frames() as i64;
let curr_f = curr.to_frames() as i64;
let jump = curr_f - prev_f;
let deviation = (jump - 1).abs();
if deviation > self.threshold_frames {
events.push(NonMonotonicEvent {
frame_index: i,
prev_tc: prev,
curr_tc: curr,
jump_frames: jump,
});
}
}
events
}
}
fn raw_timecode(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop: bool) -> Timecode {
Timecode::from_raw_fields(hours, minutes, seconds, frames, fps, drop, 0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FrameRate;
fn valid_25fps() -> Timecode {
Timecode::new(1, 30, 0, 12, FrameRate::Fps25).expect("valid timecode")
}
#[test]
fn test_valid_timecode_no_violations() {
let v = TimecodeValidator::default_validator();
assert!(v.validate(&valid_25fps()).is_empty());
}
#[test]
fn test_is_valid_returns_true_for_good_tc() {
let v = TimecodeValidator::default_validator();
assert!(v.is_valid(&valid_25fps()));
}
#[test]
fn test_hours_out_of_range() {
let tc = raw_timecode(24, 0, 0, 0, 25, false);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios.iter().any(|x| x.rule == ValidationRule::HoursInRange));
}
#[test]
fn test_minutes_out_of_range() {
let tc = raw_timecode(0, 60, 0, 0, 25, false);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios
.iter()
.any(|x| x.rule == ValidationRule::MinutesInRange));
}
#[test]
fn test_seconds_out_of_range() {
let tc = raw_timecode(0, 0, 60, 0, 25, false);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios
.iter()
.any(|x| x.rule == ValidationRule::SecondsInRange));
}
#[test]
fn test_frames_out_of_range() {
let tc = raw_timecode(0, 0, 0, 25, 25, false);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios.iter().any(|x| x.rule == ValidationRule::FramesInRange));
}
#[test]
fn test_drop_frame_illegal_position_detected() {
let tc = raw_timecode(0, 1, 0, 0, 30, true);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios
.iter()
.any(|x| x.rule == ValidationRule::DropFramePositions));
}
#[test]
fn test_drop_frame_tenth_minute_is_ok() {
let tc = raw_timecode(0, 10, 0, 0, 30, true);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(!vios
.iter()
.any(|x| x.rule == ValidationRule::DropFramePositions));
}
#[test]
fn test_within_range_pass() {
let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
rules: vec![ValidationRule::WithinRange],
allowed_range: Some((0, 100)),
};
let v = TimecodeValidator::new(cfg);
assert!(v.validate(&tc).is_empty());
}
#[test]
fn test_within_range_fail() {
let tc = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
rules: vec![ValidationRule::WithinRange],
allowed_range: Some((0, 100)),
};
let v = TimecodeValidator::new(cfg);
let vios = v.validate(&tc);
assert!(vios.iter().any(|x| x.rule == ValidationRule::WithinRange));
}
#[test]
fn test_validate_range_empty_slice() {
let v = TimecodeValidator::default_validator();
assert!(v.validate_range(&[]).is_empty());
}
#[test]
fn test_validate_range_all_valid() {
let tcs: Vec<Timecode> = (0u8..5)
.map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid timecode"))
.collect();
let v = TimecodeValidator::default_validator();
assert!(v.validate_range(&tcs).is_empty());
}
#[test]
fn test_validate_range_with_violation() {
let good = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
let bad = raw_timecode(0, 0, 0, 25, 25, false); let v = TimecodeValidator::default_validator();
let results = v.validate_range(&[good, bad]);
assert!(!results.is_empty());
assert_eq!(results[0].0, 1); }
#[test]
fn test_rule_display() {
assert_eq!(ValidationRule::HoursInRange.to_string(), "hours-in-range");
assert_eq!(
ValidationRule::DropFramePositions.to_string(),
"drop-frame-positions"
);
assert_eq!(ValidationRule::WithinRange.to_string(), "within-range");
}
#[test]
fn test_violation_display() {
let v = TcViolation::new(ValidationRule::FramesInRange, "frames 30 >= fps 30");
let s = v.to_string();
assert!(s.contains("frames-in-range"));
assert!(s.contains("frames 30"));
}
#[test]
fn test_no_rules_produces_no_violations() {
let tc = raw_timecode(99, 99, 99, 99, 25, false); let cfg = ValidatorConfig {
rules: vec![],
allowed_range: None,
};
let v = TimecodeValidator::new(cfg);
assert!(v.validate(&tc).is_empty());
}
#[test]
fn test_multiple_violations_accumulate() {
let tc = raw_timecode(24, 60, 60, 25, 25, false);
let v = TimecodeValidator::default_validator();
let vios = v.validate(&tc);
assert!(vios.len() >= 4);
}
#[test]
fn test_non_monotonic_empty_slice() {
let events = NonMonotonicDetector::new(0).scan_sequence(&[]);
assert!(events.is_empty());
}
#[test]
fn test_non_monotonic_single_element() {
let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let events = NonMonotonicDetector::new(0).scan_sequence(&[tc]);
assert!(events.is_empty());
}
#[test]
fn test_non_monotonic_normal_sequence_no_events() {
let tcs: Vec<Timecode> = (0u8..25)
.map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid"))
.collect();
let events = NonMonotonicDetector::new(0).scan_sequence(&tcs);
assert!(
events.is_empty(),
"sequential sequence should produce no events, got: {:?}",
events
);
}
#[test]
fn test_non_monotonic_2_second_jump_detected() {
let tc0 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let tc1 = Timecode::new(0, 0, 2, 0, FrameRate::Fps25).expect("valid");
let events = NonMonotonicDetector::new(1).scan_sequence(&[tc0, tc1]);
assert_eq!(events.len(), 1);
assert_eq!(events[0].frame_index, 1);
assert_eq!(events[0].jump_frames, 50);
}
#[test]
fn test_non_monotonic_backwards_detected() {
let tc0 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid");
let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid"); let events = NonMonotonicDetector::new(0).scan_sequence(&[tc0, tc1]);
assert_eq!(events.len(), 1);
assert!(events[0].jump_frames < 0);
}
#[test]
fn test_non_monotonic_threshold_filters_small_jumps() {
let tc0 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let tc1 = Timecode::new(0, 0, 0, 2, FrameRate::Fps25).expect("valid"); let events = NonMonotonicDetector::new(1).scan_sequence(&[tc0, tc1]);
assert!(
events.is_empty(),
"jump of 2 should be filtered by threshold=1"
);
let tc0b = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let tc1b = Timecode::new(0, 0, 0, 2, FrameRate::Fps25).expect("valid");
let events2 = NonMonotonicDetector::new(0).scan_sequence(&[tc0b, tc1b]);
assert_eq!(events2.len(), 1);
}
#[test]
fn test_non_monotonic_multiple_events() {
let tcs = vec![
Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid"),
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"), ];
let events = NonMonotonicDetector::new(1).scan_sequence(&tcs);
assert_eq!(events.len(), 2);
assert_eq!(events[0].frame_index, 2);
assert_eq!(events[1].frame_index, 4);
}
}