Skip to main content

motion_core/
time.rs

1//! Integer-tick time points, nonnegative durations, signed offsets and rational frame rates.
2
3use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign};
4
5mod frame_rate;
6mod offset;
7
8pub use frame_rate::FrameRate;
9pub use offset::TimeOffset;
10
11/// Flicks per second, exactly covering common video and audio sample rates.
12pub const TICKS_PER_SECOND: i64 = 705_600_000;
13
14const TICKS_PER_SECOND_UNSIGNED: u64 = TICKS_PER_SECOND as u64;
15
16/// A signed point on the timeline, stored as integer flicks since the origin.
17///
18/// Comparisons and frame conversion use integers. Floating-point seconds are
19/// intended for animation mathematics, not interval membership checks.
20/// Only meaningful operations exist: `Time - Time` is a signed [`TimeOffset`],
21/// and `Time ± Duration` or `Time ± TimeOffset` is a [`Time`]. Arithmetic panics
22/// on overflow, independently of the build profile.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct Time(i64);
25
26/// A nonnegative span of time, stored as integer flicks.
27///
28/// Negative spans cannot be constructed. Use [`TimeOffset`] where a signed
29/// displacement is meaningful, such as shifting a clip earlier. Durations add,
30/// scale and subtract (panicking below zero, see [`Duration::checked_sub`]);
31/// they share the tick unit and overflow behavior of [`Time`].
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Duration(u64);
34
35impl Time {
36    /// The time origin.
37    pub const ZERO: Self = Self(0);
38
39    /// Constructs time without losing integer tick precision.
40    pub const fn from_ticks(ticks: i64) -> Self {
41        Self(ticks)
42    }
43
44    /// Rounds seconds to the nearest tick; halfway cases round away from zero.
45    ///
46    /// # Panics
47    /// Panics for nonfinite input or a rounded result outside the signed range;
48    /// see [`Self::try_from_seconds`].
49    pub fn from_seconds(seconds: f64) -> Self {
50        Self::try_from_seconds(seconds).unwrap_or_else(|error| panic!("{}", error.message))
51    }
52
53    /// Rounds seconds like [`Self::from_seconds`], reporting nonfinite or out-of-range input.
54    pub fn try_from_seconds(seconds: f64) -> Result<Self, crate::ValidationError> {
55        try_rounded_ticks(seconds * TICKS_PER_SECOND as f64).map(Self)
56    }
57
58    /// Returns the first tick at or after the exact rational frame boundary.
59    ///
60    /// Common integer and NTSC rates have exact tick boundaries. Other rates
61    /// round toward positive infinity by less than one tick, including before
62    /// zero. Compute each boundary directly; multiplying a rounded frame duration
63    /// would accumulate drift. Frame indices round-trip through [`Self::to_frame`]
64    /// when the rate does not exceed one frame per tick.
65    ///
66    /// # Panics
67    /// Panics if the boundary cannot be represented as signed ticks.
68    pub fn from_frame(frame: i64, rate: FrameRate) -> Self {
69        Self::checked_from_frame(frame, rate).expect("frame time exceeds the signed tick range")
70    }
71
72    /// Converts a frame boundary with the same ceiling rule as from_frame.
73    /// Returns None instead of panicking when signed ticks would overflow.
74    pub fn checked_from_frame(frame: i64, rate: FrameRate) -> Option<Self> {
75        let numerator =
76            i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
77        let denominator = i128::from(rate.numerator());
78        let ticks =
79            numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
80        i64::try_from(ticks).ok().map(Self)
81    }
82
83    /// Returns the containing frame, rounding toward negative infinity.
84    ///
85    /// # Panics
86    /// Panics if the frame index is outside the signed 64-bit range.
87    pub fn to_frame(self, rate: FrameRate) -> i64 {
88        self.checked_to_frame(rate)
89            .expect("frame index exceeds the signed 64-bit range")
90    }
91
92    /// Finds the containing frame using integer floor division.
93    /// Returns None instead of panicking when the frame index would overflow.
94    pub fn checked_to_frame(self, rate: FrameRate) -> Option<i64> {
95        let numerator = i128::from(self.0) * i128::from(rate.numerator());
96        let denominator = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
97        i64::try_from(numerator.div_euclid(denominator)).ok()
98    }
99
100    /// Converts to floating-point seconds for animation mathematics.
101    pub fn as_seconds(self) -> f64 {
102        self.0 as f64 / TICKS_PER_SECOND as f64
103    }
104
105    /// Returns the exact signed tick count.
106    pub const fn ticks(self) -> i64 {
107        self.0
108    }
109
110    /// Returns the signed displacement from the origin to this time point.
111    pub const fn since_origin(self) -> TimeOffset {
112        TimeOffset::from_ticks(self.0)
113    }
114
115    /// Returns the span since an earlier time point, or None when `earlier` is later.
116    pub fn duration_since(self, earlier: Self) -> Option<Duration> {
117        (self.0 >= earlier.0).then(|| Duration(self.0.abs_diff(earlier.0)))
118    }
119
120    /// Adds a duration, returning None on signed overflow.
121    pub fn checked_add(self, span: Duration) -> Option<Self> {
122        i64::try_from(span.0)
123            .ok()
124            .and_then(|ticks| self.0.checked_add(ticks))
125            .map(Self)
126    }
127
128    /// Subtracts a duration, returning None on signed overflow.
129    pub fn checked_sub(self, span: Duration) -> Option<Self> {
130        i64::try_from(span.0)
131            .ok()
132            .and_then(|ticks| self.0.checked_sub(ticks))
133            .map(Self)
134    }
135}
136
137impl Add<Duration> for Time {
138    type Output = Self;
139
140    fn add(self, span: Duration) -> Self {
141        self.checked_add(span).expect("time addition overflow")
142    }
143}
144
145impl Sub<Duration> for Time {
146    type Output = Self;
147
148    fn sub(self, span: Duration) -> Self {
149        self.checked_sub(span).expect("time subtraction overflow")
150    }
151}
152
153impl Add<TimeOffset> for Time {
154    type Output = Self;
155
156    fn add(self, offset: TimeOffset) -> Self {
157        Self(
158            self.0
159                .checked_add(offset.ticks())
160                .expect("time addition overflow"),
161        )
162    }
163}
164
165impl Sub<TimeOffset> for Time {
166    type Output = Self;
167
168    fn sub(self, offset: TimeOffset) -> Self {
169        Self(
170            self.0
171                .checked_sub(offset.ticks())
172                .expect("time subtraction overflow"),
173        )
174    }
175}
176
177impl Sub for Time {
178    type Output = TimeOffset;
179
180    fn sub(self, other: Self) -> TimeOffset {
181        TimeOffset::from_ticks(
182            self.0
183                .checked_sub(other.0)
184                .expect("time subtraction overflow"),
185        )
186    }
187}
188
189impl AddAssign<Duration> for Time {
190    fn add_assign(&mut self, span: Duration) {
191        *self = *self + span;
192    }
193}
194
195impl SubAssign<Duration> for Time {
196    fn sub_assign(&mut self, span: Duration) {
197        *self = *self - span;
198    }
199}
200
201impl AddAssign<TimeOffset> for Time {
202    fn add_assign(&mut self, offset: TimeOffset) {
203        *self = *self + offset;
204    }
205}
206
207impl SubAssign<TimeOffset> for Time {
208    fn sub_assign(&mut self, offset: TimeOffset) {
209        *self = *self - offset;
210    }
211}
212
213impl Duration {
214    /// The empty duration.
215    pub const ZERO: Self = Self(0);
216
217    /// Constructs a duration without losing integer tick precision.
218    pub const fn from_ticks(ticks: u64) -> Self {
219        Self(ticks)
220    }
221
222    /// Rounds seconds to the nearest tick; halfway cases round away from zero.
223    ///
224    /// # Panics
225    /// Panics for negative or nonfinite input or a rounded result beyond the
226    /// tick range; see [`Self::try_from_seconds`].
227    pub fn from_seconds(seconds: f64) -> Self {
228        Self::try_from_seconds(seconds).unwrap_or_else(|error| panic!("{}", error.message))
229    }
230
231    /// Rounds seconds like [`Self::from_seconds`], reporting negative, nonfinite or out-of-range input.
232    pub fn try_from_seconds(seconds: f64) -> Result<Self, crate::ValidationError> {
233        try_rounded_unsigned_ticks(seconds * TICKS_PER_SECOND as f64).map(Self)
234    }
235
236    /// Returns the duration from the origin to a frame boundary, with the same
237    /// ceiling rule as [`Time::from_frame`].
238    ///
239    /// # Panics
240    /// Panics if the boundary cannot be represented as ticks.
241    pub fn from_frame(frame: u64, rate: FrameRate) -> Self {
242        Self::checked_from_frame(frame, rate).expect("frame time exceeds the tick range")
243    }
244
245    /// Converts a frame boundary like [`Self::from_frame`], returning None on overflow.
246    pub fn checked_from_frame(frame: u64, rate: FrameRate) -> Option<Self> {
247        let numerator = u128::from(frame)
248            * u128::from(TICKS_PER_SECOND_UNSIGNED)
249            * u128::from(rate.denominator());
250        let denominator = u128::from(rate.numerator());
251        let ticks = numerator / denominator + u128::from(numerator % denominator != 0);
252        u64::try_from(ticks).ok().map(Self)
253    }
254
255    /// Returns the frame containing this span's end, rounding down.
256    ///
257    /// # Panics
258    /// Panics if the frame index is outside the unsigned 64-bit range.
259    pub fn to_frame(self, rate: FrameRate) -> u64 {
260        self.checked_to_frame(rate)
261            .expect("frame index exceeds the unsigned 64-bit range")
262    }
263
264    /// Finds the frame count like [`Self::to_frame`], returning None on overflow.
265    pub fn checked_to_frame(self, rate: FrameRate) -> Option<u64> {
266        let numerator = u128::from(self.0) * u128::from(rate.numerator());
267        let denominator = u128::from(TICKS_PER_SECOND_UNSIGNED) * u128::from(rate.denominator());
268        u64::try_from(numerator / denominator).ok()
269    }
270
271    /// Converts to floating-point seconds for animation mathematics.
272    pub fn as_seconds(self) -> f64 {
273        self.0 as f64 / TICKS_PER_SECOND as f64
274    }
275
276    /// Returns the exact tick count.
277    pub const fn ticks(self) -> u64 {
278        self.0
279    }
280
281    /// Returns the time point this far from the origin.
282    ///
283    /// # Panics
284    /// Panics if the duration exceeds the signed tick range.
285    pub fn after_origin(self) -> Time {
286        Time(i64::try_from(self.0).expect("duration exceeds the signed tick range"))
287    }
288
289    /// Returns the signed displacement of this duration, or None beyond the signed tick range.
290    pub fn checked_offset(self) -> Option<TimeOffset> {
291        i64::try_from(self.0).ok().map(TimeOffset::from_ticks)
292    }
293
294    /// Returns the signed displacement of this duration, for shifting time later.
295    ///
296    /// # Panics
297    /// Panics if the duration exceeds the signed tick range; see [`Self::checked_offset`].
298    pub fn offset(self) -> TimeOffset {
299        self.checked_offset()
300            .expect("duration exceeds the signed tick range")
301    }
302
303    /// Subtracts exactly, returning None when `other` is longer.
304    pub fn checked_sub(self, other: Self) -> Option<Self> {
305        self.0.checked_sub(other.0).map(Self)
306    }
307
308    /// Subtracts, stopping at [`Self::ZERO`] when `other` is longer.
309    pub fn saturating_sub(self, other: Self) -> Self {
310        Self(self.0.saturating_sub(other.0))
311    }
312
313    /// Multiplies ticks exactly, returning None on overflow.
314    pub fn checked_mul(self, factor: u64) -> Option<Self> {
315        self.0.checked_mul(factor).map(Self)
316    }
317
318    /// Scales like the `* f64` operator, reporting a negative or nonfinite factor and overflow.
319    pub fn try_mul(self, factor: f64) -> Result<Self, crate::ValidationError> {
320        if factor == 1.0 {
321            return Ok(self);
322        }
323        if !factor.is_finite() || factor < 0.0 {
324            return Err(time_error(
325                "factor",
326                "duration factor must be finite and nonnegative",
327            ));
328        }
329        try_rounded_unsigned_ticks(self.0 as f64 * factor).map(Self)
330    }
331
332    /// Divides like the `/ f64` operator, reporting a negative, nonfinite or zero divisor and overflow.
333    pub fn try_div(self, divisor: f64) -> Result<Self, crate::ValidationError> {
334        if !divisor.is_finite() || divisor <= 0.0 {
335            return Err(time_error(
336                "divisor",
337                "duration divisor must be finite and positive",
338            ));
339        }
340        if divisor == 1.0 {
341            return Ok(self);
342        }
343        try_rounded_unsigned_ticks(self.0 as f64 / divisor).map(Self)
344    }
345
346    /// Returns the dimensionless ratio, reporting a zero divisor.
347    pub fn try_ratio(self, divisor: Self) -> Result<f64, crate::ValidationError> {
348        if divisor.0 == 0 {
349            return Err(time_error(
350                "divisor",
351                "duration ratio requires a nonzero divisor",
352            ));
353        }
354        Ok(self.0 as f64 / divisor.0 as f64)
355    }
356}
357
358impl Add for Duration {
359    type Output = Self;
360
361    fn add(self, other: Self) -> Self {
362        Self(
363            self.0
364                .checked_add(other.0)
365                .expect("duration addition overflow"),
366        )
367    }
368}
369
370impl Sub for Duration {
371    type Output = Self;
372
373    /// Subtracts exactly.
374    ///
375    /// # Panics
376    /// Panics when `other` is longer; see [`Duration::checked_sub`].
377    fn sub(self, other: Self) -> Self {
378        self.checked_sub(other)
379            .expect("duration subtraction underflow")
380    }
381}
382
383impl AddAssign for Duration {
384    fn add_assign(&mut self, other: Self) {
385        *self = *self + other;
386    }
387}
388
389impl SubAssign for Duration {
390    fn sub_assign(&mut self, other: Self) {
391        *self = *self - other;
392    }
393}
394
395impl std::iter::Sum for Duration {
396    fn sum<Spans: Iterator<Item = Self>>(spans: Spans) -> Self {
397        spans.fold(Self::ZERO, |total, span| total + span)
398    }
399}
400
401impl Mul<f64> for Duration {
402    type Output = Self;
403
404    /// Scales the duration and rounds to the nearest tick, away from zero at ties.
405    ///
406    /// # Panics
407    /// Panics for a negative or nonfinite factor or overflow; see [`Duration::try_mul`].
408    fn mul(self, factor: f64) -> Self {
409        self.try_mul(factor)
410            .unwrap_or_else(|error| panic!("{}", error.message))
411    }
412}
413
414macro_rules! integer_duration_mul {
415    ($($integer:ty),+ $(,)?) => {$ (
416        impl Mul<$integer> for Duration {
417            type Output = Duration;
418
419            /// Scales exactly.
420            ///
421            /// # Panics
422            /// Panics for a negative factor or on overflow; see [`Duration::checked_mul`].
423            fn mul(self, factor: $integer) -> Duration {
424                Duration(u64::try_from(u128::from(self.0) * u128::try_from(factor).expect("duration multiplication overflow")).expect("duration multiplication overflow"))
425            }
426        }
427        impl Mul<Duration> for $integer {
428            type Output = Duration;
429            fn mul(self, span: Duration) -> Duration { span * self }
430        }
431    )+};
432}
433
434integer_duration_mul!(i32, i64, u32, u64, usize);
435
436impl Mul<Duration> for f64 {
437    type Output = Duration;
438
439    fn mul(self, span: Duration) -> Duration {
440        span * self
441    }
442}
443
444impl Div<f64> for Duration {
445    type Output = Self;
446
447    /// Divides the duration.
448    ///
449    /// # Panics
450    /// Panics for a negative, nonfinite or zero divisor; see [`Duration::try_div`].
451    fn div(self, divisor: f64) -> Self {
452        self.try_div(divisor)
453            .unwrap_or_else(|error| panic!("{}", error.message))
454    }
455}
456
457impl Div for Duration {
458    type Output = f64;
459
460    /// Returns a dimensionless ratio.
461    ///
462    /// # Panics
463    /// Panics for a zero divisor; see [`Duration::try_ratio`].
464    fn div(self, divisor: Self) -> f64 {
465        self.try_ratio(divisor)
466            .unwrap_or_else(|error| panic!("{}", error.message))
467    }
468}
469
470/// Readable nonnegative time literals, such as `2.seconds()` and `1.5.seconds()`.
471///
472/// Floating-point literals round to the nearest tick, away from zero at ties.
473/// Negative, nonfinite or out-of-range values panic; the literals are source
474/// constants, and runtime values use [`Duration::try_from_seconds`]. Integer
475/// literals use exact arithmetic and panic on overflow, independently of the
476/// build profile.
477pub trait TimeLiteral {
478    /// Converts seconds to a tick duration.
479    fn seconds(self) -> Duration;
480
481    /// Converts milliseconds to a tick duration.
482    fn milliseconds(self) -> Duration;
483}
484
485impl TimeLiteral for i64 {
486    fn seconds(self) -> Duration {
487        Duration(
488            u64::try_from(self)
489                .ok()
490                .and_then(|seconds| seconds.checked_mul(TICKS_PER_SECOND_UNSIGNED))
491                .expect("seconds overflow"),
492        )
493    }
494
495    fn milliseconds(self) -> Duration {
496        Duration(
497            u64::try_from(self)
498                .ok()
499                .and_then(|milliseconds| {
500                    milliseconds.checked_mul(TICKS_PER_SECOND_UNSIGNED / 1_000)
501                })
502                .expect("milliseconds overflow"),
503        )
504    }
505}
506
507impl TimeLiteral for i32 {
508    fn seconds(self) -> Duration {
509        i64::from(self).seconds()
510    }
511
512    fn milliseconds(self) -> Duration {
513        i64::from(self).milliseconds()
514    }
515}
516
517impl TimeLiteral for u64 {
518    fn seconds(self) -> Duration {
519        Duration(
520            self.checked_mul(TICKS_PER_SECOND_UNSIGNED)
521                .expect("seconds overflow"),
522        )
523    }
524
525    fn milliseconds(self) -> Duration {
526        Duration(
527            self.checked_mul(TICKS_PER_SECOND_UNSIGNED / 1_000)
528                .expect("milliseconds overflow"),
529        )
530    }
531}
532
533macro_rules! unsigned_time_literal {
534    ($($integer:ty),+ $(,)?) => {$ (
535        impl TimeLiteral for $integer {
536            fn seconds(self) -> Duration { u64::try_from(self).expect("seconds overflow").seconds() }
537            fn milliseconds(self) -> Duration { u64::try_from(self).expect("milliseconds overflow").milliseconds() }
538        }
539    )+};
540}
541
542unsigned_time_literal!(u32, usize);
543
544impl TimeLiteral for f64 {
545    fn seconds(self) -> Duration {
546        Duration::from_seconds(self)
547    }
548
549    fn milliseconds(self) -> Duration {
550        Duration(
551            try_rounded_unsigned_ticks(self * (TICKS_PER_SECOND / 1_000) as f64)
552                .unwrap_or_else(|error| panic!("{}", error.message)),
553        )
554    }
555}
556
557fn try_rounded_ticks(ticks: f64) -> Result<i64, crate::ValidationError> {
558    let rounded = ticks.round();
559    if rounded.is_finite() && rounded >= i64::MIN as f64 && rounded < -(i64::MIN as f64) {
560        Ok(rounded as i64)
561    } else {
562        Err(time_error(
563            "seconds",
564            "time must be finite and fit the signed tick range",
565        ))
566    }
567}
568
569fn try_rounded_unsigned_ticks(ticks: f64) -> Result<u64, crate::ValidationError> {
570    let rounded = ticks.round();
571    if rounded.is_finite() && rounded >= 0.0 && rounded < u64::MAX as f64 {
572        Ok(rounded as u64)
573    } else {
574        Err(time_error(
575            "seconds",
576            "duration must be finite, nonnegative and fit the tick range",
577        ))
578    }
579}
580
581fn time_error(field: &'static str, message: &'static str) -> crate::ValidationError {
582    crate::ValidationError::invalid("time", field, message)
583}
584
585#[cfg(test)]
586mod tests;