1use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
4
5pub const TICKS_PER_SECOND: i64 = 705_600_000;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Time(i64);
15
16pub type Duration = Time;
18
19impl Time {
20 pub const ZERO: Self = Self(0);
22
23 pub const fn from_ticks(ticks: i64) -> Self {
25 Self(ticks)
26 }
27
28 pub fn from_seconds(seconds: f64) -> Self {
33 Self::rounded_ticks(seconds * TICKS_PER_SECOND as f64)
34 }
35
36 pub fn from_frame(frame: i64, rate: FrameRate) -> Self {
47 Self::checked_from_frame(frame, rate).expect("frame time exceeds the signed tick range")
48 }
49
50 pub fn checked_from_frame(frame: i64, rate: FrameRate) -> Option<Self> {
53 let numerator =
54 i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator);
55 let denominator = i128::from(rate.numerator);
56 let ticks =
57 numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
58 i64::try_from(ticks).ok().map(Self)
59 }
60
61 pub fn to_frame(self, rate: FrameRate) -> i64 {
66 self.checked_to_frame(rate)
67 .expect("frame index exceeds the signed 64-bit range")
68 }
69
70 pub fn checked_to_frame(self, rate: FrameRate) -> Option<i64> {
73 let numerator = i128::from(self.0) * i128::from(rate.numerator);
74 let denominator = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator);
75 i64::try_from(numerator.div_euclid(denominator)).ok()
76 }
77
78 pub fn as_seconds(self) -> f64 {
80 self.0 as f64 / TICKS_PER_SECOND as f64
81 }
82
83 pub const fn ticks(self) -> i64 {
85 self.0
86 }
87
88 pub fn checked_mul(self, factor: i64) -> Option<Self> {
90 self.0.checked_mul(factor).map(Self)
91 }
92
93 fn rounded_ticks(ticks: f64) -> Self {
94 let rounded = ticks.round();
95 assert!(
96 rounded.is_finite() && rounded >= i64::MIN as f64 && rounded < -(i64::MIN as f64),
97 "time must be finite and fit the signed tick range"
98 );
99 Self(rounded as i64)
100 }
101}
102
103impl Add for Time {
104 type Output = Self;
105
106 fn add(self, other: Self) -> Self {
107 Self(self.0.checked_add(other.0).expect("time addition overflow"))
108 }
109}
110
111impl Sub for Time {
112 type Output = Self;
113
114 fn sub(self, other: Self) -> Self {
115 Self(
116 self.0
117 .checked_sub(other.0)
118 .expect("time subtraction overflow"),
119 )
120 }
121}
122
123impl AddAssign for Time {
124 fn add_assign(&mut self, other: Self) {
125 *self = *self + other;
126 }
127}
128
129impl SubAssign for Time {
130 fn sub_assign(&mut self, other: Self) {
131 *self = *self - other;
132 }
133}
134
135impl Neg for Time {
136 type Output = Self;
137
138 fn neg(self) -> Self {
139 Self(self.0.checked_neg().expect("time negation overflow"))
140 }
141}
142
143impl Mul<f64> for Time {
144 type Output = Self;
145
146 fn mul(self, factor: f64) -> Self {
148 match factor {
149 1.0 => self,
150 -1.0 => -self,
151 _ => Self::rounded_ticks(self.0 as f64 * factor),
152 }
153 }
154}
155
156macro_rules! integer_time_mul {
157 ($($integer:ty),+ $(,)?) => {$ (
158 impl Mul<$integer> for Time {
159 type Output = Time;
160 fn mul(self, factor: $integer) -> Time {
161 Time(i64::try_from(i128::from(self.0) * factor as i128).expect("time multiplication overflow"))
162 }
163 }
164 impl Mul<Time> for $integer {
165 type Output = Time;
166 fn mul(self, time: Time) -> Time { time * self }
167 }
168 )+};
169}
170
171integer_time_mul!(i32, i64, u32, u64, usize);
172
173impl Mul<Time> for f64 {
174 type Output = Time;
175
176 fn mul(self, time: Time) -> Time {
177 time * self
178 }
179}
180
181impl Div<f64> for Time {
182 type Output = Self;
183
184 fn div(self, divisor: f64) -> Self {
186 assert!(
187 divisor.is_finite() && divisor != 0.0,
188 "time divisor must be finite and nonzero"
189 );
190 match divisor {
191 1.0 => self,
192 -1.0 => -self,
193 _ => Self::rounded_ticks(self.0 as f64 / divisor),
194 }
195 }
196}
197
198impl Div for Time {
199 type Output = f64;
200
201 fn div(self, duration: Self) -> f64 {
203 assert!(duration.0 != 0, "time ratio requires a nonzero duration");
204 self.0 as f64 / duration.0 as f64
205 }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
210pub struct FrameRate {
211 numerator: u32,
212 denominator: u32,
213}
214
215impl FrameRate {
216 pub const NTSC_FILM: Self = Self::new(24_000, 1_001);
218
219 pub const NTSC: Self = Self::new(30_000, 1_001);
221
222 pub const NTSC_DOUBLE: Self = Self::new(60_000, 1_001);
224
225 pub const fn new(numerator: u32, denominator: u32) -> Self {
230 assert!(numerator != 0, "frame-rate numerator must be nonzero");
231 assert!(denominator != 0, "frame-rate denominator must be nonzero");
232 let mut common = numerator;
233 let mut remainder = denominator;
234 while remainder != 0 {
235 let next = common % remainder;
236 common = remainder;
237 remainder = next;
238 }
239 Self {
240 numerator: numerator / common,
241 denominator: denominator / common,
242 }
243 }
244
245 pub const fn numerator(self) -> u32 {
247 self.numerator
248 }
249
250 pub const fn denominator(self) -> u32 {
252 self.denominator
253 }
254
255 pub fn frame_duration(self) -> Duration {
258 Time::from_frame(1, self)
259 }
260}
261
262impl From<u32> for FrameRate {
263 fn from(frames_per_second: u32) -> Self {
264 Self::new(frames_per_second, 1)
265 }
266}
267
268pub trait TimeLiteral {
274 fn seconds(self) -> Time;
276
277 fn milliseconds(self) -> Time;
279}
280
281impl TimeLiteral for i64 {
282 fn seconds(self) -> Time {
283 Time(
284 self.checked_mul(TICKS_PER_SECOND)
285 .expect("seconds overflow"),
286 )
287 }
288
289 fn milliseconds(self) -> Time {
290 Time(
291 self.checked_mul(TICKS_PER_SECOND / 1_000)
292 .expect("milliseconds overflow"),
293 )
294 }
295}
296
297impl TimeLiteral for i32 {
298 fn seconds(self) -> Time {
299 i64::from(self).seconds()
300 }
301
302 fn milliseconds(self) -> Time {
303 i64::from(self).milliseconds()
304 }
305}
306
307macro_rules! unsigned_time_literal {
308 ($($integer:ty),+ $(,)?) => {$ (
309 impl TimeLiteral for $integer {
310 fn seconds(self) -> Time { i64::try_from(self).expect("seconds overflow").seconds() }
311 fn milliseconds(self) -> Time { i64::try_from(self).expect("milliseconds overflow").milliseconds() }
312 }
313 )+};
314}
315
316unsigned_time_literal!(u32, u64, usize);
317
318impl TimeLiteral for f64 {
319 fn seconds(self) -> Time {
320 Time::from_seconds(self)
321 }
322
323 fn milliseconds(self) -> Time {
324 Time::rounded_ticks(self * (TICKS_PER_SECOND / 1_000) as f64)
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn unsigned_literals_and_integer_multipliers_preserve_large_tick_precision() {
334 assert_eq!(2_u32.seconds(), 2_i64.seconds());
335 assert_eq!(3_usize.milliseconds(), 3_i64.milliseconds());
336 assert_eq!(4_u64.seconds(), 4_i64.seconds());
337 let large = Time::from_ticks((1_i64 << 53) + 1);
338 assert_eq!((large * 3_i64).ticks(), ((1_i64 << 53) + 1) * 3);
339 assert_eq!((3_u32 * large).ticks(), (large * 3_i64).ticks());
340 assert_eq!((Time::from_ticks(-2) * 4_u64).ticks(), -8);
341 assert_eq!(Time::from_ticks(i64::MAX).checked_mul(2), None);
342 assert!(std::panic::catch_unwind(|| Time::from_ticks(i64::MAX) * 2_u32).is_err());
343 assert!(std::panic::catch_unwind(|| u64::MAX.seconds()).is_err());
344 }
345
346 #[test]
347 fn standard_video_and_audio_intervals_are_exact() {
348 for numerator in [24, 25, 30, 48, 50, 60, 90, 100, 120] {
349 let rate = FrameRate::from(numerator);
350 assert_eq!(
351 rate.frame_duration().ticks() * i64::from(numerator),
352 TICKS_PER_SECOND
353 );
354 }
355 for rate in [
356 FrameRate::NTSC_FILM,
357 FrameRate::NTSC,
358 FrameRate::NTSC_DOUBLE,
359 ] {
360 assert_eq!(
361 i128::from(rate.frame_duration().ticks()) * i128::from(rate.numerator()),
362 i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator())
363 );
364 }
365 for sample_rate in [44_100, 48_000, 96_000] {
366 assert_eq!(TICKS_PER_SECOND % sample_rate, 0);
367 }
368 assert_eq!(FrameRate::new(60_000, 2_002), FrameRate::NTSC);
369 assert_eq!(FrameRate::new(120, 2), FrameRate::from(60));
370 }
371
372 #[test]
373 fn signed_frame_boundaries_round_trip_without_drift() {
374 for rate in [
375 FrameRate::from(24),
376 FrameRate::from(60),
377 FrameRate::NTSC,
378 FrameRate::NTSC_FILM,
379 FrameRate::new(11, 1),
380 FrameRate::new(123_457, 1_001),
381 ] {
382 for frame in [-1_000_000, -101, -1, 0, 1, 101, 1_000_000] {
383 let time = Time::from_frame(frame, rate);
384 assert_eq!(time.to_frame(rate), frame);
385 assert_eq!((time - Time::from_ticks(1)).to_frame(rate), frame - 1);
386 }
387 }
388 let rate = FrameRate::from(11);
389 assert_eq!(Time::from_frame(11, rate), 1.seconds());
390 assert_ne!(rate.frame_duration() * 11.0, 1.seconds());
391 assert_eq!(Time::from_ticks(-1).to_frame(FrameRate::from(30)), -1);
392 let tiny_time = Time::from_frame(i64::MAX, FrameRate::new(u32::MAX, 1));
393 assert!(tiny_time.ticks() > 0);
394 }
395
396 #[test]
397 fn checked_frame_conversions_preserve_rounding_and_reject_overflow() {
398 for rate in [
399 FrameRate::NTSC,
400 FrameRate::new(11, 1),
401 FrameRate::new(123_457, 1_001),
402 ] {
403 for frame in [-1_000_000, -1, 0, 1, 1_000_000] {
404 let time = Time::checked_from_frame(frame, rate).unwrap();
405 assert_eq!(time, Time::from_frame(frame, rate));
406 assert_eq!(time.checked_to_frame(rate), Some(frame));
407 assert_eq!(
408 (time - Time::from_ticks(1)).checked_to_frame(rate),
409 Some(frame - 1)
410 );
411 }
412 }
413 assert_eq!(Time::checked_from_frame(i64::MAX, FrameRate::from(1)), None);
414 assert_eq!(Time::checked_from_frame(i64::MIN, FrameRate::from(1)), None);
415 assert_eq!(
416 Time::from_ticks(i64::MAX).checked_to_frame(FrameRate::new(u32::MAX, 1)),
417 None
418 );
419 assert_eq!(
420 Time::from_ticks(i64::MIN).checked_to_frame(FrameRate::new(u32::MAX, 1)),
421 None
422 );
423 }
424
425 #[test]
426 fn literals_and_arithmetic_share_signed_tick_time() {
427 let duration: Duration = 1.5.seconds();
428 assert_eq!(duration, 1.seconds() + 500.milliseconds());
429 assert_eq!((-250).milliseconds(), -250.milliseconds());
430 assert_eq!(Time::from_seconds(1.0 / 1024.0).ticks(), 689_063);
431 assert_eq!(Time::from_seconds(-1.0 / 1024.0).ticks(), -689_063);
432 assert_eq!((1.0 / 128.0).milliseconds().ticks(), 5_513);
433 assert_eq!(2.0 * duration, 3.seconds());
434 assert_eq!(duration / 2.0, 750.milliseconds());
435 assert_eq!(duration / 500.milliseconds(), 3.0);
436 assert_eq!(duration.as_seconds(), 1.5);
437 let mut time = Time::ZERO;
438 time += duration;
439 time -= 2.seconds();
440 assert_eq!(time, -500.milliseconds());
441 for ticks in [i64::MIN, i64::MAX] {
442 assert_eq!(Time::from_ticks(ticks) * 1.0, Time::from_ticks(ticks));
443 assert_eq!(Time::from_ticks(ticks) / 1.0, Time::from_ticks(ticks));
444 }
445 }
446
447 #[test]
448 fn invalid_inputs_and_overflow_do_not_saturate_or_wrap() {
449 for seconds in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MAX] {
450 assert!(std::panic::catch_unwind(|| Time::from_seconds(seconds)).is_err());
451 }
452 for components in [(0, 1), (1, 0), (0, 0)] {
453 assert!(
454 std::panic::catch_unwind(|| FrameRate::new(components.0, components.1)).is_err()
455 );
456 }
457 let maximum = Time::from_ticks(i64::MAX);
458 let minimum = Time::from_ticks(i64::MIN);
459 assert!(std::panic::catch_unwind(|| maximum + Time::from_ticks(1)).is_err());
460 assert!(std::panic::catch_unwind(|| minimum - Time::from_ticks(1)).is_err());
461 assert!(std::panic::catch_unwind(|| -minimum).is_err());
462 assert!(std::panic::catch_unwind(|| maximum * 2.0).is_err());
463 assert!(std::panic::catch_unwind(|| 1.seconds() * f64::NAN).is_err());
464 assert!(std::panic::catch_unwind(|| 1.seconds() / 0.0).is_err());
465 assert!(std::panic::catch_unwind(|| Time::ZERO / Time::ZERO).is_err());
466 assert!(std::panic::catch_unwind(|| i64::MAX.seconds()).is_err());
467 assert!(std::panic::catch_unwind(|| i64::MAX.milliseconds()).is_err());
468 assert!(
469 std::panic::catch_unwind(|| Time::from_frame(i64::MAX, FrameRate::from(1))).is_err()
470 );
471 assert!(
472 std::panic::catch_unwind(|| maximum.to_frame(FrameRate::new(u32::MAX, 1))).is_err()
473 );
474 }
475}