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 { type_found: json::ValueKind },
43
44 Overflow,
46}
47
48impl Warning {
49 fn invalid_type(elem: &json::Element<'_>) -> Self {
50 Self::InvalidType {
51 type_found: elem.value().kind(),
52 }
53 }
54}
55
56impl fmt::Display for Warning {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::Invalid(err) => write!(f, "Unable to parse the duration: {err}"),
60 Self::InvalidType { type_found } => {
61 write!(f, "The value should be an int but is `{type_found}`")
62 }
63 Self::Overflow => f.write_str("A numeric overflow occurred while creating a duration"),
64 }
65 }
66}
67
68impl crate::Warning for Warning {
69 fn id(&self) -> warning::Id {
70 match self {
71 Self::Invalid(_) => warning::Id::from_static("invalid"),
72 Self::InvalidType { type_found } => {
73 warning::Id::from_string(format!("invalid_type({type_found})"))
74 }
75 Self::Overflow => warning::Id::from_static("overflow"),
76 }
77 }
78}
79
80impl From<rust_decimal::Error> for Warning {
81 fn from(_: rust_decimal::Error) -> Self {
82 Self::Overflow
83 }
84}
85
86pub trait ToHoursDecimal {
88 fn to_hours_dec(&self) -> Decimal;
90 fn to_hours_dec_in_ocpi_precision(&self) -> Decimal {
96 self.to_hours_dec().round_to_ocpi_scale()
97 }
98}
99
100impl ToHoursDecimal for TimeDelta {
101 fn to_hours_dec(&self) -> Decimal {
102 let num_sec = Decimal::from(self.num_seconds());
103 let num_nano = Decimal::from(self.subsec_nanos());
104 let sec_part = num_sec.checked_div(SECONDS_IN_HOUR).unwrap_or(Decimal::MAX);
105 let nano_part = num_nano.checked_div(NANOS_IN_HOUR).unwrap_or(Decimal::MAX);
106 sec_part.checked_add(nano_part).unwrap_or(Decimal::MAX)
107 }
108}
109
110pub trait ToDuration {
112 fn to_duration(&self) -> TimeDelta;
114}
115
116impl ToDuration for Decimal {
117 fn to_duration(&self) -> TimeDelta {
121 let nanos = self
122 .saturating_mul(NANOS_IN_HOUR)
123 .round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
128 .to_i64()
129 .unwrap_or(i64::MAX);
130 TimeDelta::nanoseconds(nanos)
131 }
132}
133
134pub(crate) struct Seconds(TimeDelta);
137
138impl number::IsZero for Seconds {
139 fn is_zero(&self) -> bool {
140 self.0.is_zero()
141 }
142}
143
144impl From<Seconds> for TimeDelta {
146 fn from(value: Seconds) -> Self {
147 value.0
148 }
149}
150
151impl fmt::Debug for Seconds {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 f.debug_tuple("Seconds")
154 .field(&self.0.num_seconds())
155 .finish()
156 }
157}
158
159impl fmt::Display for Seconds {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "{}", self.0.num_seconds())
162 }
163}
164
165impl<'buf> FromSchema<'buf, schema::Number<'buf>> for Seconds {
172 type Warning = Warning;
173
174 fn from_schema(source: &schema::Number<'buf>) -> Verdict<Self, Self::Warning> {
175 let warnings = warning::Set::new();
176
177 let (elem, digits) = match source {
181 schema::Number::Number { elem, digits } => (elem, *digits),
182 schema::Number::StringEncoded { elem, .. } => {
183 return warnings.bail(elem, Warning::invalid_type(elem));
184 }
185 };
186
187 let seconds = match digits.parse::<u64>() {
189 Ok(n) => n,
190 Err(err) => {
191 return warnings.bail(elem, Warning::Invalid(int_error_kind_as_str(*err.kind())));
192 }
193 };
194
195 let Ok(seconds) = i64::try_from(seconds) else {
198 return warnings.bail(
199 elem,
200 Warning::Invalid("The duration value is larger than an i64 can represent."),
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#[expect(clippy::allow_attributes, reason = "used during debug sessions")]
231#[allow(dead_code, reason = "used during debug sessions")]
232pub(crate) trait AsHms {
233 fn as_hms(&self) -> Hms;
235}
236
237impl AsHms for TimeDelta {
238 fn as_hms(&self) -> Hms {
239 Hms(*self)
240 }
241}
242
243impl AsHms for Decimal {
244 fn as_hms(&self) -> Hms {
246 Hms(self.to_duration())
247 }
248}
249
250#[derive(Copy, Clone)]
252pub struct Hms(pub TimeDelta);
253
254impl fmt::Debug for Hms {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 fmt::Display::fmt(self, f)
258 }
259}
260
261impl fmt::Display for Hms {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 let duration = self.0;
264 let seconds = duration.num_seconds();
265
266 if seconds.is_negative() {
268 f.write_str("-")?;
269 }
270
271 let seconds_total = seconds.abs();
273
274 let seconds = seconds_total % SECS_IN_MIN;
275 let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
276 let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
277
278 write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
279 }
280}
281
282#[cfg(test)]
283mod test_hms {
284 use chrono::TimeDelta;
285
286 use super::Hms;
287
288 #[test]
289 fn should_display_seconds() {
290 assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
291 assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
292 }
293
294 #[test]
295 fn should_display_minutes() {
296 assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
297 assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
298 }
299
300 #[test]
301 fn should_display_hours() {
302 assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
303 assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
304 }
305
306 #[test]
307 fn should_display_hours_mins_secs() {
308 assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
309 }
310}
311
312#[cfg(test)]
313mod test_to_hours_decimal {
314 use chrono::TimeDelta;
315 use rust_decimal_macros::dec;
316
317 use crate::ToHoursDecimal as _;
318
319 #[test]
320 fn to_hours_dec_should_be_correct() {
321 let actual = TimeDelta::hours(1).to_hours_dec();
322 assert_eq!(actual, dec!(1.0));
323
324 let actual = TimeDelta::seconds(3960).to_hours_dec();
325 assert_eq!(actual, dec!(1.1));
326
327 let actual = TimeDelta::seconds(360).to_hours_dec();
328 assert_eq!(actual, dec!(0.1));
329
330 let actual = TimeDelta::seconds(36).to_hours_dec();
331 assert_eq!(actual, dec!(0.01));
332
333 let actual = TimeDelta::milliseconds(36).to_hours_dec();
334 assert_eq!(actual, dec!(0.00001));
335
336 let actual = TimeDelta::nanoseconds(1).to_hours_dec();
337 assert_eq!(actual, dec!(2.777777777777778e-13));
338 }
339}
340
341#[cfg(test)]
342mod test_to_duration {
343 use chrono::TimeDelta;
344 use rust_decimal_macros::dec;
345
346 use crate::ToDuration as _;
347
348 #[test]
349 fn to_duration_should_be_correct() {
350 let actual = dec!(1.0).to_duration();
351 assert_eq!(actual, TimeDelta::hours(1));
352
353 let actual = dec!(1.1).to_duration();
354 assert_eq!(actual, TimeDelta::seconds(3960));
355
356 let actual = dec!(0.1).to_duration();
357 assert_eq!(actual, TimeDelta::seconds(360));
358
359 let actual = dec!(1e-14).to_duration();
360 assert_eq!(actual, TimeDelta::zero());
361
362 let actual = dec!(2.777e-13).to_duration();
363 assert_eq!(actual, TimeDelta::nanoseconds(1));
364 }
365}