1use rust_decimal::prelude::ToPrimitive as _;
52use rust_decimal::Decimal;
53use serde::de::Error as DeError;
54use serde::de::Visitor;
55use serde::Deserialize;
56use serde::Deserializer;
57use serde::Serialize;
58use serde::Serializer;
59use std::convert::TryFrom;
60use std::error::Error as StdError;
61use std::fmt::Display;
62use std::fmt::Formatter;
63use std::fmt::Result as FmtResult;
64use std::str::Chars;
65use std::str::FromStr;
66use std::time::Duration as StdDuration;
67
68#[derive(Debug, Clone, Copy)]
74#[repr(transparent)]
75pub struct EdmDuration(Decimal);
76
77impl EdmDuration {
78 #[must_use]
81 pub fn as_f64_seconds(&self) -> f64 {
82 Self::decimal_to_f64_lossy(self.0)
83 }
84
85 #[must_use]
87 pub const fn as_decimal(&self) -> Decimal {
88 self.0
89 }
90
91 fn take_digits<'a>(chars: &Chars<'a>) -> (&'a str, Option<char>, Chars<'a>) {
92 let s = chars.as_str();
93 for (i, ch) in s.char_indices() {
94 if ch.is_ascii_digit() || ch == '.' {
95 continue;
96 }
97 let digits = &s[..i];
98 let rest = &s[i + ch.len_utf8()..];
99 return (digits, Some(ch), rest.chars());
100 }
101 (s, None, "".chars())
102 }
103
104 fn decimal_to_f64_lossy(d: Decimal) -> f64 {
105 d.to_f64().unwrap_or_else(|| {
106 if d.is_sign_negative() {
107 f64::NEG_INFINITY
108 } else {
109 f64::INFINITY
110 }
111 })
112 }
113
114 fn div_with_reminder(v: Decimal, d: Decimal) -> (Decimal, Decimal) {
115 let reminder = v % d;
116 ((v - reminder) / d, reminder)
117 }
118}
119
120#[derive(Debug)]
122pub enum Error {
123 InvalidEdmDuration(String),
125 Overflow(String),
127 CannotConvertNegativeEdmDuration,
129 ValueTooBig,
132}
133
134impl Display for Error {
135 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
136 match self {
137 Self::InvalidEdmDuration(v) => write!(f, "invalid duration: {v}"),
138 Self::Overflow(v) => write!(f, "invalid duration: number overflow: {v}"),
139 Self::CannotConvertNegativeEdmDuration => "cannot convert negative duration".fmt(f),
140 Self::ValueTooBig => "duration: value to big".fmt(f),
141 }
142 }
143}
144
145impl StdError for Error {}
146
147impl TryFrom<EdmDuration> for StdDuration {
152 type Error = Error;
153
154 fn try_from(v: EdmDuration) -> Result<Self, Error> {
155 if v.0.is_sign_negative() {
156 return Err(Error::CannotConvertNegativeEdmDuration);
157 }
158 if v.0.is_integer() {
159 let p = u64::try_from(v.0).map_err(|_| Error::ValueTooBig)?;
160 return Ok(Self::from_secs(p));
161 }
162 let v =
163 v.0.checked_mul(Decimal::ONE_THOUSAND)
164 .ok_or(Error::ValueTooBig)?;
165 if v.is_integer() {
166 let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
167 return Ok(Self::from_millis(p));
168 }
169 let v = v
170 .checked_mul(Decimal::ONE_THOUSAND)
171 .ok_or(Error::ValueTooBig)?;
172 if v.is_integer() {
173 let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
174 return Ok(Self::from_micros(p));
175 }
176 let v = v
177 .checked_mul(Decimal::ONE_THOUSAND)
178 .ok_or(Error::ValueTooBig)?
179 .round();
180 let p = u64::try_from(v).map_err(|_| Error::ValueTooBig)?;
181 Ok(Self::from_nanos(p))
182 }
183}
184
185impl FromStr for EdmDuration {
186 type Err = Error;
187 fn from_str(v: &str) -> Result<Self, Error> {
188 let mut chars = v.chars();
189 let make_err = || Error::InvalidEdmDuration(v.into());
190 let overflow_err = || Error::Overflow(v.into());
191 let maybe_sign = chars.next().ok_or_else(make_err)?;
192 let (neg, p) = if maybe_sign == '-' {
193 (Decimal::NEGATIVE_ONE, chars.next().ok_or_else(make_err)?)
194 } else {
195 (Decimal::ONE, maybe_sign)
196 };
197 (p == 'P').then_some(()).ok_or_else(make_err)?;
198
199 let to_decimal = |val: &str, mul| {
200 Decimal::from_str_exact(val)
201 .map(|d| d * Decimal::from(mul))
202 .map_err(|_| make_err())
203 };
204
205 let mut result = Decimal::ZERO;
206 let (val, maybe_next, mut chars) = Self::take_digits(&chars);
207 match maybe_next {
208 Some('T') => (),
209 Some('D') => match chars.next() {
210 Some('T') => {
211 result = result
212 .checked_add(to_decimal(val, 3600 * 24)?)
213 .ok_or_else(overflow_err)?;
214 }
215 None => return to_decimal(val, 3600 * 24).map(|v| Self(v * neg)),
216 _ => Err(make_err())?,
217 },
218 _ => Err(make_err())?,
219 }
220
221 loop {
222 let (val, maybe_next, new_chars) = Self::take_digits(&chars);
223 chars = new_chars;
224 let mul = match maybe_next {
225 Some('H') => 3600,
226 Some('M') => 60,
227 Some('S') => 1,
228 Some(_) => Err(make_err())?,
229 None => break,
230 };
231 result = result
232 .checked_add(to_decimal(val, mul)?)
233 .ok_or_else(overflow_err)?;
234 }
235 Ok(Self(result * neg))
236 }
237}
238
239impl Display for EdmDuration {
240 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
241 if self.0 == Decimal::ZERO {
242 return write!(f, "PT0S");
244 }
245 let value = if self.0.is_sign_negative() {
246 write!(f, "-P")?;
247 -self.0
248 } else {
249 write!(f, "P")?;
250 self.0
251 };
252 let (days, value) = Self::div_with_reminder(value, Decimal::from(24 * 3600));
253 if days != Decimal::ZERO {
254 write!(f, "{}D", days.normalize())?;
255 }
256 if value != Decimal::ZERO {
257 write!(f, "T")?;
258 let (hours, value) = Self::div_with_reminder(value, Decimal::from(3600));
259 if hours != Decimal::ZERO {
260 write!(f, "{}H", hours.normalize())?;
261 }
262 let (mins, value) = Self::div_with_reminder(value, Decimal::from(60));
263 if mins != Decimal::ZERO {
264 write!(f, "{}M", mins.normalize())?;
265 }
266 write!(f, "{}S", value.normalize())?;
267 }
268 Ok(())
269 }
270}
271
272impl<'de> Deserialize<'de> for EdmDuration {
273 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
274 struct ValVisitor {}
275 impl Visitor<'_> for ValVisitor {
276 type Value = EdmDuration;
277
278 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> FmtResult {
279 formatter.write_str("Edm.Duration string")
280 }
281 fn visit_str<E: DeError>(self, value: &str) -> Result<Self::Value, E> {
282 value.parse().map_err(DeError::custom)
283 }
284 }
285
286 de.deserialize_string(ValVisitor {})
287 }
288}
289
290impl Serialize for EdmDuration {
291 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
292 self.to_string().serialize(serializer)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use rust_decimal::Decimal;
300
301 fn dec(s: &str) -> Decimal {
302 Decimal::from_str_exact(s).unwrap()
303 }
304
305 #[test]
306 fn parses_time_only_hms() {
307 let d = EdmDuration::from_str("PT1H2M3S").unwrap();
308 assert_eq!(d.0, Decimal::from(3600 + 120 + 3));
309 }
310
311 #[test]
312 fn parses_day_only() {
313 let d = EdmDuration::from_str("P3D").unwrap();
314 assert_eq!(d.0, Decimal::from(3 * 86400));
315 }
316
317 #[test]
318 fn parses_day_and_time() {
319 let d = EdmDuration::from_str("P1DT1H").unwrap();
320 assert_eq!(d.0, Decimal::from(86400 + 3600));
321 }
322
323 #[test]
324 fn parses_fractional_seconds() {
325 let d = EdmDuration::from_str("PT0.25S").unwrap();
326 assert_eq!(d.0, dec("0.25"));
327 }
328
329 #[test]
330 fn parses_fractional_minutes_and_days() {
331 let d1 = EdmDuration::from_str("PT1.5M").unwrap();
332 assert_eq!(d1.0, Decimal::from(90));
333
334 let d2 = EdmDuration::from_str("P1.5D").unwrap();
335 assert_eq!(d2.0, Decimal::from(129600));
336 }
337
338 #[test]
339 fn parses_negative_durations() {
340 let d1 = EdmDuration::from_str("-PT2M").unwrap();
341 assert_eq!(d1.0, Decimal::from(-120));
342
343 let d2 = EdmDuration::from_str("-P1D").unwrap();
344 assert_eq!(d2.0, Decimal::from(-86400));
345 }
346
347 #[test]
348 fn parses_zero_variants() {
349 let d1 = EdmDuration::from_str("PT0S").unwrap();
350 assert_eq!(d1.0, Decimal::from(0));
351
352 let d2 = EdmDuration::from_str("PT").unwrap();
353 assert_eq!(d2.0, Decimal::from(0));
354 }
355
356 #[test]
357 fn rejects_malformed_inputs() {
358 assert!(EdmDuration::from_str("").is_err());
359 assert!(EdmDuration::from_str("P").is_err());
360 assert!(EdmDuration::from_str("T1H").is_err());
361 assert!(EdmDuration::from_str("PT1X").is_err());
362 assert!(EdmDuration::from_str("-P").is_err());
363 }
364
365 #[test]
366 fn formats_zero_duration() {
367 let d = EdmDuration::from_str("PT").unwrap();
368 assert_eq!(format!("{}", d), "PT0S");
369 }
370
371 #[test]
372 fn formats_seconds_only() {
373 let d = EdmDuration::from_str("PT3S").unwrap();
374 assert_eq!(format!("{}", d), "PT3S");
375 }
376
377 #[test]
378 fn formats_fractional_seconds() {
379 let d = EdmDuration::from_str("PT0.25S").unwrap();
380 assert_eq!(format!("{}", d), "PT0.25S");
381 }
382
383 #[test]
384 fn formats_minutes_and_hours_with_zero_seconds() {
385 let d1 = EdmDuration::from_str("PT2M").unwrap();
386 assert_eq!(format!("{}", d1), "PT2M0S");
387
388 let d2 = EdmDuration::from_str("PT1H").unwrap();
389 assert_eq!(format!("{}", d2), "PT1H0S");
390
391 let d3 = EdmDuration::from_str("PT1H2M").unwrap();
392 assert_eq!(format!("{}", d3), "PT1H2M0S");
393 }
394
395 #[test]
396 fn formats_days_only_and_day_time() {
397 let d1 = EdmDuration::from_str("P3D").unwrap();
398 assert_eq!(format!("{}", d1), "P3D");
399
400 let d2 = EdmDuration::from_str("P1DT1H").unwrap();
401 assert_eq!(format!("{}", d2), "P1DT1H0S");
402 }
403
404 #[test]
405 fn formats_negative_durations() {
406 let d1 = EdmDuration::from_str("-PT2M").unwrap();
407 assert_eq!(format!("{}", d1), "-PT2M0S");
408
409 let d2 = EdmDuration::from_str("-P1D").unwrap();
410 assert_eq!(format!("{}", d2), "-P1D");
411 }
412
413 #[test]
414 fn normalizes_fractional_minutes_on_display() {
415 let d = EdmDuration::from_str("PT1.5M").unwrap();
416 assert_eq!(format!("{}", d), "PT1M30S");
417 }
418
419 #[test]
420 fn formats_trims_trailing_zero_seconds() {
421 let d = EdmDuration::from_str("PT30.0S").unwrap();
422 assert_eq!(format!("{}", d), "PT30S");
424 }
425
426 #[test]
427 fn formats_leading_zero_inputs() {
428 let d = EdmDuration::from_str("PT01S").unwrap();
429 assert_eq!(format!("{}", d), "PT1S");
430 }
431
432 #[test]
433 fn formats_large_hours_breakdown() {
434 let d = EdmDuration::from_str("PT100000H").unwrap();
436 assert_eq!(format!("{}", d), "P4166DT16H0S");
437 }
438
439 #[test]
440 fn formats_fractional_hours() {
441 let d = EdmDuration::from_str("PT1.75H").unwrap();
442 assert_eq!(format!("{}", d), "PT1H45M0S");
443 }
444
445 #[test]
446 fn formats_fractional_days() {
447 let d = EdmDuration::from_str("P1.25D").unwrap();
448 assert_eq!(format!("{}", d), "P1DT6H0S");
449 }
450
451 #[test]
452 fn formats_minute_and_hour_carry_from_seconds() {
453 let d1 = EdmDuration::from_str("PT60S").unwrap();
454 assert_eq!(format!("{}", d1), "PT1M0S");
455
456 let d2 = EdmDuration::from_str("PT3600S").unwrap();
457 assert_eq!(format!("{}", d2), "PT1H0S");
458 }
459
460 #[test]
461 fn formats_trims_excess_zero_fraction() {
462 let d = EdmDuration::from_str("PT1.2300S").unwrap();
463 assert_eq!(format!("{}", d), "PT1.23S");
464 }
465
466 #[test]
467 fn test_exact_division() {
468 let (q, r) = EdmDuration::div_with_reminder(Decimal::new(10, 0), Decimal::new(5, 0));
469 assert_eq!(q, Decimal::new(2, 0));
470 assert_eq!(r, Decimal::new(0, 0));
471 }
472
473 #[test]
474 fn test_positive_non_exact() {
475 let (q, r) = EdmDuration::div_with_reminder(Decimal::new(10, 0), Decimal::new(4, 0));
476 assert_eq!(q, Decimal::new(2, 0));
477 assert_eq!(r, Decimal::new(2, 0));
478 }
479
480 #[test]
481 fn test_non_integer_division() {
482 let v = Decimal::new(105, 1); let d = Decimal::new(4, 0); let (q, r) = EdmDuration::div_with_reminder(v, d);
485 assert_eq!(q, Decimal::new(2, 0));
486 assert_eq!(r, Decimal::new(25, 1)); }
488
489 #[test]
490 fn test_zero_dividend() {
491 let (q, r) = EdmDuration::div_with_reminder(Decimal::new(0, 0), Decimal::new(5, 0));
492 assert_eq!(q, Decimal::new(0, 0));
493 assert_eq!(r, Decimal::new(0, 0));
494 }
495}