vtc/
source_seconds.rs

1use crate::{timecode_parse::round_seconds_to_frame, Framerate, TimecodeParseError};
2use core::result::Result;
3use core::result::Result::Ok;
4use num::Rational32;
5use num::{FromPrimitive, Rational64};
6use regex::Match;
7
8use crate::consts::{RUNTIME_REGEX, SECONDS_PER_HOUR_I64, SECONDS_PER_MINUTE_I64};
9use crate::timecode_parse::convert_tc_int;
10use std::fmt::Debug;
11
12/// The result type of [SecondsSource::to_seconds].
13pub type SecondsSourceResult = Result<num::Rational64, TimecodeParseError>;
14
15/// Types implementing this trait can be converted into the number of real-world seconds that
16/// have elapsed since a timecode value of 00:00:00:00.
17pub trait SecondsSource: Debug {
18    /// Returns the number of real-world seconds this value represents.
19    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult;
20}
21
22impl SecondsSource for &dyn SecondsSource {
23    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult {
24        (*self).to_seconds(rate)
25    }
26}
27
28impl<T> SecondsSource for &T
29where
30    T: SecondsSource,
31{
32    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult {
33        (*self).to_seconds(rate)
34    }
35}
36
37/// Types implementing this trait can be converted into the number of real-world seconds that have
38/// elapsed since a timecode value of 00:00:00:00.
39impl SecondsSource for num::Rational64 {
40    fn to_seconds(&self, _: Framerate) -> SecondsSourceResult {
41        Ok(*self)
42    }
43}
44
45impl SecondsSource for f64 {
46    fn to_seconds(&self, _: Framerate) -> SecondsSourceResult {
47        // Floats are tricky, as they can often result in rational values which try to
48        // capture their imprecision using every bit available in the numerator and
49        // denominator integer values.
50        //
51        // For this reason, we are going to first parse as a Rational32, then upgrade to
52        // a Rational64. This will give operations down the line which need to multiply
53        // and divide by the frame rate plenty of room to do so without running into an
54        // overflow.
55        let rat32 = match Rational32::from_f64(*self) {
56            None => {
57                return Err(TimecodeParseError::Conversion(
58                    "could not convert f64 to Rational64".to_string(),
59                ))
60            }
61            Some(parsed) => parsed,
62        };
63
64        Ok(Rational64::new(
65            *rat32.numer() as i64,
66            *rat32.denom() as i64,
67        ))
68    }
69}
70
71impl SecondsSource for f32 {
72    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult {
73        // Cast to an f64 then use the f64 conversion.
74        f64::from(*self).to_seconds(rate)
75    }
76}
77
78impl SecondsSource for &str {
79    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult {
80        if let Some(matched) = RUNTIME_REGEX.captures(self) {
81            return parse_runtime_str(matched, rate);
82        }
83
84        Err(TimecodeParseError::UnknownStrFormat(format!(
85            "{} is not a known seconds timecode format",
86            self
87        )))
88    }
89}
90
91impl SecondsSource for String {
92    fn to_seconds(&self, rate: Framerate) -> SecondsSourceResult {
93        self.as_str().to_seconds(rate)
94    }
95}
96
97fn parse_runtime_str(matched: regex::Captures, rate: Framerate) -> SecondsSourceResult {
98    // The whole goal of this conversion will be to convert the runtime string to a rational
99    // representation of it's seconds count, then use the implementation on Rational64 to finish
100    // our conversion.
101
102    // We need to figure out how many other sections were present. We'll put them into this vec.
103    let mut sections: Vec<Match> = Vec::new();
104    if let Some(section) = matched.name("section1") {
105        sections.push(section);
106    };
107    if let Some(section) = matched.name("section2") {
108        sections.push(section);
109    };
110
111    // Get whether this value was a negative timecode value.
112    let is_negative = matched.name("negative").is_some();
113
114    let minutes: i64 = match sections.pop() {
115        None => 0,
116        Some(section) => convert_tc_int(section.as_str(), "minutes")?,
117    };
118
119    let hours: i64 = match sections.pop() {
120        None => 0,
121        Some(section) => convert_tc_int(section.as_str(), "frames")?,
122    };
123
124    // We know this group MUST be present on a match, so we can unwrap this;
125    let seconds_str = matched.name("seconds").unwrap().as_str();
126    let seconds_split = seconds_str.split('.').collect::<Vec<&str>>();
127
128    // Get the whole seconds and use it to calculate our total non-fractal seconds.
129    let mut seconds = convert_tc_int(seconds_split[0], "seconds")?;
130    seconds += hours * SECONDS_PER_HOUR_I64 + minutes * SECONDS_PER_MINUTE_I64;
131
132    // Next we need to convert the fractal, which may or may not be blank, into a float. We want
133    // to convert the fractal and not the whole seconds value as the smaller a float value is, the
134    // more accurate it is as well.
135    let maybe_fractal = seconds_split.get(1);
136    let seconds_fractal_str = if let Some(seconds_fractal_str) = maybe_fractal {
137        let mut fixed_fractal = "0.".to_string();
138        fixed_fractal.push_str(seconds_fractal_str);
139        fixed_fractal
140    } else {
141        "0.0".to_string()
142    };
143
144    // Now parse the fractal as a float.
145    let seconds_fractal = match seconds_fractal_str.parse::<f64>() {
146        Ok(parsed) => parsed,
147        Err(err) => {
148            return Err(TimecodeParseError::Conversion(format!(
149                "error conversion seconds of runtime to f64: {}",
150                err
151            )))
152        }
153    };
154
155    // And transform it to a rational value. We are going to use a Rational32 here, then
156    // cast it to a Rational64 so if we have a float which parses to a rational value
157    // which would fill up the entire integer bits to be as precise as possible, we
158    // don't cause an overfow when we add it to the seconds value.
159    let seconds_fractal_rat32 = match Rational32::from_f64(seconds_fractal) {
160        None => {
161            return Err(TimecodeParseError::Conversion(
162                "error conversion fractal seconds of runtime to rational".to_string(),
163            ))
164        }
165        Some(parsed) => parsed,
166    };
167
168    let mut seconds_fractal_rat64 = Rational64::new(
169        *seconds_fractal_rat32.numer() as i64,
170        *seconds_fractal_rat32.denom() as i64,
171    );
172
173    // We're still in danger of getting an overflow here with large numbers that could have complex
174    // time bases, so before we add the fractal seconds to our whole seconds, we're going to bring
175    // the fractal value into the corrct base, THEN add it.
176    seconds_fractal_rat64 = round_seconds_to_frame(seconds_fractal_rat64, rate);
177
178    // Which we can combine with the integer-calculated seconds to get a full rational
179    // value of our seconds.
180    let mut seconds_rat = Rational64::from_integer(seconds) + seconds_fractal_rat64;
181    if is_negative {
182        seconds_rat = -seconds_rat
183    }
184
185    // Finally, convert using the rational implementation on out seconds.
186    seconds_rat.to_seconds(rate)
187}