//! Time and duration primitives (Chapter 3). //! //! The core has **two clocks** (Chapter 3 §"Design Principles"): musical time, //! measured in exact whole-note rationals ([`RationalTime`]), and wall-clock //! time, measured in fixed-point nanoseconds ([`WallClockTime`]). Position and //! duration are *distinct types* whose algebra is enforced at the type level //! ([`MusicalPosition`] + [`MusicalDuration`] → [`MusicalPosition`]; //! position − position → duration; position + position is not defined). //! //! Exactness is non-negotiable: musical time is an exact rational, never a //! float (Chapter 3; Appendix D §"Exact and Quantized Representations"). The //! recommended inline-or-promoted representation packs the common case and //! promotes to arbitrary precision on overflow, with no observable behavioural //! difference (Chapter 3 §"Promotion and Demotion"). use core::num::NonZeroU32; use core::ops::{Add, Sub}; use std::sync::Arc; use epiphany_determinism::{CanonicalDecode, CanonicalEncode, DecodeError}; use num_bigint::{BigInt, Sign}; use num_rational::BigRational; use num_traits::{Signed, Zero}; use crate::ids::{EventId, MeasureId, RegionId}; /// An exact rational musical-time value: a [`MusicalPosition`] or /// [`MusicalDuration`] before the newtype distinction is applied (Chapter 3 /// §"The Rational Time Type"). The unit is the **whole note**: a quarter note /// is `1/4`, a triplet eighth is `1/12`. /// /// Representation is inline-or-promoted (Chapter 3 §"Recommended /// Implementation"): the inline [`SmallRational`] (`i32` numerator, /// `NonZeroU32` denominator) covers the overwhelmingly common case; arithmetic /// that exceeds it silently promotes to an [`Arc`]-shared [`BigRational`]. /// /// **Canonical-form invariant.** A value is [`RationalTime::Small`] *if and /// only if* its normalized numerator fits `i32` and its denominator fits a /// nonzero `u32`. Every constructor and operation re-establishes this, so two /// numerically-equal values always share a variant and demotion is never /// observable (Chapter 3 §"Promotion and Demotion"). #[derive(Clone)] pub enum RationalTime { /// Inline case: fits in 8 bytes, always normalized. Small(SmallRational), /// Promoted case: arbitrary-precision rational, used only when arithmetic /// overflows the inline range. Large(Arc), } /// The inline rational: `i32` numerator over a `NonZeroU32` denominator, /// always normalized so `gcd(|numerator|, denominator) == 1` (Chapter 3). #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct SmallRational { numerator: i32, denominator: NonZeroU32, } impl SmallRational { /// The numerator. #[inline] pub fn numerator(self) -> i32 { self.numerator } /// The (strictly positive) denominator. #[inline] pub fn denominator(self) -> u32 { self.denominator.get() } } impl RationalTime { /// The additive identity, `0/1`. pub fn zero() -> Self { RationalTime::Small(SmallRational { numerator: 0, denominator: NonZeroU32::new(1).unwrap(), }) } /// The multiplicative identity and the duration of a whole note, `1/1`. pub fn one() -> Self { RationalTime::from_int(1) } /// An integer count of whole notes. pub fn from_int(n: i32) -> Self { RationalTime::Small(SmallRational { numerator: n, denominator: NonZeroU32::new(1).unwrap(), }) } /// Constructs `numerator / denominator`, normalized. Returns `None` if the /// denominator is zero (the only non-representable input; magnitude is /// handled by promotion). pub fn new(numerator: i64, denominator: i64) -> Option { if denominator == 0 { return None; } Some(Self::from_big(BigRational::new( BigInt::from(numerator), BigInt::from(denominator), ))) } /// Builds from a (reduced or unreduced) [`BigRational`], demoting to /// [`RationalTime::Small`] when the normalized value fits the inline range. /// This is the single chokepoint that maintains the canonical-form /// invariant. pub(crate) fn from_big(value: BigRational) -> Self { // `BigRational` keeps the denominator positive and the fraction // reduced, so the sign lives on the numerator. let numer = value.numer(); let denom = value.denom(); if let (Some(n), Some(d)) = (bigint_to_i32(numer), bigint_to_u32(denom)) { if let Some(d) = NonZeroU32::new(d) { return RationalTime::Small(SmallRational { numerator: n, denominator: d, }); } } RationalTime::Large(Arc::new(value)) } /// The value as a [`BigRational`] (allocates for the inline case; used on /// the slow arithmetic path and for canonical encoding). pub(crate) fn to_big(&self) -> BigRational { match self { RationalTime::Small(s) => { BigRational::new(BigInt::from(s.numerator), BigInt::from(s.denominator.get())) } RationalTime::Large(b) => (**b).clone(), } } /// Whether the value is exactly zero. pub fn is_zero(&self) -> bool { match self { RationalTime::Small(s) => s.numerator == 0, RationalTime::Large(b) => b.is_zero(), } } /// Whether the value is strictly negative. pub fn is_negative(&self) -> bool { match self { RationalTime::Small(s) => s.numerator < 0, RationalTime::Large(b) => b.is_negative(), } } /// A lossy `f64` approximation. For *advisory* use only — tempo conversion, /// spacing hints, diagnostics — never canonical state (musical time is the /// exact rational; Appendix D §"Exact and Quantized Representations"). pub fn to_f64(&self) -> f64 { match self { RationalTime::Small(s) => s.numerator as f64 / s.denominator.get() as f64, RationalTime::Large(b) => { use num_traits::ToPrimitive; b.to_f64().unwrap_or(f64::NAN) } } } /// Exact addition. pub fn add(&self, other: &Self) -> Self { if let (RationalTime::Small(a), RationalTime::Small(b)) = (self, other) { // Fast path: a/b + c/d in widened i128, then fit-or-promote. let (n, d) = (a.numerator as i128, a.denominator.get() as i128); let (n2, d2) = (b.numerator as i128, b.denominator.get() as i128); if let Some(r) = small_from_i128(n * d2 + n2 * d, d * d2) { return r; } } RationalTime::from_big(self.to_big() + other.to_big()) } /// Exact subtraction. pub fn sub(&self, other: &Self) -> Self { if let (RationalTime::Small(a), RationalTime::Small(b)) = (self, other) { let (n, d) = (a.numerator as i128, a.denominator.get() as i128); let (n2, d2) = (b.numerator as i128, b.denominator.get() as i128); if let Some(r) = small_from_i128(n * d2 - n2 * d, d * d2) { return r; } } RationalTime::from_big(self.to_big() - other.to_big()) } /// Exact multiplication (used to scale durations by a tuplet ratio, etc.). pub fn mul(&self, other: &Self) -> Self { if let (RationalTime::Small(a), RationalTime::Small(b)) = (self, other) { let n = a.numerator as i128 * b.numerator as i128; let d = a.denominator.get() as i128 * b.denominator.get() as i128; if let Some(r) = small_from_i128(n, d) { return r; } } RationalTime::from_big(self.to_big() * other.to_big()) } /// Sums a sequence of rationals exactly (left fold). Useful for the /// tuplet- and decomposition-sum invariants (Chapter 5). pub fn sum<'a, I: IntoIterator>(iter: I) -> RationalTime { let mut acc = RationalTime::zero(); for r in iter { acc = acc.add(r); } acc } } /// Reduces `num/den` (with arbitrary-sign `den`) and returns it as a /// [`RationalTime::Small`] iff it fits the inline range; otherwise `None`, /// signalling the caller to take the [`BigRational`] path. fn small_from_i128(mut num: i128, mut den: i128) -> Option { if den == 0 { return None; } if den < 0 { num = -num; den = -den; } let g = { let mut a = num.unsigned_abs(); let mut b = den as u128; while b != 0 { let t = a % b; a = b; b = t; } a.max(1) } as i128; num /= g; den /= g; let n: i32 = i32::try_from(num).ok()?; let d: u32 = u32::try_from(den).ok()?; Some(RationalTime::Small(SmallRational { numerator: n, denominator: NonZeroU32::new(d)?, })) } fn bigint_to_i32(b: &BigInt) -> Option { i32::try_from(b.clone()).ok() } fn bigint_to_u32(b: &BigInt) -> Option { u32::try_from(b.clone()).ok() } impl PartialEq for RationalTime { fn eq(&self, other: &Self) -> bool { match (self, other) { // Canonical-form invariant: equal values share a variant, so the // fast inline comparison is exact for the common case. (RationalTime::Small(a), RationalTime::Small(b)) => a == b, _ => self.cmp(other) == core::cmp::Ordering::Equal, } } } impl Eq for RationalTime {} impl Ord for RationalTime { fn cmp(&self, other: &Self) -> core::cmp::Ordering { if let (RationalTime::Small(a), RationalTime::Small(b)) = (self, other) { // Cross-multiply in i128 to avoid overflow (Chapter 3 §"Equality // and Ordering"). Denominators are positive, so the inequality // direction is preserved. let lhs = a.numerator as i128 * b.denominator.get() as i128; let rhs = b.numerator as i128 * a.denominator.get() as i128; return lhs.cmp(&rhs); } self.to_big().cmp(&other.to_big()) } } impl PartialOrd for RationalTime { #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl core::hash::Hash for RationalTime { fn hash(&self, state: &mut H) { // Within each variant the normalized form is unique, and the // canonical-form invariant guarantees numerically-equal values never // straddle the variant boundary, so per-variant hashing is consistent // with `Eq`. match self { RationalTime::Small(s) => { state.write_u8(0); state.write_i32(s.numerator); state.write_u32(s.denominator.get()); } RationalTime::Large(b) => { state.write_u8(1); let (sign, bytes) = b.numer().to_bytes_be(); state.write_i8(match sign { Sign::Minus => -1, Sign::NoSign => 0, Sign::Plus => 1, }); state.write(&bytes); state.write(&b.denom().to_bytes_be().1); } } } } impl core::fmt::Debug for RationalTime { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { RationalTime::Small(s) => write!(f, "{}/{}", s.numerator, s.denominator.get()), RationalTime::Large(b) => write!(f, "{}/{} (large)", b.numer(), b.denom()), } } } impl Default for RationalTime { fn default() -> Self { RationalTime::zero() } } /// Canonical, arbitrary-precision, reversible byte form for a rational: the /// numerator's sign and big-endian magnitude (length-prefixed), then the /// positive denominator's big-endian magnitude (length-prefixed). The value is /// always reduced first, so equal rationals encode to equal bytes (Appendix D /// §"Canonical serialization determinism"). RATIFIED by Pass 11 (item 1.7, /// P11-4): this primitive layout is now normative in core_spec §"Binary Format /// Companion", Requirement `req:format:rationaltime-encoding`; the full /// composite wire format remains the Binary Format companion's (Agent J), which /// inherits the ratified convention baseline (`req:format:codec-conventions`). impl CanonicalEncode for RationalTime { fn encode_canonical(&self, out: &mut Vec) { let big = self.to_big(); let (sign, numer_mag) = big.numer().to_bytes_be(); let denom_mag = big.denom().to_bytes_be().1; out.push(match sign { Sign::Minus => 2, Sign::NoSign => 0, Sign::Plus => 1, }); out.extend_from_slice(&(numer_mag.len() as u32).to_le_bytes()); out.extend_from_slice(&numer_mag); out.extend_from_slice(&(denom_mag.len() as u32).to_le_bytes()); out.extend_from_slice(&denom_mag); } } impl CanonicalDecode for RationalTime { fn decode_canonical(bytes: &[u8]) -> Result { let mut cur = bytes; let take = |cur: &mut &[u8], n: usize| -> Result, DecodeError> { if cur.len() < n { return Err(DecodeError::UnexpectedLength { expected: n, actual: cur.len(), }); } let (head, tail) = cur.split_at(n); *cur = tail; Ok(head.to_vec()) }; let sign_byte = take(&mut cur, 1)?[0]; let sign = match sign_byte { 0 => Sign::NoSign, 1 => Sign::Plus, 2 => Sign::Minus, _ => return Err(DecodeError::MalformedDomainTag), }; let numer_len = u32::from_le_bytes(take(&mut cur, 4)?.try_into().unwrap()) as usize; let numer_mag = take(&mut cur, numer_len)?; let denom_len = u32::from_le_bytes(take(&mut cur, 4)?.try_into().unwrap()) as usize; let denom_mag = take(&mut cur, denom_len)?; if !cur.is_empty() { return Err(DecodeError::UnexpectedLength { expected: bytes.len() - cur.len(), actual: bytes.len(), }); } let numer = BigInt::from_bytes_be(sign, &numer_mag); let denom = BigInt::from_bytes_be(Sign::Plus, &denom_mag); if denom.is_zero() { return Err(DecodeError::MalformedDomainTag); } Ok(RationalTime::from_big(BigRational::new(numer, denom))) } } /// A point in musical time, relative to the origin of a time region /// (Chapter 3 §"Position and Duration as Distinct Types"). Wraps a /// [`RationalTime`]; adding two positions is intentionally not defined. #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] pub struct MusicalPosition(pub RationalTime); /// A span of musical time (Chapter 3). Wraps a [`RationalTime`]. #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] pub struct MusicalDuration(pub RationalTime); impl MusicalPosition { /// The region origin, `0`. pub fn origin() -> Self { MusicalPosition(RationalTime::zero()) } /// The underlying rational. pub fn rational(&self) -> &RationalTime { &self.0 } } impl MusicalDuration { /// The zero-length duration. pub fn zero() -> Self { MusicalDuration(RationalTime::zero()) } /// A whole note (`1/1`). pub fn whole() -> Self { MusicalDuration(RationalTime::one()) } /// The underlying rational. pub fn rational(&self) -> &RationalTime { &self.0 } /// Whether the duration is strictly positive (the usual well-formedness /// condition for a sounding event). pub fn is_positive(&self) -> bool { !self.0.is_zero() && !self.0.is_negative() } /// Sums a sequence of durations exactly. pub fn sum<'a, I: IntoIterator>(iter: I) -> MusicalDuration { MusicalDuration(RationalTime::sum(iter.into_iter().map(|d| &d.0))) } } // The type-level algebra of Chapter 3 §"Position and Duration as Distinct // Types". `MusicalPosition + MusicalPosition` is deliberately absent. impl Add for MusicalPosition { type Output = MusicalPosition; fn add(self, rhs: MusicalDuration) -> MusicalPosition { MusicalPosition(self.0.add(&rhs.0)) } } impl Add for MusicalDuration { type Output = MusicalDuration; fn add(self, rhs: MusicalDuration) -> MusicalDuration { MusicalDuration(self.0.add(&rhs.0)) } } impl Sub for MusicalPosition { type Output = MusicalDuration; fn sub(self, rhs: MusicalPosition) -> MusicalDuration { MusicalDuration(self.0.sub(&rhs.0)) } } impl Sub for MusicalDuration { type Output = MusicalDuration; fn sub(self, rhs: MusicalDuration) -> MusicalDuration { MusicalDuration(self.0.sub(&rhs.0)) } } macro_rules! delegate_canon { ($name:ident) => { impl CanonicalEncode for $name { #[inline] fn encode_canonical(&self, out: &mut Vec) { self.0.encode_canonical(out); } } impl CanonicalDecode for $name { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { Ok($name(RationalTime::decode_canonical(bytes)?)) } } }; } delegate_canon!(MusicalPosition); delegate_canon!(MusicalDuration); /// A point in wall-clock time, in nanoseconds from a region origin (Chapter 3 /// §"Wall-Clock Time"). 64-bit signed: range ±~292 years. Floating-point /// wall-clock time is forbidden in stored data. #[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] pub struct WallClockTime(pub i64); /// A span of wall-clock time, in nanoseconds. #[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)] pub struct WallClockDuration(pub i64); impl WallClockTime { /// Canonical little-endian bytes (8, `i64`), matching the integer /// convention of [`epiphany_determinism::QuantizedCoord`]. #[inline] pub fn to_le_bytes(self) -> [u8; 8] { self.0.to_le_bytes() } } impl CanonicalEncode for WallClockTime { #[inline] fn encode_canonical(&self, out: &mut Vec) { out.extend_from_slice(&self.0.to_le_bytes()); } } impl CanonicalDecode for WallClockTime { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { let arr: [u8; 8] = bytes .try_into() .map_err(|_| DecodeError::UnexpectedLength { expected: 8, actual: bytes.len(), })?; Ok(WallClockTime(i64::from_le_bytes(arr))) } } impl CanonicalEncode for WallClockDuration { #[inline] fn encode_canonical(&self, out: &mut Vec) { out.extend_from_slice(&self.0.to_le_bytes()); } } impl CanonicalDecode for WallClockDuration { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { let arr: [u8; 8] = bytes .try_into() .map_err(|_| DecodeError::UnexpectedLength { expected: 8, actual: bytes.len(), })?; Ok(WallClockDuration(i64::from_le_bytes(arr))) } } /// Which boundary of a measure an anchor points at. #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum MeasurePosition { Start, End, } /// Which edge of a region an anchor points at. #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum RegionEdge { Start, End, } /// An offset applied to an anchor target (Chapter 3 §"Time Anchors"). The /// admissible variant is constrained by the target's enclosing region's time /// model — see [`OffsetKind`] and invariant 9 in the `invariants` module. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum AnchorOffset { /// Offset in musical time. Valid for targets in metric regions (and /// musical-discipline aleatoric regions). Musical(MusicalDuration), /// Offset in wall-clock time. Valid for targets in proportional regions /// (and wall-clock-discipline aleatoric regions). WallClock(WallClockDuration), /// No offset; the anchor refers to the target's reference point exactly. /// Valid in any region. Zero, } /// The clock an [`AnchorOffset`] is expressed in, used to check it against a /// region's time model (Chapter 3; invariant 9). #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum OffsetKind { Musical, WallClock, Zero, } impl AnchorOffset { /// The clock this offset is expressed in. pub fn kind(&self) -> OffsetKind { match self { AnchorOffset::Musical(_) => OffsetKind::Musical, AnchorOffset::WallClock(_) => OffsetKind::WallClock, AnchorOffset::Zero => OffsetKind::Zero, } } } /// A stored reference to a point in time (Chapter 3 §"Time Anchors"). Stored /// references to *external* time points must anchor to identified objects plus /// offsets, never to absolute positions that could shift under edits. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum TimeAnchor { /// Anchored to a specific event. Survives edits that do not delete it. Event { id: EventId, offset: AnchorOffset }, /// Anchored to a measure boundary. Survives measure reordering. Measure { id: MeasureId, position: MeasurePosition, offset: AnchorOffset, }, /// Anchored to the start or end of a region. Region { id: RegionId, edge: RegionEdge, offset: AnchorOffset, }, /// Anchored to absolute wall-clock time. Used for film and audio sync. WallClock { time: WallClockTime }, } impl TimeAnchor { /// The offset of this anchor, if it has one (a [`TimeAnchor::WallClock`] /// anchor carries no separate offset; its position is absolute). pub fn offset(&self) -> Option<&AnchorOffset> { match self { TimeAnchor::Event { offset, .. } | TimeAnchor::Measure { offset, .. } | TimeAnchor::Region { offset, .. } => Some(offset), TimeAnchor::WallClock { .. } => None, } } } /// An event's position within its owning voice and region (Chapter 5 /// §"Event Position and Duration"). Unioned over the two clocks; the admissible /// variant is fixed by the enclosing region's time model. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum EventPosition { Musical(MusicalPosition), WallClock(WallClockTime), } /// An event's duration (Chapter 5). Unioned over musical, wall-clock, and /// indeterminate forms. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum EventDuration { Musical(MusicalDuration), WallClock(WallClockDuration), Indeterminate(DurationBounds), } /// A concrete (non-indeterminate) duration in one of the two clocks /// (Chapter 5). The bounds of an indeterminate duration are concrete, which /// prevents recursive indeterminacy in the type system. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum ConcreteDuration { Musical(MusicalDuration), WallClock(WallClockDuration), } /// The clock a position/duration coordinate is expressed in, used to check it /// against a region's time model (Chapter 5; invariant 4). #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum CoordinateKind { Musical, WallClock, } impl EventPosition { /// The clock this position is expressed in. pub fn kind(&self) -> CoordinateKind { match self { EventPosition::Musical(_) => CoordinateKind::Musical, EventPosition::WallClock(_) => CoordinateKind::WallClock, } } } impl ConcreteDuration { /// The clock this duration is expressed in. pub fn kind(&self) -> CoordinateKind { match self { ConcreteDuration::Musical(_) => CoordinateKind::Musical, ConcreteDuration::WallClock(_) => CoordinateKind::WallClock, } } } impl EventDuration { /// The concrete clock of a determinate duration, or `None` for an /// indeterminate one. pub fn concrete_kind(&self) -> Option { match self { EventDuration::Musical(_) => Some(CoordinateKind::Musical), EventDuration::WallClock(_) => Some(CoordinateKind::WallClock), EventDuration::Indeterminate(_) => None, } } } /// A bounded interval expressing an indeterminate duration (Chapter 5). Bounds /// are [`ConcreteDuration`] so indeterminacy cannot recurse. #[derive(Clone, PartialEq, Eq, Hash, Debug, Default)] pub struct DurationBounds { pub lower: Option, pub upper: Option, } /// An interval bound for an aleatoric event's start or end (Chapter 3 /// §"Aleatoric Time"). #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum TimeBounds { MusicalRange { min: MusicalPosition, max: MusicalPosition, }, WallClockRange { min: WallClockTime, max: WallClockTime, }, Unbounded, } /// Per-event interval bounds for an aleatoric region (Chapter 3 §"Aleatoric /// Time"): an event may begin/end anywhere within the given windows. #[derive(Clone, PartialEq, Eq, Hash, Debug, Default)] pub struct EventBounds { pub start: Option, pub end: Option, } #[cfg(test)] mod tests { use super::*; fn r(n: i64, d: i64) -> RationalTime { RationalTime::new(n, d).unwrap() } #[test] fn rationals_normalize_and_compare_by_value() { assert_eq!(r(2, 4), r(1, 2)); assert_eq!(r(-3, -6), r(1, 2)); assert!(r(1, 3) < r(1, 2)); assert!(r(-1, 2) < RationalTime::zero()); // 1/4 + 1/12 = 1/3 (the tuplet-eighth example, Chapter 3). assert_eq!(r(1, 4).add(&r(1, 12)), r(1, 3)); // A quintuplet-sixteenth in a duplet-half: 1/2 * 1/2 * 1/5 = 1/20. assert_eq!(r(1, 2).mul(&r(1, 2)).mul(&r(1, 5)), r(1, 20)); } #[test] fn arithmetic_promotes_then_stays_exact() { // Denominators 999_999_937 (prime) and 999_999_893 (prime) multiply to // ~1e18, far past u32; the result must promote and stay exact. let a = r(1, 999_999_937); let b = r(1, 999_999_893); let sum = a.add(&b); assert!(matches!(sum, RationalTime::Large(_)), "must promote"); // Cross-check against an independent BigRational computation. let expect = BigRational::new(BigInt::from(1), BigInt::from(999_999_937i64)) + BigRational::new(BigInt::from(1), BigInt::from(999_999_893i64)); assert_eq!(sum, RationalTime::from_big(expect)); // Subtracting back demotes to the inline value, unobservably. let back = sum.sub(&b); assert_eq!(back, a); assert!(matches!(back, RationalTime::Small(_)), "must demote"); } #[test] fn equal_values_hash_equally_across_construction_paths() { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let h = |x: &RationalTime| { let mut s = DefaultHasher::new(); x.hash(&mut s); s.finish() }; assert_eq!(h(&r(2, 4)), h(&r(1, 2))); assert_eq!(h(&r(6, 3)), h(&RationalTime::from_int(2))); } #[test] fn position_duration_algebra_is_typed() { let p = MusicalPosition(r(1, 2)); let d = MusicalDuration(r(1, 4)); let p2 = p.clone() + d.clone(); // position + duration -> position assert_eq!(p2, MusicalPosition(r(3, 4))); let span = p2 - p; // position - position -> duration assert_eq!(span, MusicalDuration(r(1, 4))); let dd = d.clone() + d; // duration + duration -> duration assert_eq!(dd, MusicalDuration(r(1, 2))); } #[test] fn rational_round_trips_canonically_including_large() { for v in [ RationalTime::zero(), r(1, 1), r(-7, 12), r(3, 1024), r(1, 999_999_937).add(&r(1, 999_999_893)), ] { let bytes = v.to_canonical_bytes(); let back = RationalTime::decode_canonical(&bytes).unwrap(); assert_eq!(back, v); assert_eq!(back.to_canonical_bytes(), bytes, "re-encode byte-stable"); } } #[test] fn equal_rationals_encode_identically() { assert_eq!(r(2, 4).to_canonical_bytes(), r(1, 2).to_canonical_bytes()); assert_eq!( r(6, 3).to_canonical_bytes(), RationalTime::from_int(2).to_canonical_bytes() ); } #[test] fn wallclock_round_trips() { for v in [i64::MIN, -1, 0, 1, 1_000_000_000, i64::MAX] { let t = WallClockTime(v); assert_eq!( WallClockTime::decode_canonical(&t.to_canonical_bytes()).unwrap(), t ); } } }