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)]
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
217fn raw_timecode(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop: bool) -> Timecode {
221 Timecode {
222 hours,
223 minutes,
224 seconds,
225 frames,
226 frame_rate: FrameRateInfo {
227 fps,
228 drop_frame: drop,
229 },
230 user_bits: 0,
231 }
232}
233
234#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::FrameRate;
240
241 fn valid_25fps() -> Timecode {
242 Timecode::new(1, 30, 0, 12, FrameRate::Fps25).expect("valid timecode")
243 }
244
245 #[test]
246 fn test_valid_timecode_no_violations() {
247 let v = TimecodeValidator::default_validator();
248 assert!(v.validate(&valid_25fps()).is_empty());
249 }
250
251 #[test]
252 fn test_is_valid_returns_true_for_good_tc() {
253 let v = TimecodeValidator::default_validator();
254 assert!(v.is_valid(&valid_25fps()));
255 }
256
257 #[test]
258 fn test_hours_out_of_range() {
259 let tc = raw_timecode(24, 0, 0, 0, 25, false);
260 let v = TimecodeValidator::default_validator();
261 let vios = v.validate(&tc);
262 assert!(vios.iter().any(|x| x.rule == ValidationRule::HoursInRange));
263 }
264
265 #[test]
266 fn test_minutes_out_of_range() {
267 let tc = raw_timecode(0, 60, 0, 0, 25, false);
268 let v = TimecodeValidator::default_validator();
269 let vios = v.validate(&tc);
270 assert!(vios
271 .iter()
272 .any(|x| x.rule == ValidationRule::MinutesInRange));
273 }
274
275 #[test]
276 fn test_seconds_out_of_range() {
277 let tc = raw_timecode(0, 0, 60, 0, 25, false);
278 let v = TimecodeValidator::default_validator();
279 let vios = v.validate(&tc);
280 assert!(vios
281 .iter()
282 .any(|x| x.rule == ValidationRule::SecondsInRange));
283 }
284
285 #[test]
286 fn test_frames_out_of_range() {
287 let tc = raw_timecode(0, 0, 0, 25, 25, false);
288 let v = TimecodeValidator::default_validator();
289 let vios = v.validate(&tc);
290 assert!(vios.iter().any(|x| x.rule == ValidationRule::FramesInRange));
291 }
292
293 #[test]
294 fn test_drop_frame_illegal_position_detected() {
295 let tc = raw_timecode(0, 1, 0, 0, 30, true);
297 let v = TimecodeValidator::default_validator();
298 let vios = v.validate(&tc);
299 assert!(vios
300 .iter()
301 .any(|x| x.rule == ValidationRule::DropFramePositions));
302 }
303
304 #[test]
305 fn test_drop_frame_tenth_minute_is_ok() {
306 let tc = raw_timecode(0, 10, 0, 0, 30, true);
308 let v = TimecodeValidator::default_validator();
309 let vios = v.validate(&tc);
310 assert!(!vios
311 .iter()
312 .any(|x| x.rule == ValidationRule::DropFramePositions));
313 }
314
315 #[test]
316 fn test_within_range_pass() {
317 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
319 rules: vec![ValidationRule::WithinRange],
320 allowed_range: Some((0, 100)),
321 };
322 let v = TimecodeValidator::new(cfg);
323 assert!(v.validate(&tc).is_empty());
324 }
325
326 #[test]
327 fn test_within_range_fail() {
328 let tc = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).expect("valid timecode"); let cfg = ValidatorConfig {
330 rules: vec![ValidationRule::WithinRange],
331 allowed_range: Some((0, 100)),
332 };
333 let v = TimecodeValidator::new(cfg);
334 let vios = v.validate(&tc);
335 assert!(vios.iter().any(|x| x.rule == ValidationRule::WithinRange));
336 }
337
338 #[test]
339 fn test_validate_range_empty_slice() {
340 let v = TimecodeValidator::default_validator();
341 assert!(v.validate_range(&[]).is_empty());
342 }
343
344 #[test]
345 fn test_validate_range_all_valid() {
346 let tcs: Vec<Timecode> = (0u8..5)
347 .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid timecode"))
348 .collect();
349 let v = TimecodeValidator::default_validator();
350 assert!(v.validate_range(&tcs).is_empty());
351 }
352
353 #[test]
354 fn test_validate_range_with_violation() {
355 let good = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
356 let bad = raw_timecode(0, 0, 0, 25, 25, false); let v = TimecodeValidator::default_validator();
358 let results = v.validate_range(&[good, bad]);
359 assert!(!results.is_empty());
360 assert_eq!(results[0].0, 1); }
362
363 #[test]
364 fn test_rule_display() {
365 assert_eq!(ValidationRule::HoursInRange.to_string(), "hours-in-range");
366 assert_eq!(
367 ValidationRule::DropFramePositions.to_string(),
368 "drop-frame-positions"
369 );
370 assert_eq!(ValidationRule::WithinRange.to_string(), "within-range");
371 }
372
373 #[test]
374 fn test_violation_display() {
375 let v = TcViolation::new(ValidationRule::FramesInRange, "frames 30 >= fps 30");
376 let s = v.to_string();
377 assert!(s.contains("frames-in-range"));
378 assert!(s.contains("frames 30"));
379 }
380
381 #[test]
382 fn test_no_rules_produces_no_violations() {
383 let tc = raw_timecode(99, 99, 99, 99, 25, false); let cfg = ValidatorConfig {
385 rules: vec![],
386 allowed_range: None,
387 };
388 let v = TimecodeValidator::new(cfg);
389 assert!(v.validate(&tc).is_empty());
390 }
391
392 #[test]
393 fn test_multiple_violations_accumulate() {
394 let tc = raw_timecode(24, 60, 60, 25, 25, false);
395 let v = TimecodeValidator::default_validator();
396 let vios = v.validate(&tc);
397 assert!(vios.len() >= 4);
399 }
400}