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