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;
16use rust_decimal_macros::dec;
17
18use crate::{
19 json,
20 number::{self, int_error_kind_as_str, FromDecimal as _, RoundDecimal as _},
21 warning::{self, IntoCaveat as _},
22 Cost, Money, SaturatingAdd, SaturatingSub, Verdict,
23};
24
25pub(crate) const SECS_IN_MIN: i64 = 60;
26pub(crate) const MINS_IN_HOUR: i64 = 60;
27pub(crate) const MILLIS_IN_SEC: i64 = 1000;
28const NANOS_IN_HOUR: Decimal = dec!(36e11);
29const SECONDS_IN_HOUR: Decimal = dec!(3600);
30
31#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
33pub enum Warning {
34 Invalid(&'static str),
36
37 InvalidType { type_found: json::ValueKind },
39
40 Overflow,
42}
43
44impl Warning {
45 fn invalid_type(elem: &json::Element<'_>) -> Self {
46 Self::InvalidType {
47 type_found: elem.value().kind(),
48 }
49 }
50}
51
52impl fmt::Display for Warning {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::Invalid(err) => write!(f, "Unable to parse the duration: {err}"),
56 Self::InvalidType { type_found } => {
57 write!(f, "The value should be an int but is `{type_found}`")
58 }
59 Self::Overflow => f.write_str("A numeric overflow occurred while creating a duration"),
60 }
61 }
62}
63
64impl crate::Warning for Warning {
65 fn id(&self) -> warning::Id {
66 match self {
67 Self::Invalid(_) => warning::Id::from_static("invalid"),
68 Self::InvalidType { type_found } => {
69 warning::Id::from_string(format!("invalid_type({type_found})"))
70 }
71 Self::Overflow => warning::Id::from_static("overflow"),
72 }
73 }
74}
75
76impl From<rust_decimal::Error> for Warning {
77 fn from(_: rust_decimal::Error) -> Self {
78 Self::Overflow
79 }
80}
81
82pub trait ToHoursDecimal {
84 fn to_hours_dec(&self) -> Decimal;
86 fn to_hours_dec_in_ocpi_precision(&self) -> Decimal {
92 self.to_hours_dec().round_to_ocpi_scale()
93 }
94}
95
96impl ToHoursDecimal for TimeDelta {
97 fn to_hours_dec(&self) -> Decimal {
98 let num_sec = Decimal::from(self.num_seconds());
99 let num_nano = Decimal::from(self.subsec_nanos());
100 let sec_part = num_sec.checked_div(SECONDS_IN_HOUR).unwrap_or(Decimal::MAX);
101 let nano_part = num_nano.checked_div(NANOS_IN_HOUR).unwrap_or(Decimal::MAX);
102 sec_part.checked_add(nano_part).unwrap_or(Decimal::MAX)
103 }
104}
105
106pub trait ToDuration {
108 fn to_duration(&self) -> TimeDelta;
110}
111
112impl ToDuration for Decimal {
113 fn to_duration(&self) -> TimeDelta {
117 let nanos = self
118 .saturating_mul(NANOS_IN_HOUR)
119 .round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
124 .to_i64()
125 .unwrap_or(i64::MAX);
126 TimeDelta::nanoseconds(nanos)
127 }
128}
129
130pub(crate) struct Seconds(TimeDelta);
133
134impl number::IsZero for Seconds {
135 fn is_zero(&self) -> bool {
136 self.0.is_zero()
137 }
138}
139
140impl From<Seconds> for TimeDelta {
142 fn from(value: Seconds) -> Self {
143 value.0
144 }
145}
146
147impl fmt::Debug for Seconds {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.debug_tuple("Seconds")
150 .field(&self.0.num_seconds())
151 .finish()
152 }
153}
154
155impl fmt::Display for Seconds {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{}", self.0.num_seconds())
158 }
159}
160
161impl json::FromJson<'_> for Seconds {
168 type Warning = Warning;
169
170 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
171 let warnings = warning::Set::new();
172 let Some(s) = elem.as_number_str() else {
173 return warnings.bail(elem, Warning::invalid_type(elem));
174 };
175
176 let seconds = match s.parse::<u64>() {
178 Ok(n) => n,
179 Err(err) => {
180 return warnings.bail(elem, Warning::Invalid(int_error_kind_as_str(*err.kind())));
181 }
182 };
183
184 let Ok(seconds) = i64::try_from(seconds) else {
187 return warnings.bail(
188 elem,
189 Warning::Invalid("The duration value is larger than an i64 can represent."),
190 );
191 };
192 let dt = TimeDelta::seconds(seconds);
193
194 Ok(Seconds(dt).into_caveat(warnings))
195 }
196}
197
198impl Cost for TimeDelta {
200 fn cost(&self, money: Money) -> Money {
201 let cost = self.to_hours_dec().saturating_mul(Decimal::from(money));
202 Money::from_decimal(cost)
203 }
204}
205
206impl SaturatingAdd for TimeDelta {
207 fn saturating_add(self, other: TimeDelta) -> TimeDelta {
208 self.checked_add(&other).unwrap_or(TimeDelta::MAX)
209 }
210}
211
212impl SaturatingSub for TimeDelta {
213 fn saturating_sub(self, other: TimeDelta) -> TimeDelta {
214 self.checked_sub(&other).unwrap_or_else(TimeDelta::zero)
215 }
216}
217
218#[expect(clippy::allow_attributes, reason = "used during debug sessions")]
220#[allow(dead_code, reason = "used during debug sessions")]
221pub(crate) trait AsHms {
222 fn as_hms(&self) -> Hms;
224}
225
226impl AsHms for TimeDelta {
227 fn as_hms(&self) -> Hms {
228 Hms(*self)
229 }
230}
231
232impl AsHms for Decimal {
233 fn as_hms(&self) -> Hms {
235 Hms(self.to_duration())
236 }
237}
238
239#[derive(Copy, Clone)]
241pub struct Hms(pub TimeDelta);
242
243impl fmt::Debug for Hms {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 fmt::Display::fmt(self, f)
247 }
248}
249
250impl fmt::Display for Hms {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 let duration = self.0;
253 let seconds = duration.num_seconds();
254
255 if seconds.is_negative() {
257 f.write_str("-")?;
258 }
259
260 let seconds_total = seconds.abs();
262
263 let seconds = seconds_total % SECS_IN_MIN;
264 let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
265 let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
266
267 write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
268 }
269}
270
271#[cfg(test)]
272mod test_hms {
273 use chrono::TimeDelta;
274
275 use super::Hms;
276
277 #[test]
278 fn should_display_seconds() {
279 assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
280 assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
281 }
282
283 #[test]
284 fn should_display_minutes() {
285 assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
286 assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
287 }
288
289 #[test]
290 fn should_display_hours() {
291 assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
292 assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
293 }
294
295 #[test]
296 fn should_display_hours_mins_secs() {
297 assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
298 }
299}
300
301#[cfg(test)]
302mod test_to_hours_decimal {
303 use chrono::TimeDelta;
304 use rust_decimal_macros::dec;
305
306 use crate::ToHoursDecimal as _;
307
308 #[test]
309 fn to_hours_dec_should_be_correct() {
310 let actual = TimeDelta::hours(1).to_hours_dec();
311 assert_eq!(actual, dec!(1.0));
312
313 let actual = TimeDelta::seconds(3960).to_hours_dec();
314 assert_eq!(actual, dec!(1.1));
315
316 let actual = TimeDelta::seconds(360).to_hours_dec();
317 assert_eq!(actual, dec!(0.1));
318
319 let actual = TimeDelta::seconds(36).to_hours_dec();
320 assert_eq!(actual, dec!(0.01));
321
322 let actual = TimeDelta::milliseconds(36).to_hours_dec();
323 assert_eq!(actual, dec!(0.00001));
324
325 let actual = TimeDelta::nanoseconds(1).to_hours_dec();
326 assert_eq!(actual, dec!(2.777777777777778e-13));
327 }
328}
329
330#[cfg(test)]
331mod test_to_duration {
332 use chrono::TimeDelta;
333 use rust_decimal_macros::dec;
334
335 use crate::ToDuration as _;
336
337 #[test]
338 fn to_duration_should_be_correct() {
339 let actual = dec!(1.0).to_duration();
340 assert_eq!(actual, TimeDelta::hours(1));
341
342 let actual = dec!(1.1).to_duration();
343 assert_eq!(actual, TimeDelta::seconds(3960));
344
345 let actual = dec!(0.1).to_duration();
346 assert_eq!(actual, TimeDelta::seconds(360));
347
348 let actual = dec!(1e-14).to_duration();
349 assert_eq!(actual, TimeDelta::zero());
350
351 let actual = dec!(2.777e-13).to_duration();
352 assert_eq!(actual, TimeDelta::nanoseconds(1));
353 }
354}