use std::ops::{Add, Div, Sub};
use crate::math::{Fraction, Number};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Time {
Clock(ClockTime),
Beat(BeatTime),
}
impl PartialOrd for Time {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self, other) {
(Time::Clock(x), Time::Clock(y)) => x.0.partial_cmp(&y.0),
(Time::Beat(x), Time::Beat(y)) => x.0.partial_cmp(&y.0),
_ => None,
}
}
}
impl Time {
pub fn beat(beat_num: impl Into<Number>) -> Option<Self> {
BeatTime::from_number(beat_num.into()).map(Self::Beat)
}
pub fn clock_milis(ms: impl Into<Number>) -> Option<Self> {
ClockTime::from_milis(ms).map(Self::Clock)
}
pub fn clock_secs(secs: impl Into<Number>) -> Option<Self> {
ClockTime::from_seconds(secs).map(Self::Clock)
}
pub fn clock_secs_fraction(secs: Fraction) -> Self {
Time::Clock(ClockTime::from_fraction_seconds(secs))
}
pub fn beat_fraction(beats: Fraction) -> Self {
Time::Beat(BeatTime::from_fraction(beats))
}
}
#[derive(Debug, Clone, Copy)]
pub struct SameUnitTime<'id>(Time, generativity::Id<'id>);
impl From<SameUnitTime<'_>> for Time {
fn from(value: SameUnitTime<'_>) -> Self {
value.0
}
}
impl Time {
pub fn prove_same_unit<'id>(
self,
other: Time,
guard: generativity::Guard<'id>,
) -> Option<(SameUnitTime<'id>, SameUnitTime<'id>)> {
let id = guard.into();
match (self, other) {
(Time::Clock(_), Time::Clock(_)) | (Time::Beat(_), Time::Beat(_)) => {
Some((SameUnitTime(self, id), SameUnitTime(other, id)))
}
_ => None,
}
}
}
impl<'id> SameUnitTime<'id> {
pub fn prove_same_unit(self, other: Time) -> Option<SameUnitTime<'id>> {
match (self.0, other) {
(Time::Clock(_), Time::Clock(_)) | (Time::Beat(_), Time::Beat(_)) => {
Some(SameUnitTime(other, self.1))
}
_ => None,
}
}
pub fn as_time(&self) -> Time {
self.0
}
}
impl<'id> Add for SameUnitTime<'id> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
match (self.0, rhs.0) {
(Time::Clock(x), Time::Clock(y)) => {
SameUnitTime(Time::Clock(ClockTime(non_nan_add(x.0, y.0))), self.1)
}
(Time::Beat(x), Time::Beat(y)) => {
SameUnitTime(Time::Beat(BeatTime(non_nan_add(x.0, y.0))), self.1)
}
_ => unreachable!("Both are proven to have the same unit"),
}
}
}
impl<'id> Sub for SameUnitTime<'id> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
match (self.0, rhs.0) {
(Time::Clock(x), Time::Clock(y)) => {
SameUnitTime(Time::Clock(ClockTime(non_nan_sub(x.0, y.0))), self.1)
}
(Time::Beat(x), Time::Beat(y)) => {
SameUnitTime(Time::Beat(BeatTime(non_nan_sub(x.0, y.0))), self.1)
}
_ => unreachable!("Both are proven to have the same unit"),
}
}
}
impl<'id> Div for SameUnitTime<'id> {
type Output = f64;
fn div(self, rhs: Self) -> Self::Output {
match (self.0, rhs.0) {
(Time::Clock(x), Time::Clock(y)) => x.seconds() / y.seconds(),
(Time::Beat(x), Time::Beat(y)) => x.beats() / y.beats(),
_ => unreachable!("Both are proven to have the same unit"),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClockTime(Number);
impl Eq for ClockTime {}
impl PartialOrd for ClockTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ClockTime {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.partial_cmp(&other.0).expect("Time can not be NaN")
}
}
impl ClockTime {
pub const ZERO: Self = Self(Number::Float(0.0));
pub const ONE: Self = Self(Number::Float(1.0));
pub const INF: Self = Self(Number::Float(f64::INFINITY));
pub const NEG_INF: Self = Self(Number::Float(f64::NEG_INFINITY));
pub fn from_seconds(secs: impl Into<Number>) -> Option<Self> {
let secs = secs.into();
(!secs.is_nan()).then_some(Self(secs))
}
pub fn from_fraction_seconds(secs: Fraction) -> Self {
Self(Number::Fraction(secs))
}
pub fn from_milis(milis: impl Into<Number>) -> Option<Self> {
let milis = milis.into();
milis
.checked_mul(Number::Fraction(Fraction::new(1, 1000)?))
.map(Self)
}
pub const fn from_duration(dur: std::time::Duration) -> Self {
Self(Number::Float(dur.as_secs_f64()))
}
pub fn seconds(self) -> f64 {
self.0.to_f64()
}
pub fn seconds_number(self) -> Number {
self.0
}
pub fn duration(self) -> Result<std::time::Duration, std::time::TryFromFloatSecsError> {
std::time::Duration::try_from_secs_f64(self.seconds())
}
pub fn scale(self, factor: f64) -> Option<Self> {
self.0.checked_mul(Number::from(factor)).map(Self)
}
pub fn scale_fraction(self, factor: Fraction) -> Option<Self> {
self.0.checked_mul(Number::Fraction(factor)).map(Self)
}
}
impl Add for ClockTime {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(non_nan_add(self.0, rhs.0))
}
}
impl Sub for ClockTime {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(non_nan_sub(self.0, rhs.0))
}
}
impl Div for ClockTime {
type Output = f64;
fn div(self, rhs: Self) -> Self::Output {
self.seconds() / rhs.seconds()
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BeatTime(Number);
impl Eq for BeatTime {}
impl PartialOrd for BeatTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BeatTime {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.partial_cmp(&other.0).expect("Time can not be NaN")
}
}
impl BeatTime {
pub const ZERO: Self = Self(Number::Float(0.0));
pub const ONE: Self = Self(Number::Float(1.0));
pub const INF: Self = Self(Number::Float(f64::INFINITY));
pub fn new(beats: f64) -> Option<Self> {
Self::from_number(Number::from(beats))
}
pub fn from_fraction(beats: Fraction) -> Self {
Self(Number::Fraction(beats))
}
pub fn from_number(number: Number) -> Option<Self> {
(!number.is_nan()).then_some(Self(number))
}
pub fn beats(self) -> f64 {
self.0.to_f64()
}
pub fn beats_number(self) -> Number {
self.0
}
pub fn scale(self, factor: f64) -> Option<Self> {
self.0.checked_mul(Number::from(factor)).map(Self)
}
pub fn scale_fraction(self, factor: Fraction) -> Option<Self> {
self.0.checked_mul(Number::Fraction(factor)).map(Self)
}
}
impl Add for BeatTime {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(non_nan_add(self.0, rhs.0))
}
}
impl Sub for BeatTime {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(non_nan_sub(self.0, rhs.0))
}
}
impl Div for BeatTime {
type Output = f64;
fn div(self, rhs: Self) -> Self::Output {
self.beats() / rhs.beats()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SameUnitTimeVec {
Clock(Vec<ClockTime>),
Beat(Vec<BeatTime>),
}
impl SameUnitTimeVec {
pub fn try_from_iter(times: impl ExactSizeIterator<Item = Time>) -> Option<Self> {
let len = times.len();
let mut peekable = times.peekable();
let Some(first) = peekable.peek().copied() else {
return Some(Self::Clock(vec![]));
};
match first {
Time::Clock(_) => {
let mut v = Vec::with_capacity(len);
for t in peekable {
let Time::Clock(t) = t else {
return None;
};
v.push(t);
}
Some(Self::Clock(v))
}
Time::Beat(_) => {
let mut v = Vec::with_capacity(len);
for t in peekable {
let Time::Beat(t) = t else {
return None;
};
v.push(t);
}
Some(Self::Beat(v))
}
}
}
pub fn from_proven_iter<'id>(times: impl ExactSizeIterator<Item = SameUnitTime<'id>>) -> Self {
Self::try_from_iter(times.map(|x| x.into())).expect("Proven to be same unit")
}
pub fn len(&self) -> usize {
match self {
Self::Clock(times) => times.len(),
Self::Beat(times) => times.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::Clock(times) => times.is_empty(),
Self::Beat(times) => times.is_empty(),
}
}
pub fn index(&self, i: usize) -> Time {
match self {
Self::Clock(times) => Time::Clock(times[i]),
Self::Beat(times) => Time::Beat(times[i]),
}
}
pub fn unit_match(&self, t: Time) -> bool {
matches!(
(self, t),
(SameUnitTimeVec::Clock(_), Time::Clock(_)) | (SameUnitTimeVec::Beat(_), Time::Beat(_))
)
}
pub fn try_insert(&mut self, index: usize, time: Time) -> Result<(), SameUnitTimeVecEditError> {
match (self, time) {
(SameUnitTimeVec::Clock(list), Time::Clock(t)) => {
if index > list.len() {
return Err(SameUnitTimeVecEditError::OutOfBound);
}
list.insert(index, t);
Ok(())
}
(SameUnitTimeVec::Beat(list), Time::Beat(t)) => {
if index > list.len() {
return Err(SameUnitTimeVecEditError::OutOfBound);
}
list.insert(index, t);
Ok(())
}
_ => Err(SameUnitTimeVecEditError::UnitMismatch),
}
}
pub fn try_set(&mut self, index: usize, time: Time) -> Result<(), SameUnitTimeVecEditError> {
match (self, time) {
(SameUnitTimeVec::Clock(list), Time::Clock(t)) => {
if index >= list.len() {
return Err(SameUnitTimeVecEditError::OutOfBound);
}
list[index] = t;
Ok(())
}
(SameUnitTimeVec::Beat(list), Time::Beat(t)) => {
if index >= list.len() {
return Err(SameUnitTimeVecEditError::OutOfBound);
}
list[index] = t;
Ok(())
}
_ => Err(SameUnitTimeVecEditError::UnitMismatch),
}
}
pub fn remove(&mut self, index: usize) -> Time {
match self {
SameUnitTimeVec::Clock(list) => Time::Clock(list.remove(index)),
SameUnitTimeVec::Beat(list) => Time::Beat(list.remove(index)),
}
}
pub fn swap(&mut self, i: usize, j: usize) {
match self {
SameUnitTimeVec::Clock(list) => list.swap(i, j),
SameUnitTimeVec::Beat(list) => list.swap(i, j),
}
}
pub fn read_with_guard<'id>(
&'id self,
g: generativity::Guard<'id>,
) -> SameUnitTimeVecRead<'id> {
SameUnitTimeVecRead(self, g.into())
}
}
pub struct SameUnitTimeVecRead<'id>(&'id SameUnitTimeVec, generativity::Id<'id>);
impl<'id> SameUnitTimeVecRead<'id> {
pub fn index(&self, i: usize) -> SameUnitTime<'id> {
SameUnitTime(self.0.index(i), self.1)
}
pub fn prove_same_unit(self, other: Time) -> Option<SameUnitTime<'id>> {
match (self.0, other) {
(SameUnitTimeVec::Clock(_), Time::Clock(_))
| (SameUnitTimeVec::Beat(_), Time::Beat(_)) => Some(SameUnitTime(other, self.1)),
_ => None,
}
}
}
#[derive(Debug)]
pub enum SameUnitTimeVecEditError {
OutOfBound,
UnitMismatch,
}
impl std::fmt::Display for SameUnitTimeVecEditError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SameUnitTimeVecEditError::OutOfBound => {
write!(f, "Indexing a vec with an index that is out of bound")
}
SameUnitTimeVecEditError::UnitMismatch => {
write!(
f,
"Adding an Time to a SameUnitVec that does not have matching unit"
)
}
}
}
}
impl std::error::Error for SameUnitTimeVecEditError {}
impl Default for SameUnitTimeVec {
fn default() -> Self {
Self::Clock(vec![])
}
}
fn non_nan_add(a: Number, b: Number) -> Number {
a.checked_add(b).unwrap_or(Number::Float(f64::INFINITY))
}
fn non_nan_sub(a: Number, b: Number) -> Number {
a.checked_sub(b).unwrap_or(Number::Float(f64::INFINITY))
}
#[cfg(test)]
mod tests {
use super::{BeatTime, ClockTime, Time};
use crate::math::Fraction;
fn fraction(numerator: i32, denominator: i32) -> Fraction {
Fraction::new(numerator, denominator).unwrap()
}
#[test]
fn fraction_constructors_keep_exact_values() {
let half = fraction(1, 2);
let beat = BeatTime::from_fraction(half);
let clock = ClockTime::from_fraction_seconds(half);
let millis = ClockTime::from_milis(fraction(125, 1)).unwrap();
assert_eq!(beat.beats_number(), half.into());
assert_eq!(clock.seconds_number(), half.into());
assert_eq!(millis.seconds_number(), fraction(1, 8).into());
assert_eq!(
Time::clock_milis(fraction(125, 1)).unwrap(),
Time::Clock(millis)
);
}
#[test]
fn fraction_scaling_stays_exact_and_reports_overflow() {
let time = BeatTime::from_fraction(fraction(2, 3));
assert_eq!(
time.scale_fraction(fraction(3, 4)).unwrap().beats_number(),
fraction(1, 2).into()
);
assert_eq!(
BeatTime::from_fraction(fraction(i32::MAX, 1)).scale_fraction(fraction(2, 1)),
None
);
}
#[test]
fn arithmetic_saturates_checked_failures_at_positive_infinity() {
let too_large = ClockTime::from_fraction_seconds(fraction(i32::MAX, 1));
assert_eq!(
too_large + ClockTime::from_fraction_seconds(fraction(1, 1)),
ClockTime::INF
);
assert_eq!(ClockTime::INF - ClockTime::INF, ClockTime::INF);
assert_eq!(ClockTime::NEG_INF + ClockTime::INF, ClockTime::INF);
}
#[test]
fn time_ordering_uses_exact_fraction_values() {
let fraction_clock = ClockTime::from_fraction_seconds(fraction(1, 10));
let float_clock = ClockTime::from_seconds(0.1).unwrap();
assert_eq!(
fraction_clock.partial_cmp(&float_clock),
Some(std::cmp::Ordering::Less)
);
assert_eq!(fraction_clock.cmp(&float_clock), std::cmp::Ordering::Less);
assert_ne!(fraction_clock, float_clock);
let fraction_beat = BeatTime::from_fraction(fraction(1, 10));
let float_beat = BeatTime::new(0.1).unwrap();
assert_eq!(
fraction_beat.partial_cmp(&float_beat),
Some(std::cmp::Ordering::Less)
);
assert_eq!(fraction_beat.cmp(&float_beat), std::cmp::Ordering::Less);
assert_ne!(fraction_beat, float_beat);
}
#[test]
fn floating_point_constructors_still_reject_nan() {
assert!(ClockTime::from_seconds(f64::NAN).is_none());
assert!(BeatTime::new(f64::NAN).is_none());
assert!(Time::beat(f64::NAN).is_none());
assert!(Time::clock_secs(f64::NAN).is_none());
assert!(Time::clock_milis(f64::NAN).is_none());
}
}