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 let scale = 10i64.pow(9 - frac_len as u32);
239 nanos += int_part * NANOS_PER_SECOND + frac * scale;
240 } else {
241 let n: i64 = text.parse().ok()?;
242 nanos += n * NANOS_PER_SECOND;
243 }
244 has_content = true;
245 i += 1;
246 num_start = i;
247 }
248 _ => {
249 i += 1;
250 }
251 }
252 }
253
254 if !has_content {
255 return None;
256 }
257
258 let dur = Self {
259 months,
260 days,
261 nanos,
262 };
263 Some(if negative { dur.neg() } else { dur })
264 }
265}
266
267impl PartialOrd for Duration {
270 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
271 let m = self.months.cmp(&other.months);
274 let d = self.days.cmp(&other.days);
275 let n = self.nanos.cmp(&other.nanos);
276
277 if m == d && d == n {
278 Some(m)
279 } else if (m == std::cmp::Ordering::Equal || m == d)
280 && (d == std::cmp::Ordering::Equal || d == n)
281 && (m == std::cmp::Ordering::Equal || m == n)
282 {
283 if m != std::cmp::Ordering::Equal {
285 Some(m)
286 } else if d != std::cmp::Ordering::Equal {
287 Some(d)
288 } else {
289 Some(n)
290 }
291 } else {
292 None }
294 }
295}
296
297impl fmt::Debug for Duration {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 write!(f, "Duration({})", self)
300 }
301}
302
303impl fmt::Display for Duration {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 let (neg, months, days, nanos) = if self.months < 0 || self.days < 0 || self.nanos < 0 {
306 if self.months <= 0 && self.days <= 0 && self.nanos <= 0 {
308 (true, -self.months, -self.days, -self.nanos)
309 } else {
310 (false, self.months, self.days, self.nanos)
312 }
313 } else {
314 (false, self.months, self.days, self.nanos)
315 };
316
317 if neg {
318 write!(f, "-")?;
319 }
320 write!(f, "P")?;
321
322 let years = months / 12;
323 let m = months % 12;
324
325 if years != 0 {
326 write!(f, "{years}Y")?;
327 }
328 if m != 0 {
329 write!(f, "{m}M")?;
330 }
331 if days != 0 {
332 write!(f, "{days}D")?;
333 }
334
335 let hours = nanos / NANOS_PER_HOUR;
337 let remaining = nanos % NANOS_PER_HOUR;
338 let minutes = remaining / NANOS_PER_MINUTE;
339 let remaining = remaining % NANOS_PER_MINUTE;
340 let secs = remaining / NANOS_PER_SECOND;
341 let sub_nanos = remaining % NANOS_PER_SECOND;
342
343 if hours != 0 || minutes != 0 || secs != 0 || sub_nanos != 0 {
344 write!(f, "T")?;
345 if hours != 0 {
346 write!(f, "{hours}H")?;
347 }
348 if minutes != 0 {
349 write!(f, "{minutes}M")?;
350 }
351 if secs != 0 || sub_nanos != 0 {
352 if sub_nanos != 0 {
353 let frac = format!("{:09}", sub_nanos);
354 let trimmed = frac.trim_end_matches('0');
355 write!(f, "{secs}.{trimmed}S")?;
356 } else {
357 write!(f, "{secs}S")?;
358 }
359 }
360 }
361
362 if !neg && years == 0 && m == 0 && days == 0 && nanos == 0 {
364 write!(f, "T0S")?;
365 }
366
367 Ok(())
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_constructors() {
377 let d = Duration::new(14, 3, 0);
378 assert_eq!(d.months(), 14);
379 assert_eq!(d.days(), 3);
380 assert_eq!(d.nanos(), 0);
381
382 assert_eq!(Duration::from_months(6).months(), 6);
383 assert_eq!(Duration::from_days(10).days(), 10);
384 assert_eq!(Duration::from_nanos(1_000_000_000).nanos(), 1_000_000_000);
385 }
386
387 #[test]
388 fn test_parse_full() {
389 let d = Duration::parse("P1Y2M3DT4H5M6S").unwrap();
390 assert_eq!(d.months(), 14); assert_eq!(d.days(), 3);
392 assert_eq!(
393 d.nanos(),
394 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND
395 );
396 }
397
398 #[test]
399 fn test_parse_partial() {
400 let d = Duration::parse("P1Y").unwrap();
401 assert_eq!(d.months(), 12);
402 assert_eq!(d.days(), 0);
403
404 let d = Duration::parse("PT30S").unwrap();
405 assert_eq!(d.months(), 0);
406 assert_eq!(d.nanos(), 30 * NANOS_PER_SECOND);
407
408 let d = Duration::parse("P2W").unwrap();
409 assert_eq!(d.days(), 14);
410 }
411
412 #[test]
413 fn test_parse_fractional_seconds() {
414 let d = Duration::parse("PT0.5S").unwrap();
415 assert_eq!(d.nanos(), 500_000_000);
416
417 let d = Duration::parse("PT1.123S").unwrap();
418 assert_eq!(d.nanos(), 1_123_000_000);
419 }
420
421 #[test]
422 fn test_parse_negative() {
423 let d = Duration::parse("-P1Y").unwrap();
424 assert_eq!(d.months(), -12);
425 }
426
427 #[test]
428 fn test_parse_invalid() {
429 assert!(Duration::parse("").is_none());
430 assert!(Duration::parse("P").is_none());
431 assert!(Duration::parse("not-a-duration").is_none());
432 }
433
434 #[test]
435 fn test_display_roundtrip() {
436 let cases = ["P1Y2M3DT4H5M6S", "P1Y", "P3D", "PT30S", "PT0.5S", "P2W"];
437 for case in cases {
438 let d = Duration::parse(case).unwrap();
439 let reparsed = Duration::parse(&d.to_string()).unwrap();
440 assert_eq!(d, reparsed, "roundtrip failed for {case}");
441 }
442 }
443
444 #[test]
445 fn test_arithmetic() {
446 let a = Duration::new(1, 2, 3);
447 let b = Duration::new(4, 5, 6);
448 assert_eq!(a.add(b), Duration::new(5, 7, 9));
449 assert_eq!(a.sub(b), Duration::new(-3, -3, -3));
450 assert_eq!(a.mul(3), Duration::new(3, 6, 9));
451 assert_eq!(a.neg(), Duration::new(-1, -2, -3));
452 }
453
454 #[test]
455 fn test_partial_ord() {
456 let a = Duration::new(1, 2, 3);
457 let b = Duration::new(2, 3, 4);
458 assert!(a < b);
459
460 let c = Duration::new(2, 1, 0);
462 let d = Duration::new(1, 100, 0);
463 assert_eq!(c.partial_cmp(&d), None);
464 }
465
466 #[test]
467 fn test_zero() {
468 let z = Duration::default();
469 assert!(z.is_zero());
470 assert_eq!(z.to_string(), "PT0S");
471 }
472}