1extern crate alloc;
32
33use alloc::format;
34use alloc::string::String;
35use alloc::string::ToString;
36use alloc::vec::Vec;
37
38#[derive(Debug, Clone, PartialEq)]
42pub struct TimeContext {
43 pub time_base: TimeBase,
45 pub frame_rate: u32,
47 pub frame_rate_multiplier_numerator: u32,
49 pub frame_rate_multiplier_denominator: u32,
51 pub sub_frame_rate: u32,
53 pub tick_rate: u32,
55 pub drop_mode: DropMode,
57 pub marker_mode: MarkerMode,
59 pub clock_mode: ClockMode,
61}
62
63impl Default for TimeContext {
64 fn default() -> Self {
65 Self {
66 time_base: TimeBase::Media,
67 frame_rate: 30,
68 frame_rate_multiplier_numerator: 1,
69 frame_rate_multiplier_denominator: 1,
70 sub_frame_rate: 1,
71 tick_rate: 30, drop_mode: DropMode::NonDrop,
73 marker_mode: MarkerMode::Discontinuous,
74 clock_mode: ClockMode::Utc,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum TimeBase {
83 Media,
85 Smpte,
87 Clock,
89}
90
91impl TimeBase {
92 pub fn name(&self) -> &'static str {
94 match self {
95 TimeBase::Media => "media",
96 TimeBase::Smpte => "smpte",
97 TimeBase::Clock => "clock",
98 }
99 }
100}
101
102broadcast_common::impl_spec_display!(TimeBase);
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum DropMode {
108 NonDrop,
110 DropNtsc,
112 DropPal,
114}
115
116impl DropMode {
117 pub fn name(&self) -> &'static str {
119 match self {
120 DropMode::NonDrop => "nonDrop",
121 DropMode::DropNtsc => "dropNTSC",
122 DropMode::DropPal => "dropPAL",
123 }
124 }
125}
126
127broadcast_common::impl_spec_display!(DropMode);
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum MarkerMode {
133 Continuous,
135 Discontinuous,
137}
138
139impl MarkerMode {
140 pub fn name(&self) -> &'static str {
142 match self {
143 MarkerMode::Continuous => "continuous",
144 MarkerMode::Discontinuous => "discontinuous",
145 }
146 }
147}
148
149broadcast_common::impl_spec_display!(MarkerMode);
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[non_exhaustive]
154pub enum ClockMode {
155 Local,
157 Utc,
159 Gps,
161}
162
163impl ClockMode {
164 pub fn name(&self) -> &'static str {
166 match self {
167 ClockMode::Local => "local",
168 ClockMode::Utc => "utc",
169 ClockMode::Gps => "gps",
170 }
171 }
172}
173
174broadcast_common::impl_spec_display!(ClockMode);
175
176#[derive(Debug, Clone, PartialEq)]
181#[non_exhaustive]
182pub enum TimeExpression {
183 ClockTime {
185 hours: u32,
187 minutes: u8,
189 seconds: u8,
191 fraction: Option<String>,
193 frames: Option<u32>,
195 sub_frames: Option<u32>,
197 },
198 OffsetTime {
200 count: u64,
202 fraction: Option<String>,
204 metric: TimeMetric,
206 },
207 WallclockTime {
209 form: WallclockForm,
211 },
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum TimeMetric {
218 H,
220 M,
222 S,
224 Ms,
226 F,
228 T,
230}
231
232impl TimeMetric {
233 pub fn name(&self) -> &'static str {
235 match self {
236 TimeMetric::H => "h",
237 TimeMetric::M => "m",
238 TimeMetric::S => "s",
239 TimeMetric::Ms => "ms",
240 TimeMetric::F => "f",
241 TimeMetric::T => "t",
242 }
243 }
244}
245
246broadcast_common::impl_spec_display!(TimeMetric);
247
248#[derive(Debug, Clone, PartialEq)]
250#[non_exhaustive]
251pub enum WallclockForm {
252 DateTime {
254 years: u16,
256 months: u8,
258 days: u8,
260 hours: u8,
262 minutes: u8,
264 seconds: u8,
266 fraction: Option<String>,
268 },
269 WallTime {
271 hours: u8,
273 minutes: u8,
275 seconds: Option<u8>,
277 fraction: Option<String>,
279 },
280 Date {
282 years: u16,
284 months: u8,
286 days: u8,
288 },
289}
290
291pub fn parse_time_expression(
297 input: &str,
298 ctx: &TimeContext,
299) -> Result<TimeExpression, crate::error::Error> {
300 if input.is_empty() {
301 return Err(crate::error::Error::InvalidTimeExpression {
302 value: input.to_string(),
303 reason: "empty time expression".into(),
304 });
305 }
306
307 if input.starts_with("wallclock(") {
309 return parse_wallclock_time(input, ctx);
310 }
311
312 if input.contains(':') {
315 parse_clock_time(input, ctx)
316 } else {
317 parse_offset_time(input, ctx)
318 }
319}
320
321fn parse_clock_time(input: &str, ctx: &TimeContext) -> Result<TimeExpression, crate::error::Error> {
322 let err = |reason: &str| crate::error::Error::InvalidTimeExpression {
323 value: input.to_string(),
324 reason: reason.into(),
325 };
326
327 let parts: Vec<&str> = input.split(':').collect();
329
330 if parts.len() < 3 || parts.len() > 4 {
332 return Err(err(
333 "clock-time must have exactly 3 or 4 colon-separated components",
334 ));
335 }
336
337 if parts.iter().any(|p| p.is_empty()) {
339 return Err(err("empty component in clock-time"));
340 }
341
342 let hours: u32 = parse_digits(parts[0], "hours", &err)?;
344 if hours < 100 && parts[0].len() < 2 {
346 return Err(err(
347 "hours < 100 must have leading zero (at least 2 digits)",
348 ));
349 }
350
351 let minutes_raw = parts[1];
353 if minutes_raw.len() != 2 {
354 return Err(err("minutes must be exactly 2 digits"));
355 }
356 let minutes: u8 = parse_digits_u8(minutes_raw, "minutes", &err)?;
357 if minutes > 59 {
358 return Err(err("minutes must be in [0, 59]"));
359 }
360
361 let secs_part = parts[2];
363 let (secs_str, fraction): (&str, Option<String>) = if let Some(dot_pos) = secs_part.find('.') {
364 let (s, f) = secs_part.split_at(dot_pos);
365 let frac = &f[1..]; if frac.is_empty() || !frac.chars().all(|c| c.is_ascii_digit()) {
367 return Err(err("fractional seconds must be digits"));
368 }
369 (s, Some(frac.to_string()))
370 } else {
371 (secs_part, None)
372 };
373
374 if secs_str.len() != 2 {
375 return Err(err("seconds must be exactly 2 digits"));
376 }
377 let seconds: u8 = parse_digits_u8(secs_str, "seconds", &err)?;
378 if seconds > 60 {
379 return Err(err("seconds must be in [0, 60]"));
380 }
381
382 if parts.len() == 4 {
383 let frames_part = parts[3];
385
386 if ctx.time_base == TimeBase::Clock {
388 return Err(err("frames term is an error when timeBase is clock"));
389 }
390
391 let (frames_str, sub_frames): (&str, Option<u32>) =
392 if let Some(dot_pos) = frames_part.find('.') {
393 let (f, sf) = frames_part.split_at(dot_pos);
394 let sf_str = &sf[1..];
395 if sf_str.is_empty() || !sf_str.chars().all(|c| c.is_ascii_digit()) {
396 return Err(err("sub-frames must be digits"));
397 }
398 let sf_val: u32 = sf_str
399 .parse()
400 .map_err(|_| err("sub-frames value too large"))?;
401 if ctx.time_base == TimeBase::Clock {
403 return Err(err("sub-frames term is an error when timeBase is clock"));
404 }
405 if ctx.sub_frame_rate > 0 && sf_val >= ctx.sub_frame_rate {
407 return Err(err(&format!(
408 "sub-frames value {} must be < subFrameRate {}",
409 sf_val, ctx.sub_frame_rate
410 )));
411 }
412 (f, Some(sf_val))
413 } else {
414 (frames_part, None)
415 };
416
417 let frames: u32 = parse_digits(frames_str, "frames", &err)?;
418 let effective_frame_rate = if ctx.frame_rate > 0 {
420 ctx.frame_rate
421 } else {
422 30
423 };
424 if frames >= effective_frame_rate {
425 return Err(err(&format!(
426 "frames value {} must be < frameRate {}",
427 frames, effective_frame_rate
428 )));
429 }
430
431 Ok(TimeExpression::ClockTime {
432 hours,
433 minutes,
434 seconds,
435 fraction,
436 frames: Some(frames),
437 sub_frames,
438 })
439 } else {
440 Ok(TimeExpression::ClockTime {
441 hours,
442 minutes,
443 seconds,
444 fraction,
445 frames: None,
446 sub_frames: None,
447 })
448 }
449}
450
451fn parse_offset_time(
452 input: &str,
453 _ctx: &TimeContext,
454) -> Result<TimeExpression, crate::error::Error> {
455 let err = |reason: &str| crate::error::Error::InvalidTimeExpression {
456 value: input.to_string(),
457 reason: reason.into(),
458 };
459
460 let metric = if let Some(stripped) = input.strip_suffix("ms") {
462 (TimeMetric::Ms, stripped)
463 } else if let Some(stripped) = input.strip_suffix('h') {
464 (TimeMetric::H, stripped)
465 } else if let Some(stripped) = input.strip_suffix('m') {
466 (TimeMetric::M, stripped)
467 } else if let Some(stripped) = input.strip_suffix('s') {
468 (TimeMetric::S, stripped)
469 } else if let Some(stripped) = input.strip_suffix('f') {
470 (TimeMetric::F, stripped)
471 } else if let Some(stripped) = input.strip_suffix('t') {
472 (TimeMetric::T, stripped)
473 } else {
474 return Err(err(
475 "offset-time must end with a metric: h, m, s, ms, f, or t",
476 ));
477 };
478
479 let num_str = metric.1;
480 if num_str.is_empty() {
481 return Err(err(
482 "offset-time must have a numeric count before the metric",
483 ));
484 }
485
486 let (count_str, fraction): (&str, Option<String>) = if let Some(dot_pos) = num_str.find('.') {
487 let (c, f) = num_str.split_at(dot_pos);
488 let frac = &f[1..];
489 if frac.is_empty() || !frac.chars().all(|c| c.is_ascii_digit()) {
490 return Err(err("fractional part must be digits"));
491 }
492 (c, Some(frac.to_string()))
493 } else {
494 (num_str, None)
495 };
496
497 if count_str.is_empty() || !count_str.chars().all(|c| c.is_ascii_digit()) {
498 return Err(err("count part must be digits"));
499 }
500
501 let count: u64 = count_str
502 .parse()
503 .map_err(|_| err("count value too large"))?;
504
505 Ok(TimeExpression::OffsetTime {
506 count,
507 fraction,
508 metric: metric.0,
509 })
510}
511
512fn parse_wallclock_time(
513 input: &str,
514 ctx: &TimeContext,
515) -> Result<TimeExpression, crate::error::Error> {
516 let err = |reason: &str| crate::error::Error::InvalidTimeExpression {
517 value: input.to_string(),
518 reason: reason.into(),
519 };
520
521 if ctx.time_base != TimeBase::Clock {
523 return Err(err("wallclock-time is an error when timeBase is not clock"));
524 }
525
526 let rest = &input["wallclock(".len()..];
528 let rest = rest.trim_start(); let rest = if let Some(close_pos) = rest.rfind(')') {
530 rest[..close_pos].trim_end() } else {
532 return Err(err("wallclock-time missing closing parenthesis"));
533 };
534
535 let rest = rest.trim();
536
537 if rest.is_empty() {
538 return Err(err("wallclock-time has no content"));
539 }
540
541 if rest.contains('T') {
543 parse_wallclock_datetime(rest, &err)
545 } else if rest.contains('-') {
546 parse_wallclock_date(rest, &err)
548 } else {
549 parse_wallclock_walltime(rest, &err)
551 }
552}
553
554fn parse_wallclock_datetime(
555 input: &str,
556 err: &impl Fn(&str) -> crate::error::Error,
557) -> Result<TimeExpression, crate::error::Error> {
558 let parts: Vec<&str> = input.split('T').collect();
559 if parts.len() != 2 {
560 return Err(err("date-time must have exactly one 'T' separator"));
561 }
562
563 let date_part = parts[0];
564 let time_part = parts[1];
565
566 let date_components: Vec<&str> = date_part.split('-').collect();
568 if date_components.len() != 3 {
569 return Err(err("date must have exactly 3 components: YYYY-MM-DD"));
570 }
571
572 if date_components[0].len() != 4 {
573 return Err(err("years must be exactly 4 digits"));
574 }
575 let years: u16 = date_components[0]
576 .parse()
577 .map_err(|_| err("invalid years"))?;
578
579 if date_components[1].len() != 2 {
580 return Err(err("months must be exactly 2 digits"));
581 }
582 let months: u8 = date_components[1]
583 .parse()
584 .map_err(|_| err("invalid months"))?;
585 if !(1..=12).contains(&months) {
586 return Err(err("months must be in [1, 12]"));
587 }
588
589 if date_components[2].len() != 2 {
590 return Err(err("days must be exactly 2 digits"));
591 }
592 let days: u8 = date_components[2]
593 .parse()
594 .map_err(|_| err("invalid days"))?;
595 if !(1..=31).contains(&days) {
596 return Err(err("days must be in [1, 31]"));
597 }
598
599 let (hours, minutes, seconds, fraction) = parse_wallclock_time_components(time_part, err)?;
601
602 Ok(TimeExpression::WallclockTime {
603 form: WallclockForm::DateTime {
604 years,
605 months,
606 days,
607 hours,
608 minutes,
609 seconds: seconds.unwrap_or(0),
610 fraction,
611 },
612 })
613}
614
615fn parse_wallclock_date(
616 input: &str,
617 err: &impl Fn(&str) -> crate::error::Error,
618) -> Result<TimeExpression, crate::error::Error> {
619 let date_components: Vec<&str> = input.split('-').collect();
620 if date_components.len() != 3 {
621 return Err(err("date must have exactly 3 components: YYYY-MM-DD"));
622 }
623
624 if date_components[0].len() != 4 {
625 return Err(err("years must be exactly 4 digits"));
626 }
627 let years: u16 = date_components[0]
628 .parse()
629 .map_err(|_| err("invalid years"))?;
630
631 if date_components[1].len() != 2 {
632 return Err(err("months must be exactly 2 digits"));
633 }
634 let months: u8 = date_components[1]
635 .parse()
636 .map_err(|_| err("invalid months"))?;
637 if !(1..=12).contains(&months) {
638 return Err(err("months must be in [1, 12]"));
639 }
640
641 if date_components[2].len() != 2 {
642 return Err(err("days must be exactly 2 digits"));
643 }
644 let days: u8 = date_components[2]
645 .parse()
646 .map_err(|_| err("invalid days"))?;
647 if !(1..=31).contains(&days) {
648 return Err(err("days must be in [1, 31]"));
649 }
650
651 Ok(TimeExpression::WallclockTime {
652 form: WallclockForm::Date {
653 years,
654 months,
655 days,
656 },
657 })
658}
659
660fn parse_wallclock_walltime(
661 input: &str,
662 err: &impl Fn(&str) -> crate::error::Error,
663) -> Result<TimeExpression, crate::error::Error> {
664 let (hours, minutes, seconds, fraction) = parse_wallclock_time_components(input, err)?;
665
666 Ok(TimeExpression::WallclockTime {
667 form: WallclockForm::WallTime {
668 hours,
669 minutes,
670 seconds,
671 fraction,
672 },
673 })
674}
675
676fn parse_wallclock_time_components(
677 input: &str,
678 err: &impl Fn(&str) -> crate::error::Error,
679) -> Result<(u8, u8, Option<u8>, Option<String>), crate::error::Error> {
680 let parts: Vec<&str> = input.split(':').collect();
681
682 if parts.len() < 2 || parts.len() > 3 {
683 return Err(err(
684 "wallclock time must have 2 or 3 colon-separated components",
685 ));
686 }
687
688 if parts[0].len() != 2 {
689 return Err(err("wallclock hours must be exactly 2 digits"));
690 }
691 let hours: u8 = parts[0].parse().map_err(|_| err("invalid hours"))?;
692 if hours > 23 {
693 return Err(err("wallclock hours must be in [0, 23]"));
694 }
695
696 if parts[1].len() != 2 {
697 return Err(err("wallclock minutes must be exactly 2 digits"));
698 }
699 let minutes: u8 = parts[1].parse().map_err(|_| err("invalid minutes"))?;
700 if minutes > 59 {
701 return Err(err("wallclock minutes must be in [0, 59]"));
702 }
703
704 if parts.len() == 3 {
705 let (secs_str, fraction) = if let Some(dot_pos) = parts[2].find('.') {
706 let (s, f) = parts[2].split_at(dot_pos);
707 let frac = &f[1..];
708 if frac.is_empty() || !frac.chars().all(|c| c.is_ascii_digit()) {
709 return Err(err("fractional seconds must be digits"));
710 }
711 (s, Some(frac.to_string()))
712 } else {
713 (parts[2], None)
714 };
715
716 if secs_str.len() != 2 {
717 return Err(err("wallclock seconds must be exactly 2 digits"));
718 }
719 let seconds: u8 = secs_str.parse().map_err(|_| err("invalid seconds"))?;
720 if seconds > 60 {
721 return Err(err("wallclock seconds must be in [0, 60]"));
722 }
723
724 Ok((hours, minutes, Some(seconds), fraction))
725 } else {
726 Ok((hours, minutes, None, None))
727 }
728}
729
730fn parse_digits<T: core::str::FromStr>(
731 s: &str,
732 name: &str,
733 err: &impl Fn(&str) -> crate::error::Error,
734) -> Result<T, crate::error::Error> {
735 if !s.chars().all(|c| c.is_ascii_digit()) {
736 return Err(err(&format!("{name} must be digits")));
737 }
738 s.parse::<T>()
739 .map_err(|_| err(&format!("{name} value too large")))
740}
741
742fn parse_digits_u8(
743 s: &str,
744 name: &str,
745 err: &impl Fn(&str) -> crate::error::Error,
746) -> Result<u8, crate::error::Error> {
747 parse_digits::<u8>(s, name, err)
748}
749
750pub fn format_time_expression(expr: &TimeExpression) -> String {
755 match expr {
756 TimeExpression::ClockTime {
757 hours,
758 minutes,
759 seconds,
760 fraction,
761 frames,
762 sub_frames,
763 } => {
764 let mut s = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
765 if let Some(frac) = fraction {
766 s.push('.');
767 s.push_str(frac);
768 }
769 if let Some(frames) = frames {
770 s.push(':');
771 s.push_str(&format!("{:02}", frames));
773 if let Some(sf) = sub_frames {
774 s.push('.');
775 s.push_str(&sf.to_string());
776 }
777 }
778 s
779 }
780 TimeExpression::OffsetTime {
781 count,
782 fraction,
783 metric,
784 } => {
785 let mut s = count.to_string();
786 if let Some(frac) = fraction {
787 s.push('.');
788 s.push_str(frac);
789 }
790 s.push_str(metric.name());
791 s
792 }
793 TimeExpression::WallclockTime { form } => {
794 let inner = match form {
795 WallclockForm::DateTime {
796 years,
797 months,
798 days,
799 hours,
800 minutes,
801 seconds,
802 fraction,
803 } => {
804 let mut s = format!(
805 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
806 years, months, days, hours, minutes, seconds
807 );
808 if let Some(frac) = fraction {
809 s.push('.');
810 s.push_str(frac);
811 }
812 s
813 }
814 WallclockForm::WallTime {
815 hours,
816 minutes,
817 seconds,
818 fraction,
819 } => {
820 let mut s = format!("{:02}:{:02}", hours, minutes);
821 if let Some(secs) = seconds {
822 s.push(':');
823 s.push_str(&format!("{:02}", secs));
824 }
825 if let Some(frac) = fraction {
826 s.push('.');
827 s.push_str(frac);
828 }
829 s
830 }
831 WallclockForm::Date {
832 years,
833 months,
834 days,
835 } => {
836 format!("{:04}-{:02}-{:02}", years, months, days)
837 }
838 };
839 format!("wallclock({})", inner)
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 fn default_ctx() -> TimeContext {
849 TimeContext::default()
850 }
851
852 #[test]
853 fn test_offset_seconds() {
854 let expr = parse_time_expression("0s", &default_ctx()).unwrap();
855 assert_eq!(
856 expr,
857 TimeExpression::OffsetTime {
858 count: 0,
859 fraction: None,
860 metric: TimeMetric::S,
861 }
862 );
863 assert_eq!(format_time_expression(&expr), "0s");
864 }
865
866 #[test]
867 fn test_offset_fractional() {
868 let expr = parse_time_expression("1.2s", &default_ctx()).unwrap();
869 assert_eq!(
870 expr,
871 TimeExpression::OffsetTime {
872 count: 1,
873 fraction: Some("2".into()),
874 metric: TimeMetric::S,
875 }
876 );
877 assert_eq!(format_time_expression(&expr), "1.2s");
878 }
879
880 #[test]
881 fn test_offset_minutes() {
882 let expr = parse_time_expression("1.2m", &default_ctx()).unwrap();
883 assert_eq!(format_time_expression(&expr), "1.2m");
884 }
885
886 #[test]
887 fn test_offset_hours() {
888 let expr = parse_time_expression("1.2h", &default_ctx()).unwrap();
889 assert_eq!(format_time_expression(&expr), "1.2h");
890 }
891
892 #[test]
893 fn test_offset_frames() {
894 let expr = parse_time_expression("24f", &default_ctx()).unwrap();
895 assert_eq!(
896 expr,
897 TimeExpression::OffsetTime {
898 count: 24,
899 fraction: None,
900 metric: TimeMetric::F,
901 }
902 );
903 assert_eq!(format_time_expression(&expr), "24f");
904 }
905
906 #[test]
907 fn test_offset_ticks() {
908 let expr = parse_time_expression("120t", &default_ctx()).unwrap();
909 assert_eq!(format_time_expression(&expr), "120t");
910 }
911
912 #[test]
913 fn test_clock_time_simple() {
914 let expr = parse_time_expression("01:02:03", &default_ctx()).unwrap();
915 assert_eq!(
916 expr,
917 TimeExpression::ClockTime {
918 hours: 1,
919 minutes: 2,
920 seconds: 3,
921 fraction: None,
922 frames: None,
923 sub_frames: None,
924 }
925 );
926 assert_eq!(format_time_expression(&expr), "01:02:03");
927 }
928
929 #[test]
930 fn test_clock_time_fraction() {
931 let expr = parse_time_expression("01:02:03.235", &default_ctx()).unwrap();
932 if let TimeExpression::ClockTime { fraction, .. } = &expr {
933 assert_eq!(fraction.as_deref(), Some("235"));
934 } else {
935 panic!("expected ClockTime");
936 }
937 assert_eq!(format_time_expression(&expr), "01:02:03.235");
938 }
939
940 #[test]
941 fn test_clock_time_with_frames() {
942 let expr = parse_time_expression("01:02:03:20", &default_ctx()).unwrap();
943 if let TimeExpression::ClockTime { frames, .. } = &expr {
944 assert_eq!(*frames, Some(20));
945 } else {
946 panic!("expected ClockTime");
947 }
948 assert_eq!(format_time_expression(&expr), "01:02:03:20");
949 }
950
951 #[test]
952 fn test_clock_time_large_hours() {
953 let expr = parse_time_expression("100:00:00.1", &default_ctx()).unwrap();
954 let formatted = format_time_expression(&expr);
955 assert!(formatted.starts_with("100:00:00"));
957 }
958
959 #[test]
960 fn test_clock_time_with_frame_subframes() {
961 let ctx = TimeContext {
962 frame_rate: 24,
963 sub_frame_rate: 10,
964 ..default_ctx()
965 };
966 let expr = parse_time_expression("01:02:03:20.5", &ctx).unwrap();
967 if let TimeExpression::ClockTime {
968 frames, sub_frames, ..
969 } = &expr
970 {
971 assert_eq!(*frames, Some(20));
972 assert_eq!(*sub_frames, Some(5));
973 } else {
974 panic!("expected ClockTime");
975 }
976 assert_eq!(format_time_expression(&expr), "01:02:03:20.5");
978 }
979
980 #[test]
981 fn test_frame_rate_validation() {
982 let ctx = TimeContext {
983 frame_rate: 24,
984 ..default_ctx()
985 };
986 parse_time_expression("01:02:03:23", &ctx).unwrap();
988 assert!(parse_time_expression("01:02:03:24", &ctx).is_err());
992 }
993
994 #[test]
995 fn test_subframe_rate_validation() {
996 let ctx = TimeContext {
997 frame_rate: 24,
998 sub_frame_rate: 10,
999 ..default_ctx()
1000 };
1001 parse_time_expression("01:02:03:20.9", &ctx).unwrap();
1003 assert!(parse_time_expression("01:02:03:20.10", &ctx).is_err());
1005 }
1006
1007 #[test]
1008 fn test_frames_error_on_clock_timebase() {
1009 let ctx = TimeContext {
1010 time_base: TimeBase::Clock,
1011 ..default_ctx()
1012 };
1013 assert!(parse_time_expression("01:02:03:20", &ctx).is_err());
1015 }
1016
1017 #[test]
1018 fn test_negative_cases() {
1019 assert!(parse_time_expression("", &default_ctx()).is_err());
1021 assert!(parse_time_expression("123", &default_ctx()).is_err());
1023 assert!(parse_time_expression("00:60:00", &default_ctx()).is_err());
1025 assert!(parse_time_expression("00:00:61", &default_ctx()).is_err());
1027 assert!(parse_time_expression("0:00:00", &default_ctx()).is_err());
1029 assert!(parse_time_expression("s", &default_ctx()).is_err());
1031 }
1032
1033 #[test]
1034 fn test_wallclock_error_on_non_clock_timebase() {
1035 let ctx = TimeContext {
1036 time_base: TimeBase::Media,
1037 ..default_ctx()
1038 };
1039 assert!(parse_time_expression("wallclock(2024-01-01T00:00:00)", &ctx).is_err());
1040 }
1041
1042 #[test]
1043 fn test_milliseconds() {
1044 let expr = parse_time_expression("500ms", &default_ctx()).unwrap();
1045 if let TimeExpression::OffsetTime { count, metric, .. } = &expr {
1046 assert_eq!(*count, 500);
1047 assert_eq!(*metric, TimeMetric::Ms);
1048 } else {
1049 panic!("expected OffsetTime");
1050 }
1051 assert_eq!(format_time_expression(&expr), "500ms");
1052 }
1053
1054 #[test]
1055 fn test_round_trip_all_fixture_expressions() {
1056 let expressions = vec![
1057 "0s",
1058 "1.2s",
1059 "1.2m",
1060 "1.2h",
1061 "24f",
1062 "120t",
1063 "01:02:03",
1064 "01:02:03.235",
1065 "01:02:03.2350",
1066 "01:02:03:20",
1067 "100:00:00.1",
1068 "100:00:00:00",
1069 "00:00:00.000",
1070 "00:00:10.000",
1071 "1s",
1072 "5s",
1073 "6s",
1074 "9s",
1075 "10s",
1076 "20s",
1077 ];
1078
1079 for expr_str in &expressions {
1080 let parsed = parse_time_expression(expr_str, &default_ctx())
1081 .unwrap_or_else(|e| panic!("failed to parse '{expr_str}': {e}"));
1082 let formatted = format_time_expression(&parsed);
1083 let re_parsed = parse_time_expression(&formatted, &default_ctx())
1085 .unwrap_or_else(|e| panic!("failed to re-parse '{formatted}': {e}"));
1086 assert_eq!(
1087 parsed, re_parsed,
1088 "round-trip failed for '{expr_str}' -> '{formatted}'"
1089 );
1090 }
1091 }
1092}