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},
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 { .. } => warning::Id::from_static("invalid_type"),
69 Self::Overflow => warning::Id::from_static("overflow"),
70 }
71 }
72}
73
74impl From<rust_decimal::Error> for Warning {
75 fn from(_: rust_decimal::Error) -> Self {
76 Self::Overflow
77 }
78}
79
80pub trait ToHoursDecimal {
82 fn to_hours_dec(&self) -> Decimal;
84 fn to_hours_dec_in_ocpi_precision(&self) -> Decimal {
90 self.to_hours_dec().round_to_ocpi_scale()
91 }
92}
93
94impl ToHoursDecimal for TimeDelta {
95 fn to_hours_dec(&self) -> Decimal {
96 let num_sec = Decimal::from(self.num_seconds());
97 let num_nano = Decimal::from(self.subsec_nanos());
98 let sec_part = num_sec.checked_div(SECONDS_IN_HOUR).unwrap_or(Decimal::MAX);
99 let nano_part = num_nano.checked_div(NANOS_IN_HOUR).unwrap_or(Decimal::MAX);
100 sec_part.checked_add(nano_part).unwrap_or(Decimal::MAX)
101 }
102}
103
104pub trait ToDuration {
106 fn to_duration(&self) -> TimeDelta;
108 fn to_duration_ceil_nanos(&self) -> TimeDelta;
112}
113
114impl ToDuration for Decimal {
115 fn to_duration(&self) -> TimeDelta {
119 let nanos = self
120 .saturating_mul(NANOS_IN_HOUR)
121 .round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
122 .to_i64()
123 .unwrap_or(i64::MAX);
124 TimeDelta::nanoseconds(nanos)
125 }
126
127 fn to_duration_ceil_nanos(&self) -> TimeDelta {
132 let nanos = self
133 .saturating_mul(NANOS_IN_HOUR)
134 .ceil()
135 .to_i64()
136 .unwrap_or(i64::MAX);
137 TimeDelta::nanoseconds(nanos)
138 }
139}
140
141pub(crate) struct Seconds(TimeDelta);
144
145impl number::IsZero for Seconds {
146 fn is_zero(&self) -> bool {
147 self.0.is_zero()
148 }
149}
150
151impl From<Seconds> for TimeDelta {
153 fn from(value: Seconds) -> Self {
154 value.0
155 }
156}
157
158impl fmt::Debug for Seconds {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.debug_tuple("Seconds")
161 .field(&self.0.num_seconds())
162 .finish()
163 }
164}
165
166impl fmt::Display for Seconds {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "{}", self.0.num_seconds())
169 }
170}
171
172impl json::FromJson<'_> for Seconds {
179 type Warning = Warning;
180
181 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
182 let warnings = warning::Set::new();
183 let Some(s) = elem.as_number_str() else {
184 return warnings.bail(Warning::invalid_type(elem), elem);
185 };
186
187 let seconds = match s.parse::<u64>() {
189 Ok(n) => n,
190 Err(err) => {
191 return warnings.bail(Warning::Invalid(int_error_kind_as_str(*err.kind())), elem);
192 }
193 };
194
195 let Ok(seconds) = i64::try_from(seconds) else {
198 return warnings.bail(
199 Warning::Invalid("The duration value is larger than an i64 can represent."),
200 elem,
201 );
202 };
203 let dt = TimeDelta::seconds(seconds);
204
205 Ok(Seconds(dt).into_caveat(warnings))
206 }
207}
208
209impl Cost for TimeDelta {
211 fn cost(&self, money: Money) -> Money {
212 let cost = self.to_hours_dec().saturating_mul(Decimal::from(money));
213 Money::from_decimal(cost)
214 }
215}
216
217impl SaturatingAdd for TimeDelta {
218 fn saturating_add(self, other: TimeDelta) -> TimeDelta {
219 self.checked_add(&other).unwrap_or(TimeDelta::MAX)
220 }
221}
222
223impl SaturatingSub for TimeDelta {
224 fn saturating_sub(self, other: TimeDelta) -> TimeDelta {
225 self.checked_sub(&other).unwrap_or_else(TimeDelta::zero)
226 }
227}
228
229#[allow(dead_code, reason = "used during debug sessions")]
231pub(crate) trait AsHms {
232 fn as_hms(&self) -> Hms;
234}
235
236impl AsHms for TimeDelta {
237 fn as_hms(&self) -> Hms {
238 Hms(*self)
239 }
240}
241
242impl AsHms for Decimal {
243 fn as_hms(&self) -> Hms {
245 Hms(self.to_duration())
246 }
247}
248
249#[derive(Copy, Clone)]
251pub struct Hms(pub TimeDelta);
252
253impl fmt::Debug for Hms {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 fmt::Display::fmt(self, f)
257 }
258}
259
260impl fmt::Display for Hms {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 let duration = self.0;
263 let seconds = duration.num_seconds();
264
265 if seconds.is_negative() {
267 f.write_str("-")?;
268 }
269
270 let seconds_total = seconds.abs();
272
273 let seconds = seconds_total % SECS_IN_MIN;
274 let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
275 let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
276
277 write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
278 }
279}
280
281#[cfg(test)]
282mod test_hms {
283 use chrono::TimeDelta;
284
285 use super::Hms;
286
287 #[test]
288 fn should_display_seconds() {
289 assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
290 assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
291 }
292
293 #[test]
294 fn should_display_minutes() {
295 assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
296 assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
297 }
298
299 #[test]
300 fn should_display_hours() {
301 assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
302 assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
303 }
304
305 #[test]
306 fn should_display_hours_mins_secs() {
307 assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
308 }
309}
310
311#[cfg(test)]
312mod test_to_hours_decimal {
313 use chrono::TimeDelta;
314 use rust_decimal_macros::dec;
315
316 use crate::ToHoursDecimal as _;
317
318 #[test]
319 fn to_hours_dec_should_be_correct() {
320 let actual = TimeDelta::hours(1).to_hours_dec();
321 assert_eq!(actual, dec!(1.0));
322
323 let actual = TimeDelta::seconds(3960).to_hours_dec();
324 assert_eq!(actual, dec!(1.1));
325
326 let actual = TimeDelta::seconds(360).to_hours_dec();
327 assert_eq!(actual, dec!(0.1));
328
329 let actual = TimeDelta::seconds(36).to_hours_dec();
330 assert_eq!(actual, dec!(0.01));
331
332 let actual = TimeDelta::milliseconds(36).to_hours_dec();
333 assert_eq!(actual, dec!(0.00001));
334
335 let actual = TimeDelta::nanoseconds(1).to_hours_dec();
336 assert_eq!(actual, dec!(2.777777777777778e-13));
337 }
338}
339
340#[cfg(test)]
341mod test_to_duration {
342 use chrono::TimeDelta;
343 use rust_decimal_macros::dec;
344
345 use crate::ToDuration as _;
346
347 #[test]
348 fn to_duration_should_be_correct() {
349 let actual = dec!(1.0).to_duration();
350 assert_eq!(actual, TimeDelta::hours(1));
351
352 let actual = dec!(1.1).to_duration();
353 assert_eq!(actual, TimeDelta::seconds(3960));
354
355 let actual = dec!(0.1).to_duration();
356 assert_eq!(actual, TimeDelta::seconds(360));
357
358 let actual = dec!(1e-14).to_duration();
359 assert_eq!(actual, TimeDelta::zero());
360
361 let actual = dec!(2.777e-13).to_duration();
362 assert_eq!(actual, TimeDelta::nanoseconds(1));
363 }
364}