#[cfg(feature = "chrono")]
pub mod chrono;
#[cfg(feature = "rand")]
pub mod rand;
#[cfg(feature = "serde")]
pub mod serde;
use core::fmt;
use std::cmp::Ordering;
use std::cmp::max;
use std::cmp::min;
use std::error::Error;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::iter::Sum;
use std::ops::Add;
use std::ops::AddAssign;
use std::ops::Deref;
use std::ops::Div;
use std::ops::DivAssign;
use std::ops::Mul;
use std::ops::MulAssign;
use std::ops::Neg;
use std::ops::Rem;
use std::ops::RemAssign;
use std::ops::Sub;
use std::ops::SubAssign;
use std::str::FromStr;
use std::time::SystemTime;
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Copy, Clone, Default)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct Time(i64);
impl Time {
pub const MAX: Self = Self(i64::MAX);
pub const EPOCH: Self = Self(0);
const SECOND: Time = Time(1000);
const MINUTE: Time = Time(60 * Self::SECOND.0);
const HOUR: Time = Time(60 * Self::MINUTE.0);
#[must_use]
pub const fn millis(millis: i64) -> Self {
Time(millis)
}
#[must_use]
pub const fn seconds(seconds: i64) -> Self {
Time::millis(seconds * Self::SECOND.0)
}
#[must_use]
pub const fn minutes(minutes: i64) -> Self {
Time::millis(minutes * Self::MINUTE.0)
}
#[must_use]
pub const fn hours(hours: i64) -> Self {
Time::millis(hours * Self::HOUR.0)
}
#[must_use]
pub fn now() -> Time {
Time::from(SystemTime::now())
}
#[must_use]
pub const fn as_seconds(&self) -> i64 {
self.0 / Self::SECOND.0
}
#[must_use]
pub const fn as_millis(&self) -> i64 {
self.0
}
#[must_use]
pub const fn as_subsecond_nanos(&self) -> i32 {
(self.0 % Self::SECOND.0 * 1_000_000) as i32
}
#[must_use]
pub const fn round_down(&self, step_size: Duration) -> Time {
let time_milli = self.as_millis();
let part = time_milli % step_size.as_millis().abs();
Time::millis(time_milli - part)
}
#[must_use]
pub const fn round_up(&self, step_size: Duration) -> Time {
let time_milli = self.as_millis();
let step_milli = step_size.as_millis().abs();
let part = time_milli % step_milli;
let remaining = (step_milli - part) % step_milli;
Time::millis(time_milli + remaining)
}
#[must_use]
pub fn checked_sub(&self, rhs: Duration) -> Option<Self> {
if Time::EPOCH + rhs > *self {
None
} else {
Some(*self - rhs)
}
}
#[must_use]
pub const fn since_epoch(&self) -> Duration {
Duration::millis(self.as_millis())
}
}
impl Deref for Time {
type Target = i64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Time> for i64 {
fn from(time: Time) -> Self {
time.0
}
}
impl From<i64> for Time {
fn from(value: i64) -> Self {
Self(value)
}
}
impl TryFrom<Duration> for Time {
type Error = &'static str;
fn try_from(duration: Duration) -> Result<Self, Self::Error> {
if duration.is_non_negative() {
Ok(Time::millis(duration.as_millis()))
} else {
Err("Duration cannot be negative.")
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct TimeIsNegativeError;
impl TryFrom<Time> for SystemTime {
type Error = TimeIsNegativeError;
fn try_from(input: Time) -> Result<Self, Self::Error> {
u64::try_from(input.0).map_or(Err(TimeIsNegativeError), |t| {
Ok(std::time::UNIX_EPOCH + std::time::Duration::from_millis(t))
})
}
}
impl From<SystemTime> for Time {
fn from(input: SystemTime) -> Self {
let duration = match input.duration_since(SystemTime::UNIX_EPOCH) {
Ok(std_dur) => Duration::from(std_dur),
Err(err) => -Duration::from(err.duration()),
};
Self::millis(duration.as_millis())
}
}
impl Debug for Time {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let positive = self.0 >= 0;
let mut total = self.0.unsigned_abs();
let millis_part = total % 1000;
total -= millis_part;
let seconds_part = (total % (1000 * 60)) / 1000;
total -= seconds_part;
let minutes_part = (total % (1000 * 60 * 60)) / (1000 * 60);
total -= minutes_part;
let hours_part = total / (1000 * 60 * 60);
if !positive {
f.write_str("-")?;
}
write!(f, "{hours_part:02}:")?;
write!(f, "{minutes_part:02}:")?;
write!(f, "{seconds_part:02}")?;
if millis_part > 0 {
write!(f, ".{millis_part:03}")?;
}
Ok(())
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum TimeWindowError {
StartAfterEnd,
}
impl Display for TimeWindowError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::StartAfterEnd => "time window start is after end",
};
write!(f, "{message}")
}
}
impl Error for TimeWindowError {}
#[derive(Clone, Debug, Eq, PartialEq, Default, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct TimeWindow {
start: Time,
end: Time,
}
impl TimeWindow {
#[must_use]
pub fn new(start: Time, end: Time) -> Self {
debug_assert!(start <= end);
TimeWindow {
start,
end: end.max(start),
}
}
pub fn new_checked(start: Time, end: Time) -> Result<Self, TimeWindowError> {
if start <= end {
Ok(TimeWindow { start, end })
} else {
Err(TimeWindowError::StartAfterEnd)
}
}
#[must_use]
pub fn epoch_to(end: Time) -> Self {
Self::new(Time::EPOCH, end)
}
#[must_use]
pub fn from_minutes(a: i64, b: i64) -> Self {
TimeWindow::new(Time::minutes(a), Time::minutes(b))
}
#[must_use]
pub fn from_seconds(a: i64, b: i64) -> Self {
TimeWindow::new(Time::seconds(a), Time::seconds(b))
}
#[must_use]
pub fn from_length_starting_at(length: Duration, start: Time) -> Self {
TimeWindow::new(start, start.add(length.max(Duration::ZERO)))
}
#[must_use]
pub fn from_length_ending_at(length: Duration, end: Time) -> Self {
TimeWindow::new(end.sub(length.max(Duration::ZERO)), end)
}
#[must_use]
pub const fn instant(time: Time) -> Self {
TimeWindow {
start: time,
end: time,
}
}
#[must_use]
pub const fn widest() -> Self {
TimeWindow {
start: Time::EPOCH,
end: Time::MAX,
}
}
#[must_use]
pub fn instant_seconds(seconds: i64) -> Self {
TimeWindow::from_seconds(seconds, seconds)
}
#[must_use]
pub const fn start(&self) -> Time {
self.start
}
#[must_use]
pub const fn end(&self) -> Time {
self.end
}
#[must_use]
pub fn length(&self) -> Duration {
self.end - self.start
}
#[must_use]
pub fn with_start(&self, new_start: Time) -> Self {
Self::new(new_start.min(self.end), self.end)
}
#[must_use]
pub fn with_end(&self, new_end: Time) -> Self {
Self::new(self.start, new_end.max(self.start))
}
#[must_use]
pub fn prepone_start_to(&self, new_start: Time) -> Self {
self.with_start(self.start.min(new_start))
}
#[must_use]
pub fn prepone_start_by(&self, duration: Duration) -> Self {
self.with_start(self.start - duration.max(Duration::ZERO))
}
#[must_use]
pub fn prepone_start_extend_to(&self, new_length: Duration) -> Self {
self.with_start(self.end - new_length.max(self.length()))
}
#[must_use]
pub fn postpone_start_to(&self, new_start: Time) -> Self {
self.with_start(self.start.max(new_start))
}
#[must_use]
pub fn postpone_start_by(&self, duration: Duration) -> Self {
self.with_start(self.start + duration.max(Duration::ZERO))
}
#[must_use]
pub fn postpone_start_shrink_to(&self, new_length: Duration) -> Self {
let length = new_length
.min(self.length()) .max(Duration::ZERO); self.with_start(self.end - length)
}
#[must_use]
pub fn prepone_end_to(&self, new_end: Time) -> Self {
self.with_end(self.end.min(new_end))
}
#[must_use]
pub fn prepone_end_by(&self, duration: Duration) -> Self {
self.with_end(self.end - duration.max(Duration::ZERO))
}
#[must_use]
pub fn prepone_end_shrink_to(&self, new_length: Duration) -> Self {
let length = new_length
.min(self.length()) .max(Duration::ZERO); self.with_end(self.start + length)
}
#[must_use]
pub fn postpone_end_to(&self, new_end: Time) -> Self {
self.with_end(self.end.max(new_end))
}
#[must_use]
pub fn postpone_end_by(&self, duration: Duration) -> Self {
self.with_end(self.end + duration.max(Duration::ZERO))
}
#[must_use]
pub fn postpone_end_extend_to(&self, new_length: Duration) -> Self {
self.with_end(self.start + new_length.max(self.length()))
}
#[must_use]
pub fn contains(&self, that: Time) -> bool {
self.start <= that && that <= self.end
}
#[must_use]
pub fn overlaps(&self, that: &TimeWindow) -> bool {
self.start < that.end && that.start < self.end
}
#[must_use]
pub fn intersect(&self, that: &TimeWindow) -> Option<TimeWindow> {
let start = max(self.start, that.start);
let end = min(self.end, that.end);
(start <= end).then(|| TimeWindow::new(start, end))
}
pub fn shift(&mut self, duration: Duration) {
self.start += duration;
self.end += duration;
}
}
impl From<TimeWindow> for (Time, Time) {
fn from(tw: TimeWindow) -> Self {
(tw.start, tw.end)
}
}
impl From<(Time, Time)> for TimeWindow {
fn from((start, end): (Time, Time)) -> Self {
Self { start, end }
}
}
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Hash)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct Duration(i64);
impl Duration {
pub const ZERO: Self = Self(0_i64);
pub const MAX: Self = Self(i64::MAX);
const SECOND: Duration = Duration(1000);
const MINUTE: Duration = Duration(60 * Self::SECOND.0);
const HOUR: Duration = Duration(60 * Self::MINUTE.0);
#[must_use]
pub const fn hours(hours: i64) -> Self {
Duration(hours * Self::HOUR.0)
}
#[must_use]
pub const fn minutes(minutes: i64) -> Self {
Duration(minutes * Self::MINUTE.0)
}
#[must_use]
pub const fn seconds(seconds: i64) -> Self {
Duration(seconds * Self::SECOND.0)
}
#[must_use]
pub const fn millis(ms: i64) -> Self {
Duration(ms)
}
#[must_use]
pub fn abs(&self) -> Self {
if self >= &Duration::ZERO {
*self
} else {
-*self
}
}
#[must_use]
pub const fn as_millis(&self) -> i64 {
self.0
}
#[must_use]
pub const fn as_millis_unsigned(&self) -> u64 {
as_unsigned(self.0)
}
#[must_use]
pub const fn as_seconds(&self) -> i64 {
self.0 / Self::SECOND.0
}
#[must_use]
pub const fn as_seconds_unsigned(&self) -> u64 {
as_unsigned(self.0 / 1000)
}
#[must_use]
pub const fn as_minutes(&self) -> i64 {
self.0 / Self::MINUTE.0
}
#[must_use]
pub const fn is_non_negative(&self) -> bool {
self.0 >= 0
}
#[must_use]
pub const fn is_positive(&self) -> bool {
self.0 > 0
}
}
impl Deref for Duration {
type Target = i64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Duration> for i64 {
fn from(time: Duration) -> Self {
time.0
}
}
impl From<i64> for Duration {
fn from(value: i64) -> Self {
Self(value)
}
}
impl Neg for Duration {
type Output = Self;
fn neg(self) -> Self::Output {
Self(-self.0)
}
}
impl Sum for Duration {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(Self::ZERO, Add::add)
}
}
impl PartialEq<std::time::Duration> for Duration {
fn eq(&self, other: &std::time::Duration) -> bool {
(u128::from(self.as_millis_unsigned())).eq(&other.as_millis())
}
}
impl PartialOrd<std::time::Duration> for Duration {
fn partial_cmp(&self, other: &std::time::Duration) -> Option<Ordering> {
(u128::from(self.as_millis_unsigned())).partial_cmp(&other.as_millis())
}
}
impl Display for Duration {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.0 == 0 {
return write!(f, "0ms");
}
let mut string = String::new();
if self.0 < 0 {
string.push('-');
}
let abs = self.0.abs();
let ms = abs % 1000;
let s = (abs / 1000) % 60;
let m = (abs / 60000) % 60;
let h = abs / (60 * 60 * 1000);
if h > 0 {
string.push_str(&h.to_string());
string.push('h');
}
if m > 0 {
string.push_str(&m.to_string());
string.push('m');
}
if s > 0 {
string.push_str(&s.to_string());
string.push('s');
}
if ms > 0 {
string.push_str(&ms.to_string());
string.push_str("ms");
}
write!(f, "{string}")
}
}
impl Debug for Duration {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
impl From<f64> for Duration {
fn from(num: f64) -> Self {
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
{
Duration::millis(num.round() as i64)
}
}
}
impl From<Duration> for f64 {
fn from(num: Duration) -> Self {
num.0 as f64
}
}
impl Sub<Time> for Time {
type Output = Duration;
fn sub(self, rhs: Time) -> Self::Output {
debug_assert!(
self.0.checked_sub(rhs.0).is_some(),
"overflow detected: {self:?} - {rhs:?}"
);
Duration(self.0 - rhs.0)
}
}
impl Add<Duration> for Time {
type Output = Time;
fn add(self, rhs: Duration) -> Self::Output {
debug_assert!(
self.0.checked_add(rhs.0).is_some(),
"overflow detected: {self:?} + {rhs:?}"
);
Time(self.0 + rhs.0)
}
}
impl AddAssign<Duration> for Time {
fn add_assign(&mut self, rhs: Duration) {
debug_assert!(
self.0.checked_add(rhs.0).is_some(),
"overflow detected: {self:?} += {rhs:?}"
);
self.0 += rhs.0;
}
}
impl Sub<Duration> for Time {
type Output = Time;
fn sub(self, rhs: Duration) -> Self::Output {
debug_assert!(
self.0.checked_sub(rhs.0).is_some(),
"overflow detected: {self:?} - {rhs:?}"
);
Time(self.0 - rhs.0)
}
}
impl SubAssign<Duration> for Time {
fn sub_assign(&mut self, rhs: Duration) {
debug_assert!(
self.0.checked_sub(rhs.0).is_some(),
"overflow detected: {self:?} -= {rhs:?}"
);
self.0 -= rhs.0;
}
}
impl Add<Duration> for Duration {
type Output = Duration;
fn add(self, rhs: Duration) -> Self::Output {
debug_assert!(
self.0.checked_add(rhs.0).is_some(),
"overflow detected: {self:?} + {rhs:?}"
);
Duration(self.0 + rhs.0)
}
}
impl AddAssign<Duration> for Duration {
fn add_assign(&mut self, rhs: Duration) {
debug_assert!(
self.0.checked_add(rhs.0).is_some(),
"overflow detected: {self:?} += {rhs:?}"
);
self.0 += rhs.0;
}
}
impl Sub<Duration> for Duration {
type Output = Duration;
fn sub(self, rhs: Duration) -> Self::Output {
debug_assert!(
self.0.checked_sub(rhs.0).is_some(),
"overflow detected: {self:?} - {rhs:?}"
);
Duration(self.0 - rhs.0)
}
}
impl SubAssign<Duration> for Duration {
fn sub_assign(&mut self, rhs: Duration) {
debug_assert!(
self.0.checked_sub(rhs.0).is_some(),
"overflow detected: {self:?} -= {rhs:?}"
);
self.0 -= rhs.0;
}
}
impl Mul<f64> for Duration {
type Output = Duration;
fn mul(self, rhs: f64) -> Self::Output {
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
{
Duration((self.0 as f64 * rhs).round() as i64)
}
}
}
impl Mul<Duration> for f64 {
type Output = Duration;
fn mul(self, rhs: Duration) -> Self::Output {
rhs * self
}
}
impl MulAssign<f64> for Duration {
fn mul_assign(&mut self, rhs: f64) {
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
{
self.0 = (self.0 as f64 * rhs).round() as i64;
}
}
}
impl Div<f64> for Duration {
type Output = Duration;
fn div(self, rhs: f64) -> Self::Output {
debug_assert!(
rhs.abs() > f64::EPSILON,
"Dividing by zero results in INF. This is probably not what you want."
);
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
{
Duration((self.0 as f64 / rhs).round() as i64)
}
}
}
impl DivAssign<f64> for Duration {
fn div_assign(&mut self, rhs: f64) {
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
{
self.0 = (self.0 as f64 / rhs).round() as i64;
}
}
}
impl Mul<i64> for Duration {
type Output = Duration;
fn mul(self, rhs: i64) -> Self::Output {
debug_assert!(
self.0.checked_mul(rhs).is_some(),
"overflow detected: {self:?} * {rhs:?}"
);
Duration(self.0 * rhs)
}
}
impl Mul<Duration> for i64 {
type Output = Duration;
fn mul(self, rhs: Duration) -> Self::Output {
rhs * self
}
}
impl MulAssign<i64> for Duration {
fn mul_assign(&mut self, rhs: i64) {
debug_assert!(
self.0.checked_mul(rhs).is_some(),
"overflow detected: {self:?} *= {rhs:?}"
);
self.0 *= rhs;
}
}
impl Div<i64> for Duration {
type Output = Duration;
fn div(self, rhs: i64) -> Self::Output {
self / rhs as f64
}
}
impl DivAssign<i64> for Duration {
fn div_assign(&mut self, rhs: i64) {
self.div_assign(rhs as f64);
}
}
impl Div<Duration> for Duration {
type Output = f64;
fn div(self, rhs: Duration) -> Self::Output {
debug_assert_ne!(
rhs,
Duration::ZERO,
"Dividing by zero results in INF. This is probably not what you want."
);
self.0 as f64 / rhs.0 as f64
}
}
impl Rem<Duration> for Time {
type Output = Duration;
fn rem(self, rhs: Duration) -> Self::Output {
Duration(self.0 % rhs.0)
}
}
impl Rem<Duration> for Duration {
type Output = Duration;
fn rem(self, rhs: Duration) -> Self::Output {
Duration(self.0 % rhs.0)
}
}
impl RemAssign<Duration> for Duration {
fn rem_assign(&mut self, rhs: Duration) {
self.0 %= rhs.0;
}
}
impl From<Duration> for std::time::Duration {
fn from(input: Duration) -> Self {
debug_assert!(
input.is_non_negative(),
"Negative Duration {input} cannot be converted to std::time::Duration"
);
#[expect(clippy::cast_sign_loss, reason = "caught by the debug_assert above")]
let secs = (input.0 / 1000) as u64;
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "casting to u32 is safe here because it is guaranteed that the value is in 0..1_000_000_000. The sign loss is caught by the debug_assert above."
)]
let nanos = ((input.0 % 1000) * 1_000_000) as u32;
std::time::Duration::new(secs, nanos)
}
}
impl From<std::time::Duration> for Duration {
fn from(input: std::time::Duration) -> Self {
debug_assert!(
i64::try_from(input.as_millis()).is_ok(),
"Input std::time::Duration ({input:?}) is too large to be converted to tinytime::Duration"
);
#[expect(clippy::cast_possible_truncation, reason = "expected behavior")]
Duration::millis(input.as_millis() as i64)
}
}
impl FromStr for Duration {
type Err = DurationParseError;
#[expect(
clippy::string_slice,
reason = "all slice indices come from methods that guarantee correctness"
)]
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let without_sign = s.strip_prefix('-');
let negative = without_sign.is_some();
s = without_sign.unwrap_or(s);
let mut duration = Self::ZERO;
while !s.is_empty() {
let without_number = s.trim_start_matches(|c: char| c.is_ascii_digit());
let Ok(number) = s[..s.len() - without_number.len()].parse::<i64>() else {
return Err(DurationParseError::UnrecognizedFormat);
};
let without_unit = without_number.trim_start_matches(|c: char| !c.is_ascii_digit());
let unit = &without_number[..without_number.len() - without_unit.len()];
duration += match unit {
"h" => Duration::hours(number),
"m" => Duration::minutes(number),
"s" => Duration::seconds(number),
"ms" => Duration::millis(number),
_ => return Err(DurationParseError::UnrecognizedFormat),
};
s = without_unit;
}
if negative {
duration = -duration;
}
Ok(duration)
}
}
#[derive(Debug, Clone, Copy)]
pub enum DurationParseError {
UnrecognizedFormat,
}
impl Error for DurationParseError {}
impl Display for DurationParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Unrecognized Duration format, valid examples are '2h3s', '1m', '1h3m5s700ms'"
)
}
}
const fn as_unsigned(x: i64) -> u64 {
if x >= 0 { x as u64 } else { 0 }
}
#[cfg(test)]
mod time_test {
use crate::Duration;
use crate::Time;
#[test]
fn test_display() {
struct TestCase {
name: &'static str,
input: Time,
expected: String,
}
let tests = vec![
TestCase {
name: "EPOCH",
input: Time::EPOCH,
expected: "1970-01-01T00:00:00+00:00".to_string(),
},
TestCase {
name: "i16::MAX + 1",
input: Time::seconds(i64::from(i16::MAX) + 1),
expected: "1970-01-01T09:06:08+00:00".to_string(),
},
TestCase {
name: "i32::MAX + 1",
input: Time::seconds(i64::from(i32::MAX) + 1),
expected: "2038-01-19T03:14:08+00:00".to_string(),
},
TestCase {
name: "u32::MAX + 1",
input: Time::seconds(i64::from(u32::MAX) + 1),
expected: "2106-02-07T06:28:16+00:00".to_string(),
},
TestCase {
name: "very large",
input: Time::seconds(i64::from(i32::MAX) * 3500),
expected: "+240148-08-31T19:28:20+00:00".to_string(),
},
TestCase {
name: "MAX",
input: Time::MAX,
expected: "∞".to_string(),
},
TestCase {
name: "i16::MIN",
input: Time::seconds(i64::from(i16::MIN)),
expected: "1969-12-31T14:53:52+00:00".to_string(),
},
TestCase {
name: "i64::MIN",
input: Time::millis(i64::MIN),
expected: "∞".to_string(),
},
];
for test in tests {
assert_eq!(
test.expected,
test.input.to_rfc3339(),
"to_rfc3339 failed for test '{}'",
test.name
);
assert_eq!(
test.expected,
test.input.format("%Y-%m-%dT%H:%M:%S+00:00").to_string(),
"format failed for test '{}'",
test.name
);
}
}
#[test]
fn test_debug() {
struct TestCase {
name: &'static str,
input: Time,
expected: String,
}
let tests = vec![
TestCase {
name: "EPOCH",
input: Time::EPOCH,
expected: "00:00:00".to_string(),
},
TestCase {
name: "i16::MAX + 1",
input: Time::seconds(i64::from(i16::MAX) + 1),
expected: "09:06:08".to_string(),
},
TestCase {
name: "i32::MAX + 1",
input: Time::seconds(i64::from(i32::MAX) + 1),
expected: "596523:14:08".to_string(),
},
TestCase {
name: "u32::MAX + 1",
input: Time::seconds(i64::from(u32::MAX) + 1),
expected: "1193046:28:16".to_string(),
},
TestCase {
name: "very large",
input: Time::seconds(i64::from(i32::MAX) * 3500),
expected: "2087831323:28:20".to_string(),
},
TestCase {
name: "MAX",
input: Time::MAX,
expected: "2562047788015:12:55.807".to_string(),
},
TestCase {
name: "i16::MIN",
input: Time::seconds(i64::from(i16::MIN)),
expected: "-09:06:08".to_string(),
},
TestCase {
name: "i64::MIN",
input: Time::millis(i64::MIN),
expected: "-2562047788015:12:55.808".to_string(),
},
TestCase {
name: "millis",
input: Time::hours(3) + Duration::millis(42),
expected: "03:00:00.042".to_string(),
},
];
for test in tests {
assert_eq!(
test.expected,
format!("{:?}", test.input),
"test '{}' failed",
test.name
);
}
}
#[test]
fn test_time_since_epoch() {
let expected = Duration::seconds(3);
let actual = Time::seconds(3).since_epoch();
assert_eq!(expected, actual);
}
#[test]
fn test_time_from_duration() {
let duration_pos = Duration::seconds(3);
assert_eq!(Ok(Time::seconds(3)), Time::try_from(duration_pos));
let duration_neg = Duration::seconds(-3);
assert_eq!(
Err("Duration cannot be negative."),
Time::try_from(duration_neg)
);
}
}
#[cfg(test)]
mod duration_test {
use super::*;
#[test]
fn duration_display() {
assert_eq!("1ms", Duration::millis(1).to_string());
assert_eq!("2s", Duration::seconds(2).to_string());
assert_eq!("3m", Duration::minutes(3).to_string());
assert_eq!("4h", Duration::hours(4).to_string());
assert_eq!("1m1s", Duration::seconds(61).to_string());
assert_eq!(
"2h3m4s5ms",
(Duration::hours(2)
+ Duration::minutes(3)
+ Duration::seconds(4)
+ Duration::millis(5))
.to_string()
);
assert_eq!("0ms", Duration::ZERO.to_string());
assert_eq!("-1m1s", Duration::seconds(-61).to_string());
}
#[test]
fn test_time_window_display() {
assert_eq!(
"[1970-01-01T00:00:00+00:00, ∞]",
TimeWindow::new(Time::EPOCH, Time::MAX).to_string()
);
assert_eq!(
"[1970-01-01T01:00:00+00:00, 2024-02-06T16:53:47+00:00]",
TimeWindow::new(Time::hours(1), Time::millis(1_707_238_427_962)).to_string()
);
}
#[test]
fn test_duration_is_non_negative_returns_correctly() {
struct TestCase {
name: &'static str,
input: i64,
expected: bool,
}
let tests = vec![
TestCase {
name: "negative",
input: -1,
expected: false,
},
TestCase {
name: "zero",
input: 0,
expected: true,
},
TestCase {
name: "positive",
input: 1,
expected: true,
},
];
for t in tests {
let actual = Duration(t.input).is_non_negative();
assert_eq!(t.expected, actual, "failed '{}'", t.name);
}
}
#[test]
fn test_duration_abs_removes_sign() {
struct TestCase {
name: &'static str,
input: Duration,
expected: Duration,
}
let tests = vec![
TestCase {
name: "negative",
input: Duration::hours(-1),
expected: Duration::hours(1),
},
TestCase {
name: "zero",
input: Duration::ZERO,
expected: Duration::ZERO,
},
TestCase {
name: "positive",
input: Duration::minutes(1),
expected: Duration::minutes(1),
},
];
for t in tests {
let actual = t.input.abs();
assert_eq!(t.expected, actual, "failed '{}'", t.name);
}
}
#[test]
fn test_duration_is_positive_returns_correctly() {
struct TestCase {
name: &'static str,
input: i64,
expected: bool,
}
let tests = vec![
TestCase {
name: "negative",
input: -1,
expected: false,
},
TestCase {
name: "zero",
input: 0,
expected: false,
},
TestCase {
name: "positive",
input: 1,
expected: true,
},
];
for t in tests {
let actual = Duration(t.input).is_positive();
assert_eq!(t.expected, actual, "failed '{}'", t.name);
}
}
#[test]
fn time_add_duration() {
let mut time = Time::millis(1);
let expected_time = Time::millis(3);
let duration = Duration::millis(2);
assert_eq!(expected_time, time + duration);
time += duration;
assert_eq!(expected_time, time);
}
#[test]
fn time_sub_duration() {
let mut time = Time::millis(10);
let expected_time = Time::millis(3);
let duration = Duration::millis(7);
assert_eq!(expected_time, time - duration);
time -= duration;
assert_eq!(expected_time, time);
}
#[test]
fn time_sub_time() {
let time = Time::minutes(7);
let time2 = Time::minutes(3);
assert_eq!(Duration::minutes(4), time - time2);
assert_eq!(Duration::minutes(-4), time2 - time);
}
#[test]
fn time_rem_duration() {
let time57 = Time::minutes(57);
assert_eq!(Duration::ZERO, time57 % Duration::minutes(1));
assert_eq!(Duration::minutes(57), time57 % Duration::minutes(60));
assert_eq!(
Duration::minutes(57),
(time57 + Duration::hours(17)) % Duration::minutes(60)
);
}
#[test]
fn duration_rem_duration() {
let dur34 = Duration::minutes(34);
assert_eq!(Duration::ZERO, dur34 % Duration::minutes(1));
assert_eq!(Duration::minutes(34), dur34 % Duration::minutes(45));
assert_eq!(Duration::minutes(10), dur34 % Duration::minutes(12));
}
#[test]
fn duration_rem_assign_duration() {
let mut dur = Duration::minutes(734);
dur %= Duration::minutes(100);
assert_eq!(Duration::minutes(34), dur);
}
}