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