Skip to main content

moq_net/model/
time.rs

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