Skip to main content

moq_lite/model/
time.rs

1use rand::Rng;
2
3use crate::Error;
4use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt};
5
6use std::sync::LazyLock;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// A timestamp representing the presentation time in milliseconds.
10///
11/// The underlying implementation supports any scale, but everything uses milliseconds by default.
12pub type Time = Timescale<1_000>;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
15#[error("time overflow")]
16pub struct TimeOverflow;
17
18/// A timestamp representing the presentation time in a given scale. ex. 1000 for milliseconds.
19///
20/// All timestamps within a track are relative, so zero for one track is not zero for another.
21/// Values are constrained to fit within a QUIC VarInt (2^62) so they can be encoded and decoded easily.
22///
23/// This is [std::time::Instant] and [std::time::Duration] merged into one type for simplicity.
24#[derive(Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Timescale<const SCALE: u64>(VarInt);
27
28impl<const SCALE: u64> Timescale<SCALE> {
29	/// The maximum representable instant.
30	pub const MAX: Self = Self(VarInt::MAX);
31
32	/// The minimum representable instant.
33	pub const ZERO: Self = Self(VarInt::ZERO);
34
35	pub const fn new(value: u32) -> Self {
36		Self(VarInt::from_u32(value))
37	}
38
39	pub const fn new_u64(value: u64) -> Result<Self, TimeOverflow> {
40		match VarInt::from_u64(value) {
41			Some(varint) => Ok(Self(varint)),
42			None => Err(TimeOverflow),
43		}
44	}
45
46	/// Convert a number of seconds to a timestamp, returning an error if the timestamp would overflow.
47	pub const fn from_secs(seconds: u64) -> Result<Self, TimeOverflow> {
48		// Not using from_scale because it'll be slightly faster
49		match seconds.checked_mul(SCALE) {
50			Some(value) => Self::new_u64(value),
51			None => Err(TimeOverflow),
52		}
53	}
54
55	pub const fn from_secs_unchecked(seconds: u64) -> Self {
56		match Self::from_secs(seconds) {
57			Ok(time) => time,
58			Err(_) => panic!("time overflow"),
59		}
60	}
61
62	/// Convert a number of milliseconds to a timestamp, returning an error if the timestamp would overflow.
63	pub const fn from_millis(millis: u64) -> Result<Self, TimeOverflow> {
64		Self::from_scale(millis, 1000)
65	}
66
67	pub const fn from_millis_unchecked(millis: u64) -> Self {
68		Self::from_scale_unchecked(millis, 1000)
69	}
70
71	pub const fn from_micros(micros: u64) -> Result<Self, TimeOverflow> {
72		Self::from_scale(micros, 1_000_000)
73	}
74
75	pub const fn from_micros_unchecked(micros: u64) -> Self {
76		Self::from_scale_unchecked(micros, 1_000_000)
77	}
78
79	pub const fn from_nanos(nanos: u64) -> Result<Self, TimeOverflow> {
80		Self::from_scale(nanos, 1_000_000_000)
81	}
82
83	pub const fn from_nanos_unchecked(nanos: u64) -> Self {
84		Self::from_scale_unchecked(nanos, 1_000_000_000)
85	}
86
87	pub const fn from_scale(value: u64, scale: u64) -> Result<Self, TimeOverflow> {
88		match VarInt::from_u128(value as u128 * SCALE as u128 / scale as u128) {
89			Some(varint) => Ok(Self(varint)),
90			None => Err(TimeOverflow),
91		}
92	}
93
94	pub const fn from_scale_u128(value: u128, scale: u64) -> Result<Self, TimeOverflow> {
95		match value.checked_mul(SCALE as u128) {
96			Some(value) => match VarInt::from_u128(value / scale as u128) {
97				Some(varint) => Ok(Self(varint)),
98				None => Err(TimeOverflow),
99			},
100			None => Err(TimeOverflow),
101		}
102	}
103
104	pub const fn from_scale_unchecked(value: u64, scale: u64) -> Self {
105		match Self::from_scale(value, scale) {
106			Ok(time) => time,
107			Err(_) => panic!("time overflow"),
108		}
109	}
110
111	/// Get the timestamp as seconds.
112	pub const fn as_secs(self) -> u64 {
113		self.0.into_inner() / SCALE
114	}
115
116	/// Get the timestamp as milliseconds.
117	//
118	// This returns a u128 to avoid a possible overflow when SCALE < 250
119	pub const fn as_millis(self) -> u128 {
120		self.as_scale(1000)
121	}
122
123	/// Get the timestamp as microseconds.
124	pub const fn as_micros(self) -> u128 {
125		self.as_scale(1_000_000)
126	}
127
128	/// Get the timestamp as nanoseconds.
129	pub const fn as_nanos(self) -> u128 {
130		self.as_scale(1_000_000_000)
131	}
132
133	pub const fn as_scale(self, scale: u64) -> u128 {
134		self.0.into_inner() as u128 * scale as u128 / SCALE as u128
135	}
136
137	/// Get the maximum of two timestamps.
138	pub const fn max(self, other: Self) -> Self {
139		if self.0.into_inner() > other.0.into_inner() {
140			self
141		} else {
142			other
143		}
144	}
145
146	pub const fn checked_add(self, rhs: Self) -> Result<Self, TimeOverflow> {
147		let lhs = self.0.into_inner();
148		let rhs = rhs.0.into_inner();
149		match lhs.checked_add(rhs) {
150			Some(result) => Self::new_u64(result),
151			None => Err(TimeOverflow),
152		}
153	}
154
155	pub const fn checked_sub(self, rhs: Self) -> Result<Self, TimeOverflow> {
156		let lhs = self.0.into_inner();
157		let rhs = rhs.0.into_inner();
158		match lhs.checked_sub(rhs) {
159			Some(result) => Self::new_u64(result),
160			None => Err(TimeOverflow),
161		}
162	}
163
164	pub const fn is_zero(self) -> bool {
165		self.0.into_inner() == 0
166	}
167
168	pub fn now() -> Self {
169		// We use tokio so it can be stubbed for testing.
170		tokio::time::Instant::now().into()
171	}
172
173	/// Convert this timestamp to a different scale.
174	///
175	/// This allows converting between different TimeScale types, for example from milliseconds to microseconds.
176	/// Note that converting to a coarser scale may lose precision due to integer division.
177	pub const fn convert<const NEW_SCALE: u64>(self) -> Result<Timescale<NEW_SCALE>, TimeOverflow> {
178		let value = self.0.into_inner();
179		// Convert from SCALE to NEW_SCALE: value * NEW_SCALE / SCALE
180		match (value as u128).checked_mul(NEW_SCALE as u128) {
181			Some(v) => match v.checked_div(SCALE as u128) {
182				Some(v) => match VarInt::from_u128(v) {
183					Some(varint) => Ok(Timescale(varint)),
184					None => Err(TimeOverflow),
185				},
186				None => Err(TimeOverflow),
187			},
188			None => Err(TimeOverflow),
189		}
190	}
191
192	pub fn encode<W: bytes::BufMut>(&self, w: &mut W) -> Result<(), EncodeError> {
193		// Version-independent: uses QUIC varint encoding.
194		self.0.encode(w, crate::lite::Version::Lite01)?;
195		Ok(())
196	}
197
198	pub fn decode<R: bytes::Buf>(r: &mut R) -> Result<Self, Error> {
199		// Version-independent: uses QUIC varint encoding.
200		let v = VarInt::decode(r, crate::lite::Version::Lite01)?;
201		Ok(Self(v))
202	}
203}
204
205impl<const SCALE: u64> TryFrom<std::time::Duration> for Timescale<SCALE> {
206	type Error = TimeOverflow;
207
208	fn try_from(duration: std::time::Duration) -> Result<Self, Self::Error> {
209		Self::from_scale_u128(duration.as_nanos(), 1_000_000_000)
210	}
211}
212
213impl<const SCALE: u64> From<Timescale<SCALE>> for std::time::Duration {
214	fn from(time: Timescale<SCALE>) -> Self {
215		std::time::Duration::new(time.as_secs(), (time.as_nanos() % 1_000_000_000) as u32)
216	}
217}
218
219impl<const SCALE: u64> std::fmt::Debug for Timescale<SCALE> {
220	#[allow(clippy::manual_is_multiple_of)] // is_multiple_of is unstable in Rust 1.85
221	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222		let nanos = self.as_nanos();
223
224		// Choose the largest unit where we don't need decimal places
225		// Check from largest to smallest unit
226		if nanos % 1_000_000_000 == 0 {
227			write!(f, "{}s", nanos / 1_000_000_000)
228		} else if nanos % 1_000_000 == 0 {
229			write!(f, "{}ms", nanos / 1_000_000)
230		} else if nanos % 1_000 == 0 {
231			write!(f, "{}µs", nanos / 1_000)
232		} else {
233			write!(f, "{}ns", nanos)
234		}
235	}
236}
237
238impl<const SCALE: u64> std::ops::Add for Timescale<SCALE> {
239	type Output = Self;
240
241	fn add(self, rhs: Self) -> Self {
242		self.checked_add(rhs).expect("time overflow")
243	}
244}
245
246impl<const SCALE: u64> std::ops::AddAssign for Timescale<SCALE> {
247	fn add_assign(&mut self, rhs: Self) {
248		*self = *self + rhs;
249	}
250}
251
252impl<const SCALE: u64> std::ops::Sub for Timescale<SCALE> {
253	type Output = Self;
254
255	fn sub(self, rhs: Self) -> Self {
256		self.checked_sub(rhs).expect("time overflow")
257	}
258}
259
260impl<const SCALE: u64> std::ops::SubAssign for Timescale<SCALE> {
261	fn sub_assign(&mut self, rhs: Self) {
262		*self = *self - rhs;
263	}
264}
265
266// There's no zero Instant, so we need to use a reference point.
267static TIME_ANCHOR: LazyLock<(std::time::Instant, SystemTime)> = LazyLock::new(|| {
268	// To deter nerds trying to use timestamp as wall clock time, we subtract a random amount of time from the anchor.
269	// This will make our timestamps appear to be late; just enough to be annoying and obscure our clock drift.
270	// This will also catch bad implementations that assume unrelated broadcasts are synchronized.
271	let jitter = std::time::Duration::from_millis(rand::rng().random_range(0..69_420));
272	(std::time::Instant::now(), SystemTime::now() - jitter)
273});
274
275// Convert an Instant to a Unix timestamp
276impl<const SCALE: u64> From<std::time::Instant> for Timescale<SCALE> {
277	fn from(instant: std::time::Instant) -> Self {
278		let (anchor_instant, anchor_system) = *TIME_ANCHOR;
279
280		// Conver the instant to a SystemTime.
281		let system = match instant.checked_duration_since(anchor_instant) {
282			Some(forward) => anchor_system + forward,
283			None => anchor_system - anchor_instant.duration_since(instant),
284		};
285
286		// Convert the SystemTime to a Unix timestamp in nanoseconds.
287		// We'll then convert that to the desired scale.
288		system
289			.duration_since(UNIX_EPOCH)
290			.expect("dude your clock is earlier than 1970")
291			.try_into()
292			.expect("dude your clock is later than 2116")
293	}
294}
295
296impl<const SCALE: u64> From<tokio::time::Instant> for Timescale<SCALE> {
297	fn from(instant: tokio::time::Instant) -> Self {
298		instant.into_std().into()
299	}
300}
301
302impl<const SCALE: u64> Decode<crate::Version> for Timescale<SCALE> {
303	fn decode<R: bytes::Buf>(r: &mut R, version: crate::Version) -> Result<Self, DecodeError> {
304		let v = VarInt::decode(r, version)?;
305		Ok(Self(v))
306	}
307}
308
309impl<const SCALE: u64> Encode<crate::Version> for Timescale<SCALE> {
310	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: crate::Version) -> Result<(), EncodeError> {
311		self.0.encode(w, version)?;
312		Ok(())
313	}
314}
315
316#[cfg(test)]
317mod tests {
318	use super::*;
319
320	#[test]
321	fn test_from_secs() {
322		let time = Time::from_secs(5).unwrap();
323		assert_eq!(time.as_secs(), 5);
324		assert_eq!(time.as_millis(), 5000);
325		assert_eq!(time.as_micros(), 5_000_000);
326		assert_eq!(time.as_nanos(), 5_000_000_000);
327	}
328
329	#[test]
330	fn test_from_millis() {
331		let time = Time::from_millis(5000).unwrap();
332		assert_eq!(time.as_secs(), 5);
333		assert_eq!(time.as_millis(), 5000);
334	}
335
336	#[test]
337	fn test_from_micros() {
338		let time = Time::from_micros(5_000_000).unwrap();
339		assert_eq!(time.as_secs(), 5);
340		assert_eq!(time.as_millis(), 5000);
341		assert_eq!(time.as_micros(), 5_000_000);
342	}
343
344	#[test]
345	fn test_from_nanos() {
346		let time = Time::from_nanos(5_000_000_000).unwrap();
347		assert_eq!(time.as_secs(), 5);
348		assert_eq!(time.as_millis(), 5000);
349		assert_eq!(time.as_micros(), 5_000_000);
350		assert_eq!(time.as_nanos(), 5_000_000_000);
351	}
352
353	#[test]
354	fn test_zero() {
355		let time = Time::ZERO;
356		assert_eq!(time.as_secs(), 0);
357		assert_eq!(time.as_millis(), 0);
358		assert_eq!(time.as_micros(), 0);
359		assert_eq!(time.as_nanos(), 0);
360		assert!(time.is_zero());
361	}
362
363	#[test]
364	fn test_roundtrip_millis() {
365		let values = [0, 1, 100, 1000, 999999, 1_000_000_000];
366		for &val in &values {
367			let time = Time::from_millis(val).unwrap();
368			assert_eq!(time.as_millis(), val as u128);
369		}
370	}
371
372	#[test]
373	fn test_roundtrip_micros() {
374		// Note: values < 1000 will lose precision when converting to milliseconds (SCALE=1000)
375		let values = [0, 1000, 1_000_000, 1_000_000_000];
376		for &val in &values {
377			let time = Time::from_micros(val).unwrap();
378			assert_eq!(time.as_micros(), val as u128);
379		}
380	}
381
382	#[test]
383	fn test_different_scale_seconds() {
384		type TimeInSeconds = Timescale<1>;
385		let time = TimeInSeconds::from_secs(5).unwrap();
386		assert_eq!(time.as_secs(), 5);
387		assert_eq!(time.as_millis(), 5000);
388	}
389
390	#[test]
391	fn test_different_scale_microseconds() {
392		type TimeInMicros = Timescale<1_000_000>;
393		let time = TimeInMicros::from_micros(5_000_000).unwrap();
394		assert_eq!(time.as_secs(), 5);
395		assert_eq!(time.as_micros(), 5_000_000);
396	}
397
398	#[test]
399	fn test_scale_conversion() {
400		// Converting 5000 milliseconds at scale 1000 to scale 1000 (should be identity)
401		let time = Time::from_scale(5000, 1000).unwrap();
402		assert_eq!(time.as_millis(), 5000);
403		assert_eq!(time.as_secs(), 5);
404
405		// Converting 5 seconds at scale 1 to scale 1000
406		let time = Time::from_scale(5, 1).unwrap();
407		assert_eq!(time.as_millis(), 5000);
408		assert_eq!(time.as_secs(), 5);
409	}
410
411	#[test]
412	fn test_add() {
413		let a = Time::from_secs(3).unwrap();
414		let b = Time::from_secs(2).unwrap();
415		let c = a + b;
416		assert_eq!(c.as_secs(), 5);
417		assert_eq!(c.as_millis(), 5000);
418	}
419
420	#[test]
421	fn test_sub() {
422		let a = Time::from_secs(5).unwrap();
423		let b = Time::from_secs(2).unwrap();
424		let c = a - b;
425		assert_eq!(c.as_secs(), 3);
426		assert_eq!(c.as_millis(), 3000);
427	}
428
429	#[test]
430	fn test_checked_add() {
431		let a = Time::from_millis(1000).unwrap();
432		let b = Time::from_millis(2000).unwrap();
433		let c = a.checked_add(b).unwrap();
434		assert_eq!(c.as_millis(), 3000);
435	}
436
437	#[test]
438	fn test_checked_sub() {
439		let a = Time::from_millis(5000).unwrap();
440		let b = Time::from_millis(2000).unwrap();
441		let c = a.checked_sub(b).unwrap();
442		assert_eq!(c.as_millis(), 3000);
443	}
444
445	#[test]
446	fn test_checked_sub_underflow() {
447		let a = Time::from_millis(1000).unwrap();
448		let b = Time::from_millis(2000).unwrap();
449		assert!(a.checked_sub(b).is_err());
450	}
451
452	#[test]
453	fn test_max() {
454		let a = Time::from_secs(5).unwrap();
455		let b = Time::from_secs(10).unwrap();
456		assert_eq!(a.max(b), b);
457		assert_eq!(b.max(a), b);
458	}
459
460	#[test]
461	fn test_duration_conversion() {
462		let duration = std::time::Duration::from_secs(5);
463		let time: Time = duration.try_into().unwrap();
464		assert_eq!(time.as_secs(), 5);
465		assert_eq!(time.as_millis(), 5000);
466
467		let duration_back: std::time::Duration = time.into();
468		assert_eq!(duration_back.as_secs(), 5);
469	}
470
471	#[test]
472	fn test_duration_with_nanos() {
473		let duration = std::time::Duration::new(5, 500_000_000); // 5.5 seconds
474		let time: Time = duration.try_into().unwrap();
475		assert_eq!(time.as_millis(), 5500);
476
477		let duration_back: std::time::Duration = time.into();
478		assert_eq!(duration_back.as_millis(), 5500);
479	}
480
481	#[test]
482	fn test_fractional_conversion() {
483		// Test that 1500 millis = 1.5 seconds
484		let time = Time::from_millis(1500).unwrap();
485		assert_eq!(time.as_secs(), 1); // Integer division
486		assert_eq!(time.as_millis(), 1500);
487		assert_eq!(time.as_micros(), 1_500_000);
488	}
489
490	#[test]
491	fn test_precision_loss() {
492		// When converting from a finer scale to coarser, we lose precision
493		// 1234 micros = 1.234 millis, which rounds down to 1 millisecond internally
494		// When converting back, we get 1000 micros, not the original 1234
495		let time = Time::from_micros(1234).unwrap();
496		assert_eq!(time.as_millis(), 1); // 1234 micros = 1.234 millis, rounds to 1
497		assert_eq!(time.as_micros(), 1000); // Precision lost: 1 milli = 1000 micros
498	}
499
500	#[test]
501	fn test_scale_boundaries() {
502		// Test values near scale boundaries
503		let time = Time::from_millis(999).unwrap();
504		assert_eq!(time.as_secs(), 0);
505		assert_eq!(time.as_millis(), 999);
506
507		let time = Time::from_millis(1000).unwrap();
508		assert_eq!(time.as_secs(), 1);
509		assert_eq!(time.as_millis(), 1000);
510
511		let time = Time::from_millis(1001).unwrap();
512		assert_eq!(time.as_secs(), 1);
513		assert_eq!(time.as_millis(), 1001);
514	}
515
516	#[test]
517	fn test_large_values() {
518		// Test with large but valid values
519		let large_secs = 1_000_000_000u64; // ~31 years
520		let time = Time::from_secs(large_secs).unwrap();
521		assert_eq!(time.as_secs(), large_secs);
522	}
523
524	#[test]
525	fn test_new() {
526		let time = Time::new(5000); // 5000 in the current scale (millis)
527		assert_eq!(time.as_millis(), 5000);
528		assert_eq!(time.as_secs(), 5);
529	}
530
531	#[test]
532	fn test_new_u64() {
533		let time = Time::new_u64(5000).unwrap();
534		assert_eq!(time.as_millis(), 5000);
535	}
536
537	#[test]
538	fn test_ordering() {
539		let a = Time::from_secs(1).unwrap();
540		let b = Time::from_secs(2).unwrap();
541		assert!(a < b);
542		assert!(b > a);
543		assert_eq!(a, a);
544	}
545
546	#[test]
547	fn test_unchecked_variants() {
548		let time = Time::from_secs_unchecked(5);
549		assert_eq!(time.as_secs(), 5);
550
551		let time = Time::from_millis_unchecked(5000);
552		assert_eq!(time.as_millis(), 5000);
553
554		let time = Time::from_micros_unchecked(5_000_000);
555		assert_eq!(time.as_micros(), 5_000_000);
556
557		let time = Time::from_nanos_unchecked(5_000_000_000);
558		assert_eq!(time.as_nanos(), 5_000_000_000);
559
560		let time = Time::from_scale_unchecked(5000, 1000);
561		assert_eq!(time.as_millis(), 5000);
562	}
563
564	#[test]
565	fn test_as_scale() {
566		let time = Time::from_secs(1).unwrap();
567		// 1 second in scale 1000 = 1000
568		assert_eq!(time.as_scale(1000), 1000);
569		// 1 second in scale 1 = 1
570		assert_eq!(time.as_scale(1), 1);
571		// 1 second in scale 1_000_000 = 1_000_000
572		assert_eq!(time.as_scale(1_000_000), 1_000_000);
573	}
574
575	#[test]
576	fn test_convert_to_finer() {
577		// Convert from milliseconds to microseconds (coarser to finer)
578		type TimeInMillis = Timescale<1_000>;
579		type TimeInMicros = Timescale<1_000_000>;
580
581		let time_millis = TimeInMillis::from_millis(5000).unwrap();
582		let time_micros: TimeInMicros = time_millis.convert().unwrap();
583
584		assert_eq!(time_micros.as_millis(), 5000);
585		assert_eq!(time_micros.as_micros(), 5_000_000);
586	}
587
588	#[test]
589	fn test_convert_to_coarser() {
590		// Convert from milliseconds to seconds (finer to coarser)
591		type TimeInMillis = Timescale<1_000>;
592		type TimeInSeconds = Timescale<1>;
593
594		let time_millis = TimeInMillis::from_millis(5000).unwrap();
595		let time_secs: TimeInSeconds = time_millis.convert().unwrap();
596
597		assert_eq!(time_secs.as_secs(), 5);
598		assert_eq!(time_secs.as_millis(), 5000);
599	}
600
601	#[test]
602	fn test_convert_precision_loss() {
603		// Converting 1234 millis to seconds loses precision
604		type TimeInMillis = Timescale<1_000>;
605		type TimeInSeconds = Timescale<1>;
606
607		let time_millis = TimeInMillis::from_millis(1234).unwrap();
608		let time_secs: TimeInSeconds = time_millis.convert().unwrap();
609
610		// 1234 millis = 1.234 seconds, rounds down to 1 second
611		assert_eq!(time_secs.as_secs(), 1);
612		assert_eq!(time_secs.as_millis(), 1000); // Lost 234 millis
613	}
614
615	#[test]
616	fn test_convert_roundtrip() {
617		// Converting to finer and back should preserve value
618		type TimeInMillis = Timescale<1_000>;
619		type TimeInMicros = Timescale<1_000_000>;
620
621		let original = TimeInMillis::from_millis(5000).unwrap();
622		let as_micros: TimeInMicros = original.convert().unwrap();
623		let back_to_millis: TimeInMillis = as_micros.convert().unwrap();
624
625		assert_eq!(original.as_millis(), back_to_millis.as_millis());
626	}
627
628	#[test]
629	fn test_convert_same_scale() {
630		// Converting to the same scale should be identity
631		type TimeInMillis = Timescale<1_000>;
632
633		let time = TimeInMillis::from_millis(5000).unwrap();
634		let converted: TimeInMillis = time.convert().unwrap();
635
636		assert_eq!(time.as_millis(), converted.as_millis());
637	}
638
639	#[test]
640	fn test_convert_microseconds_to_nanoseconds() {
641		type TimeInMicros = Timescale<1_000_000>;
642		type TimeInNanos = Timescale<1_000_000_000>;
643
644		let time_micros = TimeInMicros::from_micros(5_000_000).unwrap();
645		let time_nanos: TimeInNanos = time_micros.convert().unwrap();
646
647		assert_eq!(time_nanos.as_micros(), 5_000_000);
648		assert_eq!(time_nanos.as_nanos(), 5_000_000_000);
649	}
650
651	#[test]
652	fn test_convert_custom_scales() {
653		// Test with unusual custom scales
654		type TimeScale60 = Timescale<60>; // 60Hz
655		type TimeScale90 = Timescale<90>; // 90Hz
656
657		let time60 = TimeScale60::from_scale(120, 60).unwrap(); // 2 seconds at 60Hz
658		let time90: TimeScale90 = time60.convert().unwrap();
659
660		// Both should represent 2 seconds
661		assert_eq!(time60.as_secs(), 2);
662		assert_eq!(time90.as_secs(), 2);
663	}
664
665	#[test]
666	fn test_debug_format_units() {
667		// Test that Debug chooses appropriate units based on value
668
669		// Milliseconds that are clean seconds
670		let t = Time::from_millis(100000).unwrap();
671		assert_eq!(format!("{:?}", t), "100s");
672
673		let t = Time::from_millis(1000).unwrap();
674		assert_eq!(format!("{:?}", t), "1s");
675
676		// Milliseconds that are clean milliseconds
677		let t = Time::from_millis(100).unwrap();
678		assert_eq!(format!("{:?}", t), "100ms");
679
680		let t = Time::from_millis(5500).unwrap();
681		assert_eq!(format!("{:?}", t), "5500ms");
682
683		// Zero
684		let t = Time::ZERO;
685		assert_eq!(format!("{:?}", t), "0s");
686
687		// Test with microsecond-scale time
688		type TimeMicros = Timescale<1_000_000>;
689		let t = TimeMicros::from_micros(1500).unwrap();
690		assert_eq!(format!("{:?}", t), "1500µs");
691
692		let t = TimeMicros::from_micros(1000).unwrap();
693		assert_eq!(format!("{:?}", t), "1ms");
694	}
695}