use std::fmt;
use chrono::Duration;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::Number;
const SECS_IN_MIN: i64 = 60;
const MINS_IN_HOUR: i64 = 60;
const MILLIS_IN_SEC: i64 = 1000;
#[derive(Debug)]
pub enum Error {
DurationOverflow,
}
impl From<rust_decimal::Error> for Error {
fn from(_: rust_decimal::Error) -> Self {
Self::DurationOverflow
}
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DurationOverflow => {
f.write_str("A numeric overflow occurred while creating a duration")
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct HoursDecimal(Duration);
impl<'de> Deserialize<'de> for HoursDecimal {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let hours = Number::deserialize(deserializer)?;
let duration = Self::from_hours_number(hours).map_err(|_e| D::Error::custom("overflow"))?;
Ok(duration)
}
}
impl Serialize for HoursDecimal {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let hours = self.as_num_hours_number();
hours.serialize(serializer)
}
}
impl fmt::Display for HoursDecimal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let duration = self.0;
let seconds = duration.num_seconds() % SECS_IN_MIN;
let minutes = (duration.num_seconds() / SECS_IN_MIN) % MINS_IN_HOUR;
let hours = duration.num_seconds() / (SECS_IN_MIN * MINS_IN_HOUR);
write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
}
}
impl From<HoursDecimal> for Duration {
fn from(value: HoursDecimal) -> Self {
value.0
}
}
impl From<Duration> for HoursDecimal {
fn from(value: Duration) -> Self {
Self(value)
}
}
impl HoursDecimal {
pub fn zero() -> Self {
Self(Duration::zero())
}
pub fn as_num_seconds_number(&self) -> Number {
Number::from(self.0.num_milliseconds())
.checked_div(Number::from(MILLIS_IN_SEC))
.expect("Can't overflow; See test `as_num_seconds_number_should_not_overflow`")
}
pub fn as_num_hours_decimal(&self) -> Decimal {
self.as_num_hours_number().into()
}
#[must_use]
pub fn as_num_hours_number(&self) -> Number {
let div = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
let num = Decimal::from(self.0.num_milliseconds());
Number::from(num.checked_div(div).unwrap_or(Decimal::MAX))
}
pub fn from_seconds_number(seconds: Number) -> Result<Self, Error> {
let millis = seconds.saturating_mul(Number::from(MILLIS_IN_SEC));
Ok(Self(
Duration::try_milliseconds(millis.try_into()?).ok_or(Error::DurationOverflow)?,
))
}
pub fn from_hours_number(hours: Number) -> Result<Self, Error> {
let millis = hours.saturating_mul(Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR));
Ok(Self(
Duration::try_milliseconds(millis.try_into()?).ok_or(Error::DurationOverflow)?,
))
}
#[must_use]
pub fn saturating_sub(self, other: Self) -> Self {
Self(self.0.checked_sub(&other.0).unwrap_or_else(Duration::zero))
}
#[must_use]
pub fn saturating_add(self, other: Self) -> Self {
Self(self.0.checked_add(&other.0).unwrap_or(Duration::MAX))
}
}
impl Default for HoursDecimal {
fn default() -> Self {
Self::zero()
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct SecondsRound(Duration);
impl<'de> Deserialize<'de> for SecondsRound {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error as DeError;
let seconds: i64 = u64::deserialize(deserializer)?
.try_into()
.map_err(|_err| DeError::custom(Error::DurationOverflow))?;
let duration = Duration::try_seconds(seconds)
.ok_or_else(|| DeError::custom(Error::DurationOverflow))?;
Ok(Self(duration))
}
}
impl Serialize for SecondsRound {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let seconds = self.0.num_seconds();
serializer.serialize_i64(seconds)
}
}
impl From<SecondsRound> for Duration {
fn from(value: SecondsRound) -> Self {
value.0
}
}
#[cfg(test)]
mod test {
use super::Error;
#[test]
const fn error_should_be_send_and_sync() {
const fn f<T: Send + Sync>() {}
f::<Error>();
}
}
#[cfg(test)]
mod hour_decimal_tests {
use chrono::Duration;
use rust_decimal_macros::dec;
use super::{HoursDecimal, Number, MILLIS_IN_SEC};
#[test]
fn zero_minutes_should_be_zero_hours() {
let hours: HoursDecimal = Duration::try_minutes(0).unwrap().into();
let number = hours.as_num_hours_number();
assert_eq!(number, Number::from(dec!(0.0)));
}
#[test]
fn thirty_minutes_should_be_fraction_of_hour() {
let hours: HoursDecimal = Duration::try_minutes(30).unwrap().into();
let number = hours.as_num_hours_number();
assert_eq!(number, Number::from(dec!(0.5)));
}
#[test]
fn sixty_minutes_should_be_fraction_of_hour() {
let hours: HoursDecimal = Duration::try_minutes(60).unwrap().into();
let number = hours.as_num_hours_number();
assert_eq!(number, Number::from(dec!(1.0)));
}
#[test]
fn ninety_minutes_should_be_fraction_of_hour() {
let hours: HoursDecimal = Duration::try_minutes(90).unwrap().into();
let number = hours.as_num_hours_number();
assert_eq!(number, Number::from(dec!(1.5)));
}
#[test]
fn as_num_seconds_number_should_not_overflow() {
let number = Number::from(i64::MAX).checked_div(Number::from(MILLIS_IN_SEC));
assert!(number.is_some(), "should not overflow");
}
}