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 json::FromJson<'_> for Seconds {
172 type Warning = Warning;
173
174 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
175 let warnings = warning::Set::new();
176 let Some(s) = elem.as_number_str() else {
177 return warnings.bail(elem, Warning::invalid_type(elem));
178 };
179
180 let seconds = match s.parse::<u64>() {
182 Ok(n) => n,
183 Err(err) => {
184 return warnings.bail(elem, Warning::Invalid(int_error_kind_as_str(*err.kind())));
185 }
186 };
187
188 let Ok(seconds) = i64::try_from(seconds) else {
191 return warnings.bail(
192 elem,
193 Warning::Invalid("The duration value is larger than an i64 can represent."),
194 );
195 };
196 let dt = TimeDelta::seconds(seconds);
197
198 Ok(Seconds(dt).into_caveat(warnings))
199 }
200}
201
202impl<'buf> FromSchema<'buf, schema::Number<'buf>> for Seconds {
203 type Warning = Warning;
204
205 fn from_schema(source: &schema::Number<'buf>) -> Verdict<Self, Self::Warning> {
206 let warnings = warning::Set::new();
207
208 let (elem, digits) = match source {
212 schema::Number::Number { elem, digits } => (elem, *digits),
213 schema::Number::StringEncoded { elem, .. } => {
214 return warnings.bail(elem, Warning::invalid_type(elem));
215 }
216 };
217
218 let seconds = match digits.parse::<u64>() {
220 Ok(n) => n,
221 Err(err) => {
222 return warnings.bail(elem, Warning::Invalid(int_error_kind_as_str(*err.kind())));
223 }
224 };
225
226 let Ok(seconds) = i64::try_from(seconds) else {
229 return warnings.bail(
230 elem,
231 Warning::Invalid("The duration value is larger than an i64 can represent."),
232 );
233 };
234 let dt = TimeDelta::seconds(seconds);
235
236 Ok(Seconds(dt).into_caveat(warnings))
237 }
238}
239
240impl Cost for TimeDelta {
242 fn cost(&self, money: Money) -> Money {
243 let cost = self.to_hours_dec().saturating_mul(Decimal::from(money));
244 Money::from_decimal(cost)
245 }
246}
247
248impl SaturatingAdd for TimeDelta {
249 fn saturating_add(self, other: TimeDelta) -> TimeDelta {
250 self.checked_add(&other).unwrap_or(TimeDelta::MAX)
251 }
252}
253
254impl SaturatingSub for TimeDelta {
255 fn saturating_sub(self, other: TimeDelta) -> TimeDelta {
256 self.checked_sub(&other).unwrap_or_else(TimeDelta::zero)
257 }
258}
259
260#[expect(clippy::allow_attributes, reason = "used during debug sessions")]
262#[allow(dead_code, reason = "used during debug sessions")]
263pub(crate) trait AsHms {
264 fn as_hms(&self) -> Hms;
266}
267
268impl AsHms for TimeDelta {
269 fn as_hms(&self) -> Hms {
270 Hms(*self)
271 }
272}
273
274impl AsHms for Decimal {
275 fn as_hms(&self) -> Hms {
277 Hms(self.to_duration())
278 }
279}
280
281#[derive(Copy, Clone)]
283pub struct Hms(pub TimeDelta);
284
285impl fmt::Debug for Hms {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 fmt::Display::fmt(self, f)
289 }
290}
291
292impl fmt::Display for Hms {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 let duration = self.0;
295 let seconds = duration.num_seconds();
296
297 if seconds.is_negative() {
299 f.write_str("-")?;
300 }
301
302 let seconds_total = seconds.abs();
304
305 let seconds = seconds_total % SECS_IN_MIN;
306 let minutes = (seconds_total / SECS_IN_MIN) % MINS_IN_HOUR;
307 let hours = seconds_total / (SECS_IN_MIN * MINS_IN_HOUR);
308
309 write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
310 }
311}
312
313#[cfg(test)]
314mod test_hms {
315 use chrono::TimeDelta;
316
317 use super::Hms;
318
319 #[test]
320 fn should_display_seconds() {
321 assert_eq!(Hms(TimeDelta::seconds(0)).to_string(), "00:00:00");
322 assert_eq!(Hms(TimeDelta::seconds(59)).to_string(), "00:00:59");
323 }
324
325 #[test]
326 fn should_display_minutes() {
327 assert_eq!(Hms(TimeDelta::seconds(60)).to_string(), "00:01:00");
328 assert_eq!(Hms(TimeDelta::seconds(3600)).to_string(), "01:00:00");
329 }
330
331 #[test]
332 fn should_display_hours() {
333 assert_eq!(Hms(TimeDelta::minutes(60)).to_string(), "01:00:00");
334 assert_eq!(Hms(TimeDelta::minutes(3600)).to_string(), "60:00:00");
335 }
336
337 #[test]
338 fn should_display_hours_mins_secs() {
339 assert_eq!(Hms(TimeDelta::seconds(87030)).to_string(), "24:10:30");
340 }
341}
342
343#[cfg(test)]
344mod test_to_hours_decimal {
345 use chrono::TimeDelta;
346 use rust_decimal_macros::dec;
347
348 use crate::ToHoursDecimal as _;
349
350 #[test]
351 fn to_hours_dec_should_be_correct() {
352 let actual = TimeDelta::hours(1).to_hours_dec();
353 assert_eq!(actual, dec!(1.0));
354
355 let actual = TimeDelta::seconds(3960).to_hours_dec();
356 assert_eq!(actual, dec!(1.1));
357
358 let actual = TimeDelta::seconds(360).to_hours_dec();
359 assert_eq!(actual, dec!(0.1));
360
361 let actual = TimeDelta::seconds(36).to_hours_dec();
362 assert_eq!(actual, dec!(0.01));
363
364 let actual = TimeDelta::milliseconds(36).to_hours_dec();
365 assert_eq!(actual, dec!(0.00001));
366
367 let actual = TimeDelta::nanoseconds(1).to_hours_dec();
368 assert_eq!(actual, dec!(2.777777777777778e-13));
369 }
370}
371
372#[cfg(test)]
373mod test_to_duration {
374 use chrono::TimeDelta;
375 use rust_decimal_macros::dec;
376
377 use crate::ToDuration as _;
378
379 #[test]
380 fn to_duration_should_be_correct() {
381 let actual = dec!(1.0).to_duration();
382 assert_eq!(actual, TimeDelta::hours(1));
383
384 let actual = dec!(1.1).to_duration();
385 assert_eq!(actual, TimeDelta::seconds(3960));
386
387 let actual = dec!(0.1).to_duration();
388 assert_eq!(actual, TimeDelta::seconds(360));
389
390 let actual = dec!(1e-14).to_duration();
391 assert_eq!(actual, TimeDelta::zero());
392
393 let actual = dec!(2.777e-13).to_duration();
394 assert_eq!(actual, TimeDelta::nanoseconds(1));
395 }
396}