1#![allow(dead_code)]
8
9use crate::{FrameRateInfo, 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)]
109pub struct TimecodeValidator {
110 config: ValidatorConfig,
111}
112
113impl TimecodeValidator {
114 pub fn new(config: ValidatorConfig) -> Self {
116 Self { config }
117 }
118
119 pub fn default_validator() -> Self {
121 Self::new(ValidatorConfig::default())
122 }
123
124 pub fn validate(&self, tc: &Timecode) -> Vec<TcViolation> {
127 let mut violations = Vec::new();
128 for &rule in &self.config.rules {
129 match rule {
130 ValidationRule::HoursInRange => {
131 if tc.hours > 23 {
132 violations.push(TcViolation::new(
133 rule,
134 format!("hours {} exceeds maximum of 23", tc.hours),
135 ));
136 }
137 }
138 ValidationRule::MinutesInRange => {
139 if tc.minutes > 59 {
140 violations.push(TcViolation::new(
141 rule,
142 format!("minutes {} exceeds maximum of 59", tc.minutes),
143 ));
144 }
145 }
146 ValidationRule::SecondsInRange => {
147 if tc.seconds > 59 {
148 violations.push(TcViolation::new(
149 rule,
150 format!("seconds {} exceeds maximum of 59", tc.seconds),
151 ));
152 }
153 }
154 ValidationRule::FramesInRange => {
155 if tc.frames >= tc.frame_rate.fps {
156 violations.push(TcViolation::new(
157 rule,
158 format!("frames {} >= fps {}", tc.frames, tc.frame_rate.fps),
159 ));
160 }
161 }
162 ValidationRule::DropFramePositions => {
163 if tc.frame_rate.drop_frame
164 && tc.seconds == 0
165 && tc.frames < 2
166 && !tc.minutes.is_multiple_of(10)
167 {
168 violations.push(TcViolation::new(
169 rule,
170 format!(
171 "frames {f} at {m}:00 is an illegal drop-frame position",
172 f = tc.frames,
173 m = tc.minutes,
174 ),
175 ));
176 }
177 }
178 ValidationRule::WithinRange => {
179 if let Some((start, end)) = self.config.allowed_range {
180 let pos = tc.to_frames();
181 if pos < start || pos > end {
182 violations.push(TcViolation::new(
183 rule,
184 format!(
185 "frame position {pos} is outside allowed range [{start}, {end}]"
186 ),
187 ));
188 }
189 }
190 }
191 }
192 }
193 violations
194 }
195
196 pub fn validate_range(&self, timecodes: &[Timecode]) -> Vec<(usize, TcViolation)> {
199 let mut out = Vec::new();
200 for (i, tc) in timecodes.iter().enumerate() {
201 for v in self.validate(tc) {
202 out.push((i, v));
203 }
204 }
205 out
206 }
207
208 pub fn is_valid(&self, tc: &Timecode) -> bool {
210 self.validate(tc).is_empty()
211 }
212}
213
214fn raw_timecode(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop: bool) -> Timecode {
218 Timecode {
219 hours,
220 minutes,
221 seconds,
222 frames,
223 frame_rate: FrameRateInfo {
224 fps,
225 drop_frame: drop,
226 },
227 user_bits: 0,
228 }
229}
230
231#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::FrameRate;
237
238 fn valid_25fps() -> Timecode {
239 Timecode::new(1, 30, 0, 12, FrameRate::Fps25).unwrap()
240 }
241
242 #[test]
243 fn test_valid_timecode_no_violations() {
244 let v = TimecodeValidator::default_validator();
245 assert!(v.validate(&valid_25fps()).is_empty());
246 }
247
248 #[test]
249 fn test_is_valid_returns_true_for_good_tc() {
250 let v = TimecodeValidator::default_validator();
251 assert!(v.is_valid(&valid_25fps()));
252 }
253
254 #[test]
255 fn test_hours_out_of_range() {
256 let tc = raw_timecode(24, 0, 0, 0, 25, false);
257 let v = TimecodeValidator::default_validator();
258 let vios = v.validate(&tc);
259 assert!(vios.iter().any(|x| x.rule == ValidationRule::HoursInRange));
260 }
261
262 #[test]
263 fn test_minutes_out_of_range() {
264 let tc = raw_timecode(0, 60, 0, 0, 25, false);
265 let v = TimecodeValidator::default_validator();
266 let vios = v.validate(&tc);
267 assert!(vios
268 .iter()
269 .any(|x| x.rule == ValidationRule::MinutesInRange));
270 }
271
272 #[test]
273 fn test_seconds_out_of_range() {
274 let tc = raw_timecode(0, 0, 60, 0, 25, false);
275 let v = TimecodeValidator::default_validator();
276 let vios = v.validate(&tc);
277 assert!(vios
278 .iter()
279 .any(|x| x.rule == ValidationRule::SecondsInRange));
280 }
281
282 #[test]
283 fn test_frames_out_of_range() {
284 let tc = raw_timecode(0, 0, 0, 25, 25, false);
285 let v = TimecodeValidator::default_validator();
286 let vios = v.validate(&tc);
287 assert!(vios.iter().any(|x| x.rule == ValidationRule::FramesInRange));
288 }
289
290 #[test]
291 fn test_drop_frame_illegal_position_detected() {
292 let tc = raw_timecode(0, 1, 0, 0, 30, true);
294 let v = TimecodeValidator::default_validator();
295 let vios = v.validate(&tc);
296 assert!(vios
297 .iter()
298 .any(|x| x.rule == ValidationRule::DropFramePositions));
299 }
300
301 #[test]
302 fn test_drop_frame_tenth_minute_is_ok() {
303 let tc = raw_timecode(0, 10, 0, 0, 30, true);
305 let v = TimecodeValidator::default_validator();
306 let vios = v.validate(&tc);
307 assert!(!vios
308 .iter()
309 .any(|x| x.rule == ValidationRule::DropFramePositions));
310 }
311
312 #[test]
313 fn test_within_range_pass() {
314 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).unwrap(); let cfg = ValidatorConfig {
316 rules: vec![ValidationRule::WithinRange],
317 allowed_range: Some((0, 100)),
318 };
319 let v = TimecodeValidator::new(cfg);
320 assert!(v.validate(&tc).is_empty());
321 }
322
323 #[test]
324 fn test_within_range_fail() {
325 let tc = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).unwrap(); let cfg = ValidatorConfig {
327 rules: vec![ValidationRule::WithinRange],
328 allowed_range: Some((0, 100)),
329 };
330 let v = TimecodeValidator::new(cfg);
331 let vios = v.validate(&tc);
332 assert!(vios.iter().any(|x| x.rule == ValidationRule::WithinRange));
333 }
334
335 #[test]
336 fn test_validate_range_empty_slice() {
337 let v = TimecodeValidator::default_validator();
338 assert!(v.validate_range(&[]).is_empty());
339 }
340
341 #[test]
342 fn test_validate_range_all_valid() {
343 let tcs: Vec<Timecode> = (0u8..5)
344 .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).unwrap())
345 .collect();
346 let v = TimecodeValidator::default_validator();
347 assert!(v.validate_range(&tcs).is_empty());
348 }
349
350 #[test]
351 fn test_validate_range_with_violation() {
352 let good = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).unwrap();
353 let bad = raw_timecode(0, 0, 0, 25, 25, false); let v = TimecodeValidator::default_validator();
355 let results = v.validate_range(&[good, bad]);
356 assert!(!results.is_empty());
357 assert_eq!(results[0].0, 1); }
359
360 #[test]
361 fn test_rule_display() {
362 assert_eq!(ValidationRule::HoursInRange.to_string(), "hours-in-range");
363 assert_eq!(
364 ValidationRule::DropFramePositions.to_string(),
365 "drop-frame-positions"
366 );
367 assert_eq!(ValidationRule::WithinRange.to_string(), "within-range");
368 }
369
370 #[test]
371 fn test_violation_display() {
372 let v = TcViolation::new(ValidationRule::FramesInRange, "frames 30 >= fps 30");
373 let s = v.to_string();
374 assert!(s.contains("frames-in-range"));
375 assert!(s.contains("frames 30"));
376 }
377
378 #[test]
379 fn test_no_rules_produces_no_violations() {
380 let tc = raw_timecode(99, 99, 99, 99, 25, false); let cfg = ValidatorConfig {
382 rules: vec![],
383 allowed_range: None,
384 };
385 let v = TimecodeValidator::new(cfg);
386 assert!(v.validate(&tc).is_empty());
387 }
388
389 #[test]
390 fn test_multiple_violations_accumulate() {
391 let tc = raw_timecode(24, 60, 60, 25, 25, false);
392 let v = TimecodeValidator::default_validator();
393 let vios = v.validate(&tc);
394 assert!(vios.len() >= 4);
396 }
397}