1#[cfg(test)]
6pub(crate) mod test;
7
8#[cfg(test)]
9mod test_hour_decimal;
10
11use std::fmt;
12
13use chrono::TimeDelta;
14use num_traits::ToPrimitive as _;
15use rust_decimal::Decimal;
16
17use crate::{
18 json,
19 number::{self, int_error_kind_as_str, FromDecimal as _, RoundDecimal},
20 warning::{self, IntoCaveat as _},
21 Cost, Money, SaturatingAdd, SaturatingSub, Verdict,
22};
23
24pub(crate) const SECS_IN_MIN: i64 = 60;
25pub(crate) const MINS_IN_HOUR: i64 = 60;
26pub(crate) const MILLIS_IN_SEC: i64 = 1000;
27
28#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub enum Warning {
31 Invalid(&'static str),
33
34 InvalidType { type_found: json::ValueKind },
36
37 Overflow,
39}
40
41impl Warning {
42 fn invalid_type(elem: &json::Element<'_>) -> Self {
43 Self::InvalidType {
44 type_found: elem.value().kind(),
45 }
46 }
47}
48
49impl fmt::Display for Warning {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::Invalid(err) => write!(f, "Unable to parse the duration: {err}"),
53 Self::InvalidType { type_found } => {
54 write!(f, "The value should be an int but is `{type_found}`")
55 }
56 Self::Overflow => f.write_str("A numeric overflow occurred while creating a duration"),
57 }
58 }
59}
60
61impl crate::Warning for Warning {
62 fn id(&self) -> warning::Id {
63 match self {
64 Self::Invalid(_) => warning::Id::from_static("invalid"),
65 Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
66 Self::Overflow => warning::Id::from_static("overflow"),
67 }
68 }
69}
70
71impl From<rust_decimal::Error> for Warning {
72 fn from(_: rust_decimal::Error) -> Self {
73 Self::Overflow
74 }
75}
76
77pub trait ToHoursDecimal {
79 fn to_hours_dec(&self) -> Decimal;
81}
82
83pub trait ToDuration {
85 fn to_duration(&self) -> TimeDelta;
87}
88
89impl ToHoursDecimal for TimeDelta {
90 fn to_hours_dec(&self) -> Decimal {
91 let div = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
92 let num = Decimal::from(self.num_milliseconds());
93 num.checked_div(div)
94 .unwrap_or(Decimal::MAX)
95 .round_to_ocpi_scale()
96 }
97}
98
99impl ToDuration for Decimal {
100 fn to_duration(&self) -> TimeDelta {
101 let factor = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
102 let millis = self.saturating_mul(factor).to_i64().unwrap_or(i64::MAX);
103 TimeDelta::milliseconds(millis)
104 }
105}
106
107pub(crate) struct Seconds(TimeDelta);
110
111impl number::IsZero for Seconds {
112 fn is_zero(&self) -> bool {
113 self.0.is_zero()
114 }
115}
116
117impl From<Seconds> for TimeDelta {
119 fn from(value: Seconds) -> Self {
120 value.0
121 }
122}
123
124impl fmt::Debug for Seconds {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.debug_tuple("Seconds")
127 .field(&self.0.num_seconds())
128 .finish()
129 }
130}
131
132impl fmt::Display for Seconds {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}", self.0.num_seconds())
135 }
136}
137
138impl json::FromJson<'_> for Seconds {
145 type Warning = Warning;
146
147 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
148 let warnings = warning::Set::new();
149 let Some(s) = elem.as_number_str() else {
150 return warnings.bail(Warning::invalid_type(elem), elem);
151 };
152
153 let seconds = match s.parse::<u64>() {
155 Ok(n) => n,
156 Err(err) => {
157 return warnings.bail(Warning::Invalid(int_error_kind_as_str(*err.kind())), elem);
158 }
159 };
160
161 let Ok(seconds) = i64::try_from(seconds) else {
164 return warnings.bail(
165 Warning::Invalid("The duration value is larger than an i64 can represent."),
166 elem,
167 );
168 };
169 let dt = TimeDelta::seconds(seconds);
170
171 Ok(Seconds(dt).into_caveat(warnings))
172 }
173}
174
175impl Cost for TimeDelta {
177 fn cost(&self, money: Money) -> Money {
178 let cost = self.to_hours_dec().saturating_mul(Decimal::from(money));
179 Money::from_decimal(cost)
180 }
181}
182
183impl SaturatingAdd for TimeDelta {
184 fn saturating_add(self, other: TimeDelta) -> TimeDelta {
185 self.checked_add(&other).unwrap_or(TimeDelta::MAX)
186 }
187}
188
189impl SaturatingSub for TimeDelta {
190 fn saturating_sub(self, other: TimeDelta) -> TimeDelta {
191 self.checked_sub(&other).unwrap_or_else(TimeDelta::zero)
192 }
193}
194
195#[allow(dead_code, reason = "used during debug sessions")]
197pub(crate) trait AsHms {
198 fn as_hms(&self) -> Hms;
200}
201
202impl AsHms for TimeDelta {
203 fn as_hms(&self) -> Hms {
204 Hms(*self)
205 }
206}
207
208impl AsHms for Decimal {
209 fn as_hms(&self) -> Hms {
211 Hms(self.to_duration())
212 }
213}
214
215#[derive(Copy, Clone)]
217pub struct Hms(pub TimeDelta);
218
219impl fmt::Debug for Hms {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 fmt::Display::fmt(self, f)
223 }
224}
225
226impl fmt::Display for Hms {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 let duration = self.0;
229 let seconds = duration.num_seconds();
230
231 if seconds.is_negative() {
233 f.write_str("-")?;
234 }
235
236 let seconds_total = seconds.abs();
238
239 let seconds = seconds_total % SECS_IN_MIN;
240 let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
241 let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
242
243 write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
244 }
245}
246
247#[cfg(test)]
248mod test_hms {
249 use chrono::TimeDelta;
250
251 use super::Hms;
252
253 #[test]
254 fn should_display_seconds() {
255 assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
256 assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
257 }
258
259 #[test]
260 fn should_display_minutes() {
261 assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
262 assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
263 }
264
265 #[test]
266 fn should_display_hours() {
267 assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
268 assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
269 }
270
271 #[test]
272 fn should_display_hours_mins_secs() {
273 assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
274 }
275}