use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign};
mod frame_rate;
mod offset;
pub use frame_rate::FrameRate;
pub use offset::TimeOffset;
pub const TICKS_PER_SECOND: i64 = 705_600_000;
const TICKS_PER_SECOND_UNSIGNED: u64 = TICKS_PER_SECOND as u64;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Time(i64);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Duration(u64);
impl Time {
pub const ZERO: Self = Self(0);
pub const fn from_ticks(ticks: i64) -> Self {
Self(ticks)
}
pub fn from_seconds(seconds: f64) -> Self {
Self::try_from_seconds(seconds).unwrap_or_else(|error| panic!("{}", error.message))
}
pub fn try_from_seconds(seconds: f64) -> Result<Self, crate::ValidationError> {
try_rounded_ticks(seconds * TICKS_PER_SECOND as f64).map(Self)
}
pub fn from_frame(frame: i64, rate: FrameRate) -> Self {
Self::checked_from_frame(frame, rate).expect("frame time exceeds the signed tick range")
}
pub fn checked_from_frame(frame: i64, rate: FrameRate) -> Option<Self> {
let numerator =
i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
let denominator = i128::from(rate.numerator());
let ticks =
numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
i64::try_from(ticks).ok().map(Self)
}
pub fn to_frame(self, rate: FrameRate) -> i64 {
self.checked_to_frame(rate)
.expect("frame index exceeds the signed 64-bit range")
}
pub fn checked_to_frame(self, rate: FrameRate) -> Option<i64> {
let numerator = i128::from(self.0) * i128::from(rate.numerator());
let denominator = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
i64::try_from(numerator.div_euclid(denominator)).ok()
}
pub fn as_seconds(self) -> f64 {
self.0 as f64 / TICKS_PER_SECOND as f64
}
pub const fn ticks(self) -> i64 {
self.0
}
pub const fn since_origin(self) -> TimeOffset {
TimeOffset::from_ticks(self.0)
}
pub fn duration_since(self, earlier: Self) -> Option<Duration> {
(self.0 >= earlier.0).then(|| Duration(self.0.abs_diff(earlier.0)))
}
pub fn checked_add(self, span: Duration) -> Option<Self> {
i64::try_from(span.0)
.ok()
.and_then(|ticks| self.0.checked_add(ticks))
.map(Self)
}
pub fn checked_sub(self, span: Duration) -> Option<Self> {
i64::try_from(span.0)
.ok()
.and_then(|ticks| self.0.checked_sub(ticks))
.map(Self)
}
}
impl Add<Duration> for Time {
type Output = Self;
fn add(self, span: Duration) -> Self {
self.checked_add(span).expect("time addition overflow")
}
}
impl Sub<Duration> for Time {
type Output = Self;
fn sub(self, span: Duration) -> Self {
self.checked_sub(span).expect("time subtraction overflow")
}
}
impl Add<TimeOffset> for Time {
type Output = Self;
fn add(self, offset: TimeOffset) -> Self {
Self(
self.0
.checked_add(offset.ticks())
.expect("time addition overflow"),
)
}
}
impl Sub<TimeOffset> for Time {
type Output = Self;
fn sub(self, offset: TimeOffset) -> Self {
Self(
self.0
.checked_sub(offset.ticks())
.expect("time subtraction overflow"),
)
}
}
impl Sub for Time {
type Output = TimeOffset;
fn sub(self, other: Self) -> TimeOffset {
TimeOffset::from_ticks(
self.0
.checked_sub(other.0)
.expect("time subtraction overflow"),
)
}
}
impl AddAssign<Duration> for Time {
fn add_assign(&mut self, span: Duration) {
*self = *self + span;
}
}
impl SubAssign<Duration> for Time {
fn sub_assign(&mut self, span: Duration) {
*self = *self - span;
}
}
impl AddAssign<TimeOffset> for Time {
fn add_assign(&mut self, offset: TimeOffset) {
*self = *self + offset;
}
}
impl SubAssign<TimeOffset> for Time {
fn sub_assign(&mut self, offset: TimeOffset) {
*self = *self - offset;
}
}
impl Duration {
pub const ZERO: Self = Self(0);
pub const fn from_ticks(ticks: u64) -> Self {
Self(ticks)
}
pub fn from_seconds(seconds: f64) -> Self {
Self::try_from_seconds(seconds).unwrap_or_else(|error| panic!("{}", error.message))
}
pub fn try_from_seconds(seconds: f64) -> Result<Self, crate::ValidationError> {
try_rounded_unsigned_ticks(seconds * TICKS_PER_SECOND as f64).map(Self)
}
pub fn from_frame(frame: u64, rate: FrameRate) -> Self {
Self::checked_from_frame(frame, rate).expect("frame time exceeds the tick range")
}
pub fn checked_from_frame(frame: u64, rate: FrameRate) -> Option<Self> {
let numerator = u128::from(frame)
* u128::from(TICKS_PER_SECOND_UNSIGNED)
* u128::from(rate.denominator());
let denominator = u128::from(rate.numerator());
let ticks = numerator / denominator + u128::from(numerator % denominator != 0);
u64::try_from(ticks).ok().map(Self)
}
pub fn to_frame(self, rate: FrameRate) -> u64 {
self.checked_to_frame(rate)
.expect("frame index exceeds the unsigned 64-bit range")
}
pub fn checked_to_frame(self, rate: FrameRate) -> Option<u64> {
let numerator = u128::from(self.0) * u128::from(rate.numerator());
let denominator = u128::from(TICKS_PER_SECOND_UNSIGNED) * u128::from(rate.denominator());
u64::try_from(numerator / denominator).ok()
}
pub fn as_seconds(self) -> f64 {
self.0 as f64 / TICKS_PER_SECOND as f64
}
pub const fn ticks(self) -> u64 {
self.0
}
pub fn after_origin(self) -> Time {
Time(i64::try_from(self.0).expect("duration exceeds the signed tick range"))
}
pub fn checked_offset(self) -> Option<TimeOffset> {
i64::try_from(self.0).ok().map(TimeOffset::from_ticks)
}
pub fn offset(self) -> TimeOffset {
self.checked_offset()
.expect("duration exceeds the signed tick range")
}
pub fn checked_sub(self, other: Self) -> Option<Self> {
self.0.checked_sub(other.0).map(Self)
}
pub fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
pub fn checked_mul(self, factor: u64) -> Option<Self> {
self.0.checked_mul(factor).map(Self)
}
pub fn try_mul(self, factor: f64) -> Result<Self, crate::ValidationError> {
if factor == 1.0 {
return Ok(self);
}
if !factor.is_finite() || factor < 0.0 {
return Err(time_error(
"factor",
"duration factor must be finite and nonnegative",
));
}
try_rounded_unsigned_ticks(self.0 as f64 * factor).map(Self)
}
pub fn try_div(self, divisor: f64) -> Result<Self, crate::ValidationError> {
if !divisor.is_finite() || divisor <= 0.0 {
return Err(time_error(
"divisor",
"duration divisor must be finite and positive",
));
}
if divisor == 1.0 {
return Ok(self);
}
try_rounded_unsigned_ticks(self.0 as f64 / divisor).map(Self)
}
pub fn try_ratio(self, divisor: Self) -> Result<f64, crate::ValidationError> {
if divisor.0 == 0 {
return Err(time_error(
"divisor",
"duration ratio requires a nonzero divisor",
));
}
Ok(self.0 as f64 / divisor.0 as f64)
}
}
impl Add for Duration {
type Output = Self;
fn add(self, other: Self) -> Self {
Self(
self.0
.checked_add(other.0)
.expect("duration addition overflow"),
)
}
}
impl Sub for Duration {
type Output = Self;
fn sub(self, other: Self) -> Self {
self.checked_sub(other)
.expect("duration subtraction underflow")
}
}
impl AddAssign for Duration {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}
impl SubAssign for Duration {
fn sub_assign(&mut self, other: Self) {
*self = *self - other;
}
}
impl std::iter::Sum for Duration {
fn sum<Spans: Iterator<Item = Self>>(spans: Spans) -> Self {
spans.fold(Self::ZERO, |total, span| total + span)
}
}
impl Mul<f64> for Duration {
type Output = Self;
fn mul(self, factor: f64) -> Self {
self.try_mul(factor)
.unwrap_or_else(|error| panic!("{}", error.message))
}
}
macro_rules! integer_duration_mul {
($($integer:ty),+ $(,)?) => {$ (
impl Mul<$integer> for Duration {
type Output = Duration;
fn mul(self, factor: $integer) -> Duration {
Duration(u64::try_from(u128::from(self.0) * u128::try_from(factor).expect("duration multiplication overflow")).expect("duration multiplication overflow"))
}
}
impl Mul<Duration> for $integer {
type Output = Duration;
fn mul(self, span: Duration) -> Duration { span * self }
}
)+};
}
integer_duration_mul!(i32, i64, u32, u64, usize);
impl Mul<Duration> for f64 {
type Output = Duration;
fn mul(self, span: Duration) -> Duration {
span * self
}
}
impl Div<f64> for Duration {
type Output = Self;
fn div(self, divisor: f64) -> Self {
self.try_div(divisor)
.unwrap_or_else(|error| panic!("{}", error.message))
}
}
impl Div for Duration {
type Output = f64;
fn div(self, divisor: Self) -> f64 {
self.try_ratio(divisor)
.unwrap_or_else(|error| panic!("{}", error.message))
}
}
pub trait TimeLiteral {
fn seconds(self) -> Duration;
fn milliseconds(self) -> Duration;
}
impl TimeLiteral for i64 {
fn seconds(self) -> Duration {
Duration(
u64::try_from(self)
.ok()
.and_then(|seconds| seconds.checked_mul(TICKS_PER_SECOND_UNSIGNED))
.expect("seconds overflow"),
)
}
fn milliseconds(self) -> Duration {
Duration(
u64::try_from(self)
.ok()
.and_then(|milliseconds| {
milliseconds.checked_mul(TICKS_PER_SECOND_UNSIGNED / 1_000)
})
.expect("milliseconds overflow"),
)
}
}
impl TimeLiteral for i32 {
fn seconds(self) -> Duration {
i64::from(self).seconds()
}
fn milliseconds(self) -> Duration {
i64::from(self).milliseconds()
}
}
impl TimeLiteral for u64 {
fn seconds(self) -> Duration {
Duration(
self.checked_mul(TICKS_PER_SECOND_UNSIGNED)
.expect("seconds overflow"),
)
}
fn milliseconds(self) -> Duration {
Duration(
self.checked_mul(TICKS_PER_SECOND_UNSIGNED / 1_000)
.expect("milliseconds overflow"),
)
}
}
macro_rules! unsigned_time_literal {
($($integer:ty),+ $(,)?) => {$ (
impl TimeLiteral for $integer {
fn seconds(self) -> Duration { u64::try_from(self).expect("seconds overflow").seconds() }
fn milliseconds(self) -> Duration { u64::try_from(self).expect("milliseconds overflow").milliseconds() }
}
)+};
}
unsigned_time_literal!(u32, usize);
impl TimeLiteral for f64 {
fn seconds(self) -> Duration {
Duration::from_seconds(self)
}
fn milliseconds(self) -> Duration {
Duration(
try_rounded_unsigned_ticks(self * (TICKS_PER_SECOND / 1_000) as f64)
.unwrap_or_else(|error| panic!("{}", error.message)),
)
}
}
fn try_rounded_ticks(ticks: f64) -> Result<i64, crate::ValidationError> {
let rounded = ticks.round();
if rounded.is_finite() && rounded >= i64::MIN as f64 && rounded < -(i64::MIN as f64) {
Ok(rounded as i64)
} else {
Err(time_error(
"seconds",
"time must be finite and fit the signed tick range",
))
}
}
fn try_rounded_unsigned_ticks(ticks: f64) -> Result<u64, crate::ValidationError> {
let rounded = ticks.round();
if rounded.is_finite() && rounded >= 0.0 && rounded < u64::MAX as f64 {
Ok(rounded as u64)
} else {
Err(time_error(
"seconds",
"duration must be finite, nonnegative and fit the tick range",
))
}
}
fn time_error(field: &'static str, message: &'static str) -> crate::ValidationError {
crate::ValidationError::invalid("time", field, message)
}
#[cfg(test)]
mod tests;