#[cfg(test)]
pub(crate) mod test;
#[cfg(test)]
mod test_hour_decimal;
use std::fmt;
use chrono::TimeDelta;
use num_traits::ToPrimitive as _;
use rust_decimal::Decimal;
use crate::{
json,
number::{self, int_error_kind_as_str, FromDecimal as _, RoundDecimal},
warning::{self, IntoCaveat as _},
Cost, Money, SaturatingAdd, SaturatingSub, Verdict,
};
pub(crate) const SECS_IN_MIN: i64 = 60;
pub(crate) const MINS_IN_HOUR: i64 = 60;
pub(crate) const MILLIS_IN_SEC: i64 = 1000;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
Invalid(&'static str),
InvalidType { type_found: json::ValueKind },
Overflow,
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(err) => write!(f, "Unable to parse the duration: {err}"),
Self::InvalidType { type_found } => {
write!(f, "The value should be an int but is `{type_found}`")
}
Self::Overflow => f.write_str("A numeric overflow occurred while creating a duration"),
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::Invalid(_) => warning::Id::from_static("invalid"),
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::Overflow => warning::Id::from_static("overflow"),
}
}
}
impl From<rust_decimal::Error> for Warning {
fn from(_: rust_decimal::Error) -> Self {
Self::Overflow
}
}
pub trait ToHoursDecimal {
fn to_hours_dec(&self) -> Decimal;
}
pub trait ToDuration {
fn to_duration(&self) -> TimeDelta;
}
impl ToHoursDecimal for TimeDelta {
fn to_hours_dec(&self) -> Decimal {
let div = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
let num = Decimal::from(self.num_milliseconds());
num.checked_div(div)
.unwrap_or(Decimal::MAX)
.round_to_ocpi_scale()
}
}
impl ToDuration for Decimal {
fn to_duration(&self) -> TimeDelta {
let factor = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
let millis = self.saturating_mul(factor).to_i64().unwrap_or(i64::MAX);
TimeDelta::milliseconds(millis)
}
}
pub(crate) struct Seconds(TimeDelta);
impl number::IsZero for Seconds {
fn is_zero(&self) -> bool {
self.0.is_zero()
}
}
impl From<Seconds> for TimeDelta {
fn from(value: Seconds) -> Self {
value.0
}
}
impl fmt::Debug for Seconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Seconds")
.field(&self.0.num_seconds())
.finish()
}
}
impl fmt::Display for Seconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.num_seconds())
}
}
impl json::FromJson<'_> for Seconds {
type Warning = Warning;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
let warnings = warning::Set::new();
let Some(s) = elem.as_number_str() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
let seconds = match s.parse::<u64>() {
Ok(n) => n,
Err(err) => {
return warnings.bail(Warning::Invalid(int_error_kind_as_str(*err.kind())), elem);
}
};
let Ok(seconds) = i64::try_from(seconds) else {
return warnings.bail(
Warning::Invalid("The duration value is larger than an i64 can represent."),
elem,
);
};
let dt = TimeDelta::seconds(seconds);
Ok(Seconds(dt).into_caveat(warnings))
}
}
impl Cost for TimeDelta {
fn cost(&self, money: Money) -> Money {
let cost = self.to_hours_dec().saturating_mul(Decimal::from(money));
Money::from_decimal(cost)
}
}
impl SaturatingAdd for TimeDelta {
fn saturating_add(self, other: TimeDelta) -> TimeDelta {
self.checked_add(&other).unwrap_or(TimeDelta::MAX)
}
}
impl SaturatingSub for TimeDelta {
fn saturating_sub(self, other: TimeDelta) -> TimeDelta {
self.checked_sub(&other).unwrap_or_else(TimeDelta::zero)
}
}
#[allow(dead_code, reason = "used during debug sessions")]
pub(crate) trait AsHms {
fn as_hms(&self) -> Hms;
}
impl AsHms for TimeDelta {
fn as_hms(&self) -> Hms {
Hms(*self)
}
}
impl AsHms for Decimal {
fn as_hms(&self) -> Hms {
Hms(self.to_duration())
}
}
#[derive(Copy, Clone)]
pub struct Hms(pub TimeDelta);
impl fmt::Debug for Hms {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for Hms {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let duration = self.0;
let seconds = duration.num_seconds();
if seconds.is_negative() {
f.write_str("-")?;
}
let seconds_total = seconds.abs();
let seconds = seconds_total % SECS_IN_MIN;
let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
}
}
#[cfg(test)]
mod test_hms {
use chrono::TimeDelta;
use super::Hms;
#[test]
fn should_display_seconds() {
assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
}
#[test]
fn should_display_minutes() {
assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
}
#[test]
fn should_display_hours() {
assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
}
#[test]
fn should_display_hours_mins_secs() {
assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
}
}