1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6const NANOS_PER_SECOND: i64 = 1_000_000_000;
7const NANOS_PER_MINUTE: i64 = 60 * NANOS_PER_SECOND;
8const NANOS_PER_HOUR: i64 = 60 * NANOS_PER_MINUTE;
9
10#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
27pub struct Duration {
28 months: i64,
30 days: i64,
32 nanos: i64,
34}
35
36impl Duration {
37 #[must_use]
39 pub const fn new(months: i64, days: i64, nanos: i64) -> Self {
40 Self {
41 months,
42 days,
43 nanos,
44 }
45 }
46
47 #[must_use]
49 pub const fn from_months(months: i64) -> Self {
50 Self {
51 months,
52 days: 0,
53 nanos: 0,
54 }
55 }
56
57 #[must_use]
59 pub const fn from_days(days: i64) -> Self {
60 Self {
61 months: 0,
62 days,
63 nanos: 0,
64 }
65 }
66
67 #[must_use]
69 pub const fn from_nanos(nanos: i64) -> Self {
70 Self {
71 months: 0,
72 days: 0,
73 nanos,
74 }
75 }
76
77 #[must_use]
79 pub const fn from_seconds(secs: i64) -> Self {
80 Self {
81 months: 0,
82 days: 0,
83 nanos: secs * NANOS_PER_SECOND,
84 }
85 }
86
87 #[must_use]
89 pub const fn months(&self) -> i64 {
90 self.months
91 }
92
93 #[must_use]
95 pub const fn days(&self) -> i64 {
96 self.days
97 }
98
99 #[must_use]
101 pub const fn nanos(&self) -> i64 {
102 self.nanos
103 }
104
105 #[must_use]
107 pub const fn is_zero(&self) -> bool {
108 self.months == 0 && self.days == 0 && self.nanos == 0
109 }
110
111 #[must_use]
113 pub const fn neg(self) -> Self {
114 Self {
115 months: -self.months,
116 days: -self.days,
117 nanos: -self.nanos,
118 }
119 }
120
121 #[must_use]
123 pub const fn add(self, other: Self) -> Self {
124 Self {
125 months: self.months + other.months,
126 days: self.days + other.days,
127 nanos: self.nanos + other.nanos,
128 }
129 }
130
131 #[must_use]
133 pub const fn sub(self, other: Self) -> Self {
134 Self {
135 months: self.months - other.months,
136 days: self.days - other.days,
137 nanos: self.nanos - other.nanos,
138 }
139 }
140
141 #[must_use]
143 pub const fn mul(self, factor: i64) -> Self {
144 Self {
145 months: self.months * factor,
146 days: self.days * factor,
147 nanos: self.nanos * factor,
148 }
149 }
150
151 #[must_use]
153 pub const fn div(self, divisor: i64) -> Self {
154 Self {
155 months: self.months / divisor,
156 days: self.days / divisor,
157 nanos: self.nanos / divisor,
158 }
159 }
160
161 #[must_use]
165 pub fn parse(s: &str) -> Option<Self> {
166 let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
167 (true, rest)
168 } else {
169 (false, s)
170 };
171
172 let s = s.strip_prefix('P')?;
173 let mut months: i64 = 0;
174 let mut days: i64 = 0;
175 let mut nanos: i64 = 0;
176 let mut in_time = false;
177 let mut num_start = 0;
178 let mut has_content = false;
179
180 let bytes = s.as_bytes();
181 let mut i = 0;
182 while i < bytes.len() {
183 match bytes[i] {
184 b'T' => {
185 in_time = true;
186 i += 1;
187 num_start = i;
188 }
189 b'Y' if !in_time => {
190 let n: i64 = s[num_start..i].parse().ok()?;
191 months += n * 12;
192 has_content = true;
193 i += 1;
194 num_start = i;
195 }
196 b'M' if !in_time => {
197 let n: i64 = s[num_start..i].parse().ok()?;
198 months += n;
199 has_content = true;
200 i += 1;
201 num_start = i;
202 }
203 b'W' if !in_time => {
204 let n: i64 = s[num_start..i].parse().ok()?;
205 days += n * 7;
206 has_content = true;
207 i += 1;
208 num_start = i;
209 }
210 b'D' if !in_time => {
211 let n: i64 = s[num_start..i].parse().ok()?;
212 days += n;
213 has_content = true;
214 i += 1;
215 num_start = i;
216 }
217 b'H' if in_time => {
218 let n: i64 = s[num_start..i].parse().ok()?;
219 nanos += n * NANOS_PER_HOUR;
220 has_content = true;
221 i += 1;
222 num_start = i;
223 }
224 b'M' if in_time => {
225 let n: i64 = s[num_start..i].parse().ok()?;
226 nanos += n * NANOS_PER_MINUTE;
227 has_content = true;
228 i += 1;
229 num_start = i;
230 }
231 b'S' if in_time => {
232 let text = &s[num_start..i];
233 if let Some(dot_pos) = text.find('.') {
234 let int_part: i64 = text[..dot_pos].parse().ok()?;
235 let frac_str = &text[dot_pos + 1..];
236 let frac_len = frac_str.len().min(9);
237 let frac: i64 = frac_str[..frac_len].parse().ok()?;
238 #[allow(clippy::cast_possible_truncation)]
240 let scale = 10i64.pow(9 - frac_len as u32);
241 nanos += int_part * NANOS_PER_SECOND + frac * scale;
242 } else {
243 let n: i64 = text.parse().ok()?;
244 nanos += n * NANOS_PER_SECOND;
245 }
246 has_content = true;
247 i += 1;
248 num_start = i;
249 }
250 _ => {
251 i += 1;
252 }
253 }
254 }
255
256 if !has_content {
257 return None;
258 }
259
260 let dur = Self {
261 months,
262 days,
263 nanos,
264 };
265 Some(if negative { dur.neg() } else { dur })
266 }
267}
268
269impl PartialOrd for Duration {
272 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
273 let m = self.months.cmp(&other.months);
276 let d = self.days.cmp(&other.days);
277 let n = self.nanos.cmp(&other.nanos);
278
279 if m == d && d == n {
280 Some(m)
281 } else if (m == std::cmp::Ordering::Equal || m == d)
282 && (d == std::cmp::Ordering::Equal || d == n)
283 && (m == std::cmp::Ordering::Equal || m == n)
284 {
285 if m != std::cmp::Ordering::Equal {
287 Some(m)
288 } else if d != std::cmp::Ordering::Equal {
289 Some(d)
290 } else {
291 Some(n)
292 }
293 } else {
294 None }
296 }
297}
298
299impl fmt::Debug for Duration {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(f, "Duration({})", self)
302 }
303}
304
305impl fmt::Display for Duration {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 let (neg, months, days, nanos) = if self.months < 0 || self.days < 0 || self.nanos < 0 {
308 if self.months <= 0 && self.days <= 0 && self.nanos <= 0 {
310 (true, -self.months, -self.days, -self.nanos)
311 } else {
312 (false, self.months, self.days, self.nanos)
314 }
315 } else {
316 (false, self.months, self.days, self.nanos)
317 };
318
319 if neg {
320 write!(f, "-")?;
321 }
322 write!(f, "P")?;
323
324 let years = months / 12;
325 let m = months % 12;
326
327 if years != 0 {
328 write!(f, "{years}Y")?;
329 }
330 if m != 0 {
331 write!(f, "{m}M")?;
332 }
333 if days != 0 {
334 write!(f, "{days}D")?;
335 }
336
337 let hours = nanos / NANOS_PER_HOUR;
339 let remaining = nanos % NANOS_PER_HOUR;
340 let minutes = remaining / NANOS_PER_MINUTE;
341 let remaining = remaining % NANOS_PER_MINUTE;
342 let secs = remaining / NANOS_PER_SECOND;
343 let sub_nanos = remaining % NANOS_PER_SECOND;
344
345 if hours != 0 || minutes != 0 || secs != 0 || sub_nanos != 0 {
346 write!(f, "T")?;
347 if hours != 0 {
348 write!(f, "{hours}H")?;
349 }
350 if minutes != 0 {
351 write!(f, "{minutes}M")?;
352 }
353 if secs != 0 || sub_nanos != 0 {
354 if sub_nanos != 0 {
355 let frac = format!("{:09}", sub_nanos);
356 let trimmed = frac.trim_end_matches('0');
357 write!(f, "{secs}.{trimmed}S")?;
358 } else {
359 write!(f, "{secs}S")?;
360 }
361 }
362 }
363
364 if !neg && years == 0 && m == 0 && days == 0 && nanos == 0 {
366 write!(f, "T0S")?;
367 }
368
369 Ok(())
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_constructors() {
379 let d = Duration::new(14, 3, 0);
380 assert_eq!(d.months(), 14);
381 assert_eq!(d.days(), 3);
382 assert_eq!(d.nanos(), 0);
383
384 assert_eq!(Duration::from_months(6).months(), 6);
385 assert_eq!(Duration::from_days(10).days(), 10);
386 assert_eq!(Duration::from_nanos(1_000_000_000).nanos(), 1_000_000_000);
387 }
388
389 #[test]
390 fn test_parse_full() {
391 let d = Duration::parse("P1Y2M3DT4H5M6S").unwrap();
392 assert_eq!(d.months(), 14); assert_eq!(d.days(), 3);
394 assert_eq!(
395 d.nanos(),
396 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND
397 );
398 }
399
400 #[test]
401 fn test_parse_partial() {
402 let d = Duration::parse("P1Y").unwrap();
403 assert_eq!(d.months(), 12);
404 assert_eq!(d.days(), 0);
405
406 let d = Duration::parse("PT30S").unwrap();
407 assert_eq!(d.months(), 0);
408 assert_eq!(d.nanos(), 30 * NANOS_PER_SECOND);
409
410 let d = Duration::parse("P2W").unwrap();
411 assert_eq!(d.days(), 14);
412 }
413
414 #[test]
415 fn test_parse_fractional_seconds() {
416 let d = Duration::parse("PT0.5S").unwrap();
417 assert_eq!(d.nanos(), 500_000_000);
418
419 let d = Duration::parse("PT1.123S").unwrap();
420 assert_eq!(d.nanos(), 1_123_000_000);
421 }
422
423 #[test]
424 fn test_parse_negative() {
425 let d = Duration::parse("-P1Y").unwrap();
426 assert_eq!(d.months(), -12);
427 }
428
429 #[test]
430 fn test_parse_invalid() {
431 assert!(Duration::parse("").is_none());
432 assert!(Duration::parse("P").is_none());
433 assert!(Duration::parse("not-a-duration").is_none());
434 }
435
436 #[test]
437 fn test_display_roundtrip() {
438 let cases = ["P1Y2M3DT4H5M6S", "P1Y", "P3D", "PT30S", "PT0.5S", "P2W"];
439 for case in cases {
440 let d = Duration::parse(case).unwrap();
441 let reparsed = Duration::parse(&d.to_string()).unwrap();
442 assert_eq!(d, reparsed, "roundtrip failed for {case}");
443 }
444 }
445
446 #[test]
447 fn test_arithmetic() {
448 let a = Duration::new(1, 2, 3);
449 let b = Duration::new(4, 5, 6);
450 assert_eq!(a.add(b), Duration::new(5, 7, 9));
451 assert_eq!(a.sub(b), Duration::new(-3, -3, -3));
452 assert_eq!(a.mul(3), Duration::new(3, 6, 9));
453 assert_eq!(a.neg(), Duration::new(-1, -2, -3));
454 }
455
456 #[test]
457 fn test_partial_ord() {
458 let a = Duration::new(1, 2, 3);
459 let b = Duration::new(2, 3, 4);
460 assert!(a < b);
461
462 let c = Duration::new(2, 1, 0);
464 let d = Duration::new(1, 100, 0);
465 assert_eq!(c.partial_cmp(&d), None);
466 }
467
468 #[test]
469 fn test_zero() {
470 let z = Duration::default();
471 assert!(z.is_zero());
472 assert_eq!(z.to_string(), "PT0S");
473 }
474}