Skip to main content

lightyear_core/
time.rs

1/*!
2This module contains some helper functions to compute the difference between two times.
3*/
4use crate::tick::Tick;
5use core::cmp::Ordering;
6use core::fmt::{Debug, Formatter};
7use core::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
8
9use bevy_reflect::Reflect;
10use core::time::Duration;
11use fixed::traits::ToFixed;
12use fixed::types::{I32F32, U0F8, U0F16, U32F32};
13use lightyear_serde::reader::ReadInteger;
14use lightyear_serde::reader::Reader;
15use lightyear_serde::writer::WriteInteger;
16use lightyear_serde::{SerializationError, ToBytes};
17use serde::{Deserialize, Serialize};
18
19#[cfg(any(not(feature = "test_utils"), feature = "not_mock"))]
20pub use bevy_platform::time::Instant;
21// We use global instead of a thread_local, because otherwise we would need to advance the Instant on all threads
22#[cfg(all(feature = "test_utils", not(feature = "not_mock")))]
23pub use mock_instant::global::Instant;
24
25// TODO: maybe let the user choose between u8 or u16 for quantization?
26// quantization error for u8 is about 0.2%, for u16 is 0.0008%
27/// Overstep fraction towards the next tick
28///
29/// Represents a value between 0.0 and 1.0 that indicates progress towards the next tick
30/// Serializes to a u8 value for network transmission
31#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default, Reflect)]
32#[reflect(opaque)]
33pub struct Overstep {
34    value: U0F16,
35}
36
37impl Overstep {
38    pub fn new(value: U0F16) -> Self {
39        Self { value }
40    }
41    pub const fn lit(src: &str) -> Self {
42        Self {
43            value: U0F16::lit(src),
44        }
45    }
46
47    pub fn value(&self) -> U0F16 {
48        self.value
49    }
50
51    pub fn from_f32(value: f32) -> Self {
52        Self::new(U0F16::saturating_from_num(value))
53    }
54
55    pub fn to_f32(&self) -> f32 {
56        self.value.into()
57    }
58
59    pub fn from_u8(value: u8) -> Self {
60        Self::new(U0F8::from_bits(value).into())
61    }
62
63    pub fn to_u8(&self) -> u8 {
64        self.value.to_num::<U0F8>().to_bits()
65    }
66}
67
68impl PartialEq for Overstep {
69    fn eq(&self, other: &Self) -> bool {
70        // For exact equality, we compare the quantized values
71        self.to_u8() == other.to_u8()
72    }
73}
74
75impl Eq for Overstep {}
76
77impl PartialOrd for Overstep {
78    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79        Some(self.cmp(other))
80    }
81}
82
83impl Ord for Overstep {
84    fn cmp(&self, other: &Self) -> Ordering {
85        self.value
86            .partial_cmp(&other.value)
87            .expect("NaN overstep is invalid")
88    }
89}
90
91impl ToBytes for Overstep {
92    fn bytes_len(&self) -> usize {
93        1 // we only need 1 byte for a u8
94    }
95
96    fn to_bytes(&self, buffer: &mut impl WriteInteger) -> Result<(), SerializationError> {
97        Ok(buffer.write_u8(self.to_u8())?)
98    }
99
100    fn from_bytes(buffer: &mut lightyear_serde::reader::Reader) -> Result<Self, SerializationError>
101    where
102        Self: Sized,
103    {
104        Ok(Self::from_u8(buffer.read_u8()?))
105    }
106}
107
108impl Add for Overstep {
109    type Output = Self;
110
111    fn add(self, rhs: Self) -> Self::Output {
112        Self::new(self.value + rhs.value)
113    }
114}
115
116impl Sub for Overstep {
117    type Output = Self;
118
119    fn sub(self, rhs: Self) -> Self::Output {
120        Self::new(self.value - rhs.value)
121    }
122}
123
124impl AddAssign for Overstep {
125    fn add_assign(&mut self, rhs: Self) {
126        self.value = self.value.saturating_add(rhs.value);
127    }
128}
129
130impl SubAssign for Overstep {
131    fn sub_assign(&mut self, rhs: Self) {
132        self.value = self.value.saturating_sub(rhs.value);
133    }
134}
135
136impl From<f32> for Overstep {
137    fn from(value: f32) -> Self {
138        Self::from_f32(value)
139    }
140}
141
142impl From<Overstep> for f32 {
143    fn from(overstep: Overstep) -> Self {
144        overstep.to_f32()
145    }
146}
147
148// TODO: it would be nice if the tick duration was encoded in the tick itself
149// TODO: maybe have a Tick trait with an associated constant TICK_DURATION
150//  then the user can specify impl Tick<TICK_DURATION=16ms> for MyTick
151
152// TODO: maybe have a constant TICK_DURATION as a generic, so we have Tick<T> around.
153
154// TODO: maybe put this in lightyear_core?
155/// Uniquely identify a instant across all timelines
156#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Reflect)]
157#[reflect(opaque)]
158pub struct TickInstant {
159    pub value: U32F32,
160}
161
162impl Debug for TickInstant {
163    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
164        write!(f, "{}", self.value)
165    }
166}
167
168impl TickInstant {
169    pub const fn lit(src: &str) -> Self {
170        Self {
171            value: U32F32::lit(src),
172        }
173    }
174    pub const fn zero() -> Self {
175        Self {
176            value: U32F32::ZERO,
177        }
178    }
179    pub fn tick(&self) -> Tick {
180        Tick(self.value.to_num())
181    }
182    /// Overstep as a fraction towards the next tick
183    pub fn overstep(&self) -> Overstep {
184        Overstep::new(self.value.wrapping_to_fixed())
185    }
186
187    /// Construct a [`TickInstant`] from an integer tick and a fractional overstep.
188    ///
189    /// `tick` is the whole tick count, and `overstep` is the fraction towards
190    /// the next tick in the range [0, 1).
191    pub fn from_tick_and_overstep(tick: Tick, overstep: Overstep) -> Self {
192        let base: U32F32 = tick.0.to_fixed();
193        let frac: U32F32 = overstep.value().into();
194        Self { value: base + frac }
195    }
196
197    /// Convert this instant to a duration
198    pub fn as_duration(&self, tick_duration: Duration) -> Duration {
199        tick_duration.mul_f32(self.value.to_num())
200    }
201
202    pub fn as_time_delta(&self, tick_duration: Duration) -> TimeDelta {
203        let duration = self.as_duration(tick_duration);
204        TimeDelta::from_duration(duration).expect("Duration should be valid")
205    }
206
207    /// Convert a duration to a TickInstant
208    pub fn from_duration(duration: Duration, tick_duration: Duration) -> Self {
209        let ticks_f32 = duration.as_secs_f32() / tick_duration.as_secs_f32();
210        Self {
211            value: ticks_f32.wrapping_to_fixed(),
212        }
213    }
214
215    pub fn from_time_delta(delta: TimeDelta, tick_duration: Duration) -> Self {
216        let duration = delta.as_duration().expect("Duration should be valid");
217        Self::from_duration(duration, tick_duration)
218    }
219}
220
221impl From<TickDelta> for TickInstant {
222    fn from(value: TickDelta) -> Self {
223        if value.is_negative() {
224            panic!("Cannot convert negative TickDelta to TickInstant");
225        }
226        Self {
227            value: value.value.cast_unsigned(),
228        }
229    }
230}
231
232impl From<Tick> for TickInstant {
233    fn from(value: Tick) -> Self {
234        Self {
235            value: value.0.to_fixed(),
236        }
237    }
238}
239
240/// Represents the difference between two TickInstants
241#[derive(Clone, Copy, PartialEq, Eq, Reflect)]
242#[reflect(opaque)]
243pub struct TickDelta {
244    /// This is the combined representation of a signed 32-bit tick diff
245    /// plus an unsigned 32-bit overstep (range 0 to ~0.99998, always positive).
246    value: I32F32,
247}
248
249impl Debug for TickDelta {
250    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
251        write!(f, "{:.6}", self.value)
252    }
253}
254
255impl From<Tick> for TickDelta {
256    fn from(value: Tick) -> Self {
257        Self {
258            value: value.0.cast_signed().to_fixed(),
259        }
260    }
261}
262
263impl From<i32> for TickDelta {
264    fn from(value: i32) -> Self {
265        Self {
266            value: value.to_fixed(),
267        }
268    }
269}
270
271impl From<PositiveTickDelta> for TickDelta {
272    fn from(value: PositiveTickDelta) -> Self {
273        Self {
274            value: value.value.wrapping_to_fixed(),
275        }
276    }
277}
278
279impl From<TickInstant> for TickDelta {
280    fn from(value: TickInstant) -> Self {
281        Self {
282            value: value.value.cast_signed(),
283        }
284    }
285}
286
287impl TickDelta {
288    pub fn new(value: I32F32) -> Self {
289        Self { value }
290    }
291    pub const fn lit(src: &str) -> Self {
292        Self {
293            value: I32F32::lit(src),
294        }
295    }
296
297    pub fn tick_diff(&self) -> u32 {
298        self.value.unsigned_abs().to_num::<u32>()
299    }
300    pub fn overstep(&self) -> Overstep {
301        Overstep::new(self.value.unsigned_abs().wrapping_to_num())
302    }
303
304    pub fn is_positive(&self) -> bool {
305        self.value.is_positive()
306    }
307
308    pub fn is_negative(&self) -> bool {
309        self.value.is_negative()
310    }
311
312    pub fn to_duration(&self, tick_duration: Duration) -> Duration {
313        tick_duration.mul_f32(self.value.to_num())
314    }
315
316    pub fn from_duration(duration: Duration, tick_duration: Duration) -> Self {
317        debug_assert!(
318            tick_duration > Duration::ZERO,
319            "Tick duration must be positive"
320        );
321        let ticks_f32 = duration.as_secs_f32() / tick_duration.as_secs_f32();
322        Self {
323            value: ticks_f32.wrapping_to_fixed(),
324        }
325    }
326
327    pub fn to_time_delta(&self, tick_duration: Duration) -> TimeDelta {
328        let tick_duration_f32 = tick_duration.as_secs_f32();
329        let duration = tick_duration_f32 * self.value.to_num::<f32>();
330        if self.is_negative() {
331            // Handle negative duration conversion
332            match TimeDelta::from_duration(Duration::from_secs_f32(-duration)) {
333                Ok(delta) => -delta,
334                Err(_) => panic!("Failed to convert duration to TimeDelta"),
335            }
336        } else {
337            TimeDelta::from_duration(Duration::from_secs_f32(duration))
338                .expect("Duration should be valid")
339        }
340    }
341
342    pub fn to_f32(&self) -> f32 {
343        self.value.to_num()
344    }
345
346    /// Apply a delta number of ticks with no overstep
347    pub fn from_i32(delta: i32) -> Self {
348        Self {
349            value: delta.to_fixed(),
350        }
351    }
352
353    /// Returns the number of tick difference (positive or negative) that this TickDelta represents,
354    /// rounding to the closes integer value
355    pub fn to_i32(&self) -> i32 {
356        self.value.to_num()
357    }
358
359    pub fn from_time_delta(mut delta: TimeDelta, tick_duration: Duration) -> Self {
360        let is_negative = !delta.is_positive();
361        if is_negative {
362            delta = -delta;
363        }
364
365        // Work with absolute duration
366        let duration = match delta.as_duration() {
367            Ok(d) => d,
368            Err(_) => panic!("Failed to convert TimeDelta to Duration"),
369        };
370
371        let mut ticks_f32 = duration.as_secs_f32() / tick_duration.as_secs_f32();
372        if is_negative {
373            ticks_f32 = -ticks_f32;
374        }
375        Self {
376            value: ticks_f32.wrapping_to_fixed(),
377        }
378    }
379
380    pub fn zero() -> Self {
381        Self {
382            value: I32F32::default(),
383        }
384    }
385}
386
387impl Neg for TickDelta {
388    type Output = Self;
389
390    fn neg(self) -> Self::Output {
391        Self { value: -self.value }
392    }
393}
394
395impl Add for TickDelta {
396    type Output = TickDelta;
397
398    fn add(self, rhs: Self) -> Self::Output {
399        Self {
400            value: self.value.wrapping_add(rhs.value),
401        }
402    }
403}
404
405impl Sub for TickDelta {
406    type Output = TickDelta;
407
408    fn sub(self, rhs: Self) -> Self::Output {
409        Self {
410            value: self.value.wrapping_sub(rhs.value),
411        }
412    }
413}
414
415impl Mul<f32> for TickDelta {
416    type Output = Self;
417
418    fn mul(self, rhs: f32) -> Self::Output {
419        Self {
420            value: self.value.to_num::<f32>().mul(rhs).wrapping_to_fixed(),
421        }
422    }
423}
424
425impl Mul<U0F16> for TickDelta {
426    type Output = Self;
427
428    fn mul(self, rhs: U0F16) -> Self::Output {
429        let rhs_fixed: I32F32 = rhs.to_fixed::<I32F32>();
430        Self {
431            value: self.value.wrapping_mul(rhs_fixed),
432        }
433    }
434}
435
436#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
437#[reflect(opaque)]
438pub struct PositiveTickDelta {
439    pub value: U32F32,
440}
441
442impl PositiveTickDelta {
443    pub const fn lit(src: &str) -> Self {
444        Self {
445            value: U32F32::lit(src),
446        }
447    }
448    pub fn tick_diff(&self) -> u32 {
449        self.value.to_num::<u32>()
450    }
451    pub fn overstep(&self) -> Overstep {
452        Overstep::new(self.value.wrapping_to_num())
453    }
454}
455
456impl From<TickDelta> for PositiveTickDelta {
457    fn from(value: TickDelta) -> Self {
458        if value.is_negative() {
459            panic!("Cannot convert negative TickDelta to PositiveTickDelta");
460        }
461        Self {
462            value: value.value.cast_unsigned(),
463        }
464    }
465}
466
467impl ToBytes for PositiveTickDelta {
468    fn bytes_len(&self) -> usize {
469        4 + self.overstep().bytes_len()
470    }
471
472    // TODO: use varint for the tick_diff since it's probably small
473    fn to_bytes(&self, buffer: &mut impl WriteInteger) -> Result<(), SerializationError> {
474        buffer.write_u32(self.tick_diff())?;
475        self.overstep().to_bytes(buffer)
476    }
477
478    fn from_bytes(buffer: &mut Reader) -> Result<Self, SerializationError>
479    where
480        Self: Sized,
481    {
482        let tick_diff = buffer.read_u32()?;
483        let overstep = Overstep::from_bytes(buffer)?;
484        Ok(Self {
485            value: tick_diff.to_fixed::<U32F32>() + U32F32::from(overstep.value),
486        })
487    }
488}
489
490/// Delta between two instants
491///
492/// This is mostly useful because it can represent a positive or a negative duration.
493#[derive(Debug, PartialEq, Eq, Clone, Copy)]
494pub struct TimeDelta {
495    duration: chrono::TimeDelta,
496}
497
498impl TimeDelta {
499    pub fn is_positive(&self) -> bool {
500        self.duration.num_nanoseconds().unwrap_or(0) >= 0
501    }
502
503    /// We convert negative durations to their absolute value
504    pub fn as_duration(&self) -> Result<Duration, chrono::OutOfRangeError> {
505        self.duration.to_std()
506    }
507
508    pub fn from_duration(duration: Duration) -> Result<Self, chrono::OutOfRangeError> {
509        Ok(Self {
510            duration: chrono::TimeDelta::from_std(duration)?,
511        })
512    }
513}
514
515impl Neg for TimeDelta {
516    type Output = Self;
517
518    fn neg(self) -> Self::Output {
519        Self {
520            duration: -self.duration,
521        }
522    }
523}
524
525impl Add<TickDelta> for TickInstant {
526    type Output = TickInstant;
527
528    fn add(self, rhs: TickDelta) -> Self::Output {
529        TickInstant {
530            value: self.value.wrapping_add_signed(rhs.value),
531        }
532    }
533}
534
535impl Sub<TickDelta> for TickInstant {
536    type Output = TickInstant;
537
538    fn sub(self, rhs: TickDelta) -> Self::Output {
539        TickInstant {
540            value: self.value.wrapping_sub_signed(rhs.value),
541        }
542    }
543}
544
545impl Sub for TickInstant {
546    type Output = TickDelta;
547
548    fn sub(self, rhs: TickInstant) -> Self::Output {
549        TickDelta {
550            value: self.value.cast_signed().wrapping_sub_unsigned(rhs.value),
551        }
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use approx::{AbsDiffEq, assert_abs_diff_eq, assert_relative_eq};
559    use core::time::Duration;
560
561    impl AbsDiffEq for Overstep {
562        type Epsilon = Overstep;
563
564        fn default_epsilon() -> Self::Epsilon {
565            Overstep {
566                value: U0F16::DELTA,
567            }
568        }
569
570        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
571            self.value.abs_diff(other.value) <= epsilon.value
572        }
573    }
574
575    impl AbsDiffEq for TickInstant {
576        type Epsilon = TickInstant;
577
578        fn default_epsilon() -> Self::Epsilon {
579            TickInstant {
580                value: U32F32::DELTA,
581            }
582        }
583
584        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
585            self.value.abs_diff(other.value) <= epsilon.value
586        }
587    }
588
589    impl AbsDiffEq for TickDelta {
590        type Epsilon = TickDelta;
591
592        fn default_epsilon() -> Self::Epsilon {
593            TickDelta {
594                value: I32F32::DELTA,
595            }
596        }
597
598        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
599            self.value.abs_diff(other.value) <= epsilon.value
600        }
601    }
602
603    #[test]
604    fn test_overstep_quantization_error() {
605        // Test that the round trip error is less than 1% for values from 0.0 to 1.0
606        for i in 0..=10 {
607            let original_value = i as f32 / 10.0;
608            let overstep = Overstep::from_f32(original_value);
609            let quantized = overstep.to_u8();
610            let round_trip = Overstep::from_u8(quantized).to_f32();
611
612            assert_relative_eq!(round_trip, original_value, epsilon = 0.01);
613        }
614    }
615
616    #[test]
617    fn test_tickinstant_ordering() {
618        let t1 = TickInstant::lit("10.5");
619        let t2 = TickInstant::lit("10.7");
620        let t3 = TickInstant::lit("11.2");
621
622        assert!(t1 < t2);
623        assert!(t2 < t3);
624        assert!(t1 < t3);
625
626        assert_eq!(t1.cmp(&t1), Ordering::Equal);
627        assert_eq!(t1.cmp(&t2), Ordering::Less);
628        assert_eq!(t2.cmp(&t1), Ordering::Greater);
629    }
630
631    #[test]
632    fn test_tickinstant_add_positive_tickdelta() {
633        let tick_instant = TickInstant::lit("10.3");
634        let tick_delta = TickDelta::lit("5.2");
635
636        let result = tick_instant + tick_delta;
637
638        assert_abs_diff_eq!(result, TickInstant::lit("15.5"));
639    }
640
641    #[test]
642    fn test_tickinstant_add_negative_tickdelta() {
643        let tick_instant = TickInstant::lit("10.3");
644        let tick_delta = TickDelta::lit("-5.2"); // negative delta
645
646        let result = tick_instant + tick_delta;
647
648        assert_abs_diff_eq!(result, TickInstant::lit("5.1"));
649    }
650
651    #[test]
652    fn test_tickinstant_add_with_overstep_overflow() {
653        let tick_instant = TickInstant::lit("10.7");
654        let tick_delta = TickDelta::lit("5.6");
655
656        let result = tick_instant + tick_delta;
657
658        // 0.7 + 0.6 = 1.3, which is 1 tick + 0.3 overstep
659        assert_abs_diff_eq!(result, TickInstant::lit("16.3"));
660    }
661
662    #[test]
663    fn test_tickinstant_sub_positive_tickdelta() {
664        let tick_instant = TickInstant::lit("10.7");
665        let tick_delta = TickDelta::lit("5.2");
666
667        let result = tick_instant - tick_delta;
668
669        assert_abs_diff_eq!(result, TickInstant::lit("5.5"));
670    }
671
672    #[test]
673    fn test_tickinstant_sub_negative_tickdelta() {
674        let tick_instant = TickInstant::lit("10.3");
675        let tick_delta = TickDelta::lit("-5.2"); // negative delta
676
677        let result = tick_instant - tick_delta;
678
679        assert_abs_diff_eq!(result, TickInstant::lit("15.5"));
680    }
681
682    #[test]
683    fn test_tickinstant_sub_with_overstep_underflow() {
684        let tick_instant = TickInstant::lit("10.3");
685        let tick_delta = TickDelta::lit("5.7");
686
687        let result = tick_instant - tick_delta;
688
689        // 0.3 - 0.7 = -0.4, which becomes 0.6 with borrowing from tick
690        assert_abs_diff_eq!(result, TickInstant::lit("4.6"));
691    }
692
693    #[test]
694    fn test_tickinstant_sub_tickinstant() {
695        let t1 = TickInstant::lit("15.7");
696        let t2 = TickInstant::lit("10.3");
697
698        // t1 - t2 (positive result)
699        let delta = t1 - t2;
700        assert_abs_diff_eq!(delta, TickDelta::lit("5.4"));
701
702        // t2 - t1 (negative result)
703        let delta = t2 - t1;
704        assert_abs_diff_eq!(delta, TickDelta::lit("-5.4"));
705    }
706
707    #[test]
708    fn test_tickinstant_sub_tickinstant_with_overstep_underflow() {
709        let t1 = TickInstant::lit("15.2");
710        let t2 = TickInstant::lit("10.7");
711
712        // Need to borrow from tick
713        let delta = t1 - t2;
714        assert_abs_diff_eq!(delta, TickDelta::lit("4.5"));
715    }
716
717    #[test]
718    fn test_tickdelta_accessors() {
719        let delta = TickDelta::lit("-32768");
720        assert_eq!(delta.is_positive(), false);
721        assert_eq!(delta.is_negative(), true);
722        assert_eq!(delta.tick_diff(), 32768);
723        assert_eq!(delta.overstep().to_f32(), 0.0);
724
725        let delta = TickDelta::lit("-32767.75");
726        assert_eq!(delta.is_positive(), false);
727        assert_eq!(delta.is_negative(), true);
728        assert_eq!(delta.tick_diff(), 32767);
729        assert_eq!(delta.overstep().to_f32(), 0.75);
730
731        let delta = TickDelta::lit("32767.75");
732        assert_eq!(delta.is_positive(), true);
733        assert_eq!(delta.is_negative(), false);
734        assert_eq!(delta.tick_diff(), 32767);
735        assert_eq!(delta.overstep().to_f32(), 0.75);
736    }
737
738    #[test]
739    fn test_tickdelta_negation() {
740        let delta = TickDelta::lit("5.3");
741        let negated = -delta;
742
743        assert_abs_diff_eq!(negated, TickDelta::lit("-5.3"));
744
745        // Double negation should return to original
746        let double_negated = -negated;
747
748        assert_abs_diff_eq!(double_negated, TickDelta::lit("5.3"));
749    }
750
751    #[test]
752    fn test_tickdelta_signed_addition() {
753        let delta = TickDelta::from_i32(10);
754
755        assert_eq!((delta + delta).to_i32(), 20);
756        assert_eq!((delta + (-delta)).to_i32(), 0);
757        assert_eq!(((-delta) + delta).to_i32(), 0);
758        assert_eq!(((-delta) + (-delta)).to_i32(), -20);
759    }
760
761    #[test]
762    fn test_tickdelta_multiplication() {
763        let delta = TickDelta::lit("10.5");
764
765        // Simple multiplication
766        let result = delta * 2.0;
767        assert_eq!(result, TickDelta::lit("21"));
768        assert_relative_eq!(result.overstep().to_f32(), 0.0);
769
770        // Fractional multiplication
771        let result = delta * 1.5;
772        assert_eq!(result, TickDelta::lit("15.75"));
773
774        // Multiplication causing overstep overflow
775        let delta = TickDelta::lit("10.8");
776        let result = delta * 1.5;
777        assert_abs_diff_eq!(
778            result,
779            TickDelta::lit("16.2"),
780            epsilon = TickDelta::lit("0.001")
781        );
782    }
783
784    #[test]
785    fn test_tickdelta_subtraction() {
786        let delta = TickDelta::from(10i32);
787        let sub = delta - TickDelta::from(20i32);
788        assert_relative_eq!(sub.to_f32(), -10.0);
789
790        let a = TickDelta::lit("0.1");
791        let b = TickDelta::lit("0.6");
792        let sub = a - b;
793        assert_relative_eq!(sub.to_f32(), -0.5);
794
795        // Same tick, a > b
796        let a = TickDelta::lit("0.8");
797        let b = TickDelta::lit("0.3");
798        let sub = a - b;
799        assert_relative_eq!(sub.to_f32(), 0.5);
800
801        // Different tick, no underflow
802        let a = TickDelta::lit("2.7");
803        let b = TickDelta::lit("1.2");
804        let sub = a - b;
805        assert_relative_eq!(sub.to_f32(), 1.5);
806
807        // Different tick, underflow
808        let a = TickDelta::lit("2.1");
809        let b = TickDelta::lit("1.6");
810        let sub = a - b;
811        assert_relative_eq!(sub.to_f32(), 0.5);
812
813        // rhs > self, no underflow
814        let a = TickDelta::lit("1.2");
815        let b = TickDelta::lit("2.7");
816        let sub = a - b;
817        assert_relative_eq!(sub.to_f32(), -1.5);
818
819        // rhs > self, underflow
820        let a = TickDelta::lit("1.6");
821        let b = TickDelta::lit("2.1");
822        let sub = a - b;
823        assert_relative_eq!(sub.to_f32(), -0.5);
824    }
825
826    #[test]
827    fn test_tick_conversion_roundtrip() {
828        let tick_duration = Duration::from_millis(100);
829        let original = TickInstant::lit("15.4");
830
831        // Convert to duration and back
832        let duration = original.as_duration(tick_duration);
833        let roundtrip = TickInstant::from_duration(duration, tick_duration);
834
835        // Allow for small floating point error in overstep
836        assert_eq!(roundtrip.tick(), original.tick());
837
838        assert!((roundtrip.overstep().to_f32() - original.overstep().to_f32()).abs() < 0.01);
839    }
840
841    #[test]
842    fn test_tickdelta_conversion_roundtrip() {
843        let tick_duration = Duration::from_millis(100);
844
845        // Test positive delta
846        let original_delta = TickDelta::lit("5.3");
847        let time_delta = original_delta.to_time_delta(tick_duration);
848        let roundtrip = TickDelta::from_time_delta(time_delta, tick_duration);
849
850        assert_eq!(roundtrip.tick_diff(), original_delta.tick_diff());
851        assert!((roundtrip.overstep().to_f32() - original_delta.overstep().to_f32()).abs() < 0.01);
852        assert_eq!(roundtrip.is_negative(), original_delta.is_negative());
853
854        // Test negative delta
855        let original_delta = TickDelta::lit("-7.6");
856        let time_delta = original_delta.to_time_delta(tick_duration);
857        let roundtrip = TickDelta::from_time_delta(time_delta, tick_duration);
858
859        assert_eq!(roundtrip.tick_diff(), original_delta.tick_diff());
860        assert!((roundtrip.overstep().to_f32() - original_delta.overstep().to_f32()).abs() < 0.01);
861        assert_eq!(roundtrip.is_negative(), original_delta.is_negative());
862    }
863}