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