1use std::{cmp::Ordering, fmt, str::FromStr};
4
5use crate::{Error, ErrorKind, Result};
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct RationalRate {
10 numerator: u32,
11 denominator: u32,
12}
13
14impl RationalRate {
15 pub fn new(numerator: u32, denominator: u32) -> Result<Self> {
21 if numerator == 0 || denominator == 0 {
22 return Err(Error::new(
23 ErrorKind::InvalidArgument,
24 "rational rate components must be greater than zero",
25 ));
26 }
27 let divisor = greatest_common_divisor(numerator, denominator);
28 Ok(Self {
29 numerator: numerator / divisor,
30 denominator: denominator / divisor,
31 })
32 }
33
34 #[must_use]
36 pub const fn numerator(self) -> u32 {
37 self.numerator
38 }
39
40 #[must_use]
42 pub const fn denominator(self) -> u32 {
43 self.denominator
44 }
45}
46
47impl fmt::Display for RationalRate {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(formatter, "{}/{}", self.numerator, self.denominator)
51 }
52}
53
54impl FromStr for RationalRate {
55 type Err = Error;
56
57 fn from_str(value: &str) -> Result<Self> {
59 let (numerator, denominator) = value.split_once('/').ok_or_else(|| {
60 Error::new(
61 ErrorKind::InvalidArgument,
62 "rational rate must use numerator/denominator syntax",
63 )
64 })?;
65 let numerator = numerator.parse::<u32>().map_err(|_| {
66 Error::new(
67 ErrorKind::InvalidArgument,
68 "rational rate numerator must be an unsigned integer",
69 )
70 })?;
71 let denominator = denominator.parse::<u32>().map_err(|_| {
72 Error::new(
73 ErrorKind::InvalidArgument,
74 "rational rate denominator must be an unsigned integer",
75 )
76 })?;
77 Self::new(numerator, denominator)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83pub struct RationalTime {
84 value: i64,
85 rate: RationalRate,
86}
87
88impl RationalTime {
89 #[must_use]
91 pub const fn new(value: i64, rate: RationalRate) -> Self {
92 Self { value, rate }
93 }
94
95 #[must_use]
97 pub const fn value(self) -> i64 {
98 self.value
99 }
100
101 #[must_use]
103 pub const fn rate(self) -> RationalRate {
104 self.rate
105 }
106
107 pub fn checked_cmp(self, other: Self) -> Result<Ordering> {
114 let left = comparison_product(self.value, self.rate.denominator, other.rate.numerator)?;
115 let right = comparison_product(other.value, other.rate.denominator, self.rate.numerator)?;
116 Ok(left.cmp(&right))
117 }
118
119 pub fn equivalent(self, other: Self) -> Result<bool> {
125 Ok(self.checked_cmp(other)? == Ordering::Equal)
126 }
127
128 pub fn rescaled_to(self, rate: RationalRate) -> Result<Self> {
135 let numerator = i128::from(self.value)
136 .checked_mul(i128::from(rate.numerator))
137 .and_then(|value| value.checked_mul(i128::from(self.rate.denominator)))
138 .ok_or_else(arithmetic_overflow)?;
139 let denominator = i128::from(self.rate.numerator)
140 .checked_mul(i128::from(rate.denominator))
141 .ok_or_else(arithmetic_overflow)?;
142 if numerator % denominator != 0 {
143 return Err(Error::new(
144 ErrorKind::InvalidArgument,
145 "rational time is not exactly representable at the target rate",
146 ));
147 }
148 let value = i64::try_from(numerator / denominator).map_err(|_| arithmetic_overflow())?;
149 Ok(Self::new(value, rate))
150 }
151}
152
153impl fmt::Display for RationalTime {
154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(formatter, "{}@{}", self.value, self.rate)
157 }
158}
159
160impl FromStr for RationalTime {
161 type Err = Error;
162
163 fn from_str(value: &str) -> Result<Self> {
165 let (value, rate) = value.split_once('@').ok_or_else(|| {
166 Error::new(
167 ErrorKind::InvalidArgument,
168 "rational time must use value@numerator/denominator syntax",
169 )
170 })?;
171 let value = value.parse::<i64>().map_err(|_| {
172 Error::new(
173 ErrorKind::InvalidArgument,
174 "rational time value must be a signed integer",
175 )
176 })?;
177 Ok(Self::new(value, rate.parse()?))
178 }
179}
180
181#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
183pub struct TimeRange {
184 start: RationalTime,
185 duration: RationalTime,
186}
187
188impl TimeRange {
189 pub fn new(start: RationalTime, duration: RationalTime) -> Result<Self> {
196 if duration.value() < 0 {
197 return Err(Error::new(
198 ErrorKind::InvalidArgument,
199 "time range duration must not be negative",
200 ));
201 }
202 if start.rate() != duration.rate() {
203 return Err(Error::new(
204 ErrorKind::InvalidArgument,
205 "time range start and duration must use the same rate",
206 ));
207 }
208 Ok(Self { start, duration })
209 }
210
211 #[must_use]
213 pub const fn start(self) -> RationalTime {
214 self.start
215 }
216
217 #[must_use]
219 pub const fn duration(self) -> RationalTime {
220 self.duration
221 }
222
223 pub fn end_exclusive(self) -> Result<RationalTime> {
230 let value = self
231 .start
232 .value()
233 .checked_add(self.duration.value())
234 .ok_or_else(arithmetic_overflow)?;
235 Ok(RationalTime::new(value, self.start.rate()))
236 }
237}
238
239impl fmt::Display for TimeRange {
240 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(formatter, "{}+{}", self.start, self.duration)
243 }
244}
245
246impl FromStr for TimeRange {
247 type Err = Error;
248
249 fn from_str(value: &str) -> Result<Self> {
251 let (start, duration) = value.split_once('+').ok_or_else(|| {
252 Error::new(
253 ErrorKind::InvalidArgument,
254 "time range must use start+duration syntax",
255 )
256 })?;
257 Self::new(start.parse()?, duration.parse()?)
258 }
259}
260
261fn comparison_product(value: i64, denominator: u32, other_numerator: u32) -> Result<i128> {
262 i128::from(value)
263 .checked_mul(i128::from(denominator))
264 .and_then(|product| product.checked_mul(i128::from(other_numerator)))
265 .ok_or_else(arithmetic_overflow)
266}
267
268fn arithmetic_overflow() -> Error {
269 Error::new(
270 ErrorKind::InvalidArgument,
271 "rational time arithmetic overflow",
272 )
273}
274
275const fn greatest_common_divisor(mut left: u32, mut right: u32) -> u32 {
276 while right != 0 {
277 let remainder = left % right;
278 left = right;
279 right = remainder;
280 }
281 left
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn rate_is_positive_and_normalized() {
290 let rate = RationalRate::new(60_000, 2_002).expect("valid rate");
291
292 assert_eq!(rate.numerator(), 30_000);
293 assert_eq!(rate.denominator(), 1_001);
294 assert!(RationalRate::new(0, 1).is_err());
295 assert!(RationalRate::new(24, 0).is_err());
296 }
297
298 #[test]
299 fn range_requires_one_rate_and_non_negative_duration() {
300 let film = RationalRate::new(24, 1).expect("valid rate");
301 let video = RationalRate::new(25, 1).expect("valid rate");
302 let start = RationalTime::new(100, film);
303
304 assert!(TimeRange::new(start, RationalTime::new(48, film)).is_ok());
305 assert!(TimeRange::new(start, RationalTime::new(-1, film)).is_err());
306 assert!(TimeRange::new(start, RationalTime::new(50, video)).is_err());
307 }
308
309 #[test]
310 fn comparison_and_rescaling_are_exact_across_rates() {
311 let film = RationalRate::new(24, 1).expect("valid rate");
312 let video = RationalRate::new(48, 1).expect("valid rate");
313 let one_second_film = RationalTime::new(24, film);
314 let one_second_video = RationalTime::new(48, video);
315
316 assert!(one_second_film.equivalent(one_second_video).unwrap());
317 assert_eq!(
318 one_second_film.rescaled_to(video).unwrap(),
319 one_second_video
320 );
321 assert!(RationalTime::new(1, film).rescaled_to(video).is_ok());
322 assert!(RationalTime::new(1, video).rescaled_to(film).is_err());
323 }
324
325 #[test]
326 fn checked_time_operations_reject_i64_overflow() {
327 let rate = RationalRate::new(1, 1).expect("valid rate");
328 let faster = RationalRate::new(u32::MAX, 1).expect("valid rate");
329
330 assert!(
331 RationalTime::new(i64::MAX, rate)
332 .rescaled_to(faster)
333 .is_err()
334 );
335 let range = TimeRange::new(
336 RationalTime::new(i64::MAX, rate),
337 RationalTime::new(1, rate),
338 )
339 .expect("valid range");
340 assert!(range.end_exclusive().is_err());
341 }
342
343 #[test]
344 fn canonical_text_round_trips_common_rational_rates() {
345 for text in ["24/1", "25/1", "30000/1001", "60000/1001"] {
346 let rate: RationalRate = text.parse().expect("parse common rate");
347 assert_eq!(rate.to_string(), text);
348 }
349
350 let time: RationalTime = "-1001@30000/1001".parse().expect("parse time");
351 assert_eq!(time.to_string(), "-1001@30000/1001");
352 let range: TimeRange = "-1001@30000/1001+2002@30000/1001"
353 .parse()
354 .expect("parse range");
355 assert_eq!(range.to_string(), "-1001@30000/1001+2002@30000/1001");
356 }
357
358 #[test]
359 fn canonical_text_rejects_ambiguous_or_invalid_values() {
360 assert!("23.976".parse::<RationalRate>().is_err());
361 assert!("1@0/1".parse::<RationalTime>().is_err());
362 assert!("1@24/1+-1@24/1".parse::<TimeRange>().is_err());
363 assert!("1@24/1+1@25/1".parse::<TimeRange>().is_err());
364 }
365}