1use alloc::string::ToString;
2use core::fmt;
3use core::str::FromStr;
4
5use crate::error::Error;
6use crate::event::Atom;
7use crate::ext::Extension;
8use crate::ext::datetime::write_fraction;
9use crate::ext::known::{WellKnown, impl_well_known, invalid, out_of_range, round, trunc};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub struct Duration {
34 pub seconds: i64,
36 pub nanosecond: i32,
39}
40
41impl Duration {
42 pub fn is_valid(&self) -> bool {
45 self.nanosecond.unsigned_abs() < 1_000_000_000
46 && !(self.seconds > 0 && self.nanosecond < 0)
47 && !(self.seconds < 0 && self.nanosecond > 0)
48 }
49
50 pub fn is_negative(&self) -> bool {
52 self.seconds < 0 || self.nanosecond < 0
53 }
54
55 pub fn as_secs_f64(&self) -> f64 {
57 self.seconds as f64 + f64::from(self.nanosecond) / 1e9
58 }
59}
60
61impl fmt::Display for Duration {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 if self.is_negative() {
64 f.write_str("-")?;
65 }
66 f.write_str("PT")?;
67 let seconds = self.seconds.unsigned_abs();
68 let nanosecond = self.nanosecond.unsigned_abs();
69 let (hours, minutes, seconds) = (seconds / 3600, seconds / 60 % 60, seconds % 60);
70 if hours != 0 {
71 write!(f, "{}H", hours)?;
72 }
73 if minutes != 0 {
74 write!(f, "{}M", minutes)?;
75 }
76 if seconds != 0 || nanosecond != 0 || (hours == 0 && minutes == 0) {
77 write!(f, "{}", seconds)?;
78 write_fraction(f, nanosecond)?;
79 f.write_str("S")?;
80 }
81 Ok(())
82 }
83}
84
85impl FromStr for Duration {
86 type Err = Error;
87
88 fn from_str(s: &str) -> Result<Duration, Error> {
89 let (negative, rest) = match s.as_bytes().first() {
90 Some(b'-') => (true, &s[1..]),
91 Some(b'+') => (false, &s[1..]),
92 _ => (false, s),
93 };
94 let rest = rest
95 .strip_prefix("PT")
96 .or_else(|| rest.strip_prefix("pt"))
97 .ok_or_else(|| invalid("invalid duration, expected ISO 8601 duration (PT...)"))?;
98 if rest.is_empty() {
99 return Err(invalid("invalid duration"));
100 }
101
102 let overflow = || out_of_range("duration out of range");
103 let mut seconds: u64 = 0;
104 let mut nanosecond: i64 = 0;
105 let mut last_unit = 0;
107 let mut rest = rest.as_bytes();
108 while !rest.is_empty() {
109 let int_len = rest.iter().take_while(|x| x.is_ascii_digit()).count();
110 if int_len == 0 {
111 return Err(invalid("invalid duration"));
112 }
113 let value: u64 = core::str::from_utf8(&rest[..int_len])
114 .unwrap()
115 .parse()
116 .map_err(|_| overflow())?;
117 rest = &rest[int_len..];
118 let mut fraction = None;
119 if let Some(b'.' | b',') = rest.first() {
120 let frac_len = rest[1..].iter().take_while(|x| x.is_ascii_digit()).count();
121 if frac_len == 0 {
122 return Err(invalid("invalid duration"));
123 }
124 let mut nanos = 0i64;
125 for (idx, &c) in rest[1..1 + frac_len].iter().enumerate() {
126 if idx < 9 {
128 nanos = nanos * 10 + i64::from(c - b'0');
129 }
130 }
131 for _ in frac_len..9 {
132 nanos *= 10;
133 }
134 fraction = Some(nanos);
135 rest = &rest[1 + frac_len..];
136 }
137 let (unit, factor) = match rest.first() {
138 Some(b'H' | b'h') => (1, 3600),
139 Some(b'M' | b'm') => (2, 60),
140 Some(b'S' | b's') => (3, 1),
141 _ => return Err(invalid("invalid duration unit")),
142 };
143 if unit <= last_unit || (fraction.is_some() && unit != 3) {
144 return Err(invalid("invalid duration"));
145 }
146 last_unit = unit;
147 rest = &rest[1..];
148 seconds = value
149 .checked_mul(factor)
150 .and_then(|x| x.checked_add(seconds))
151 .ok_or_else(overflow)?;
152 nanosecond = fraction.unwrap_or(0);
153 }
154
155 let seconds = if negative {
156 nanosecond = -nanosecond;
157 0i64.checked_sub_unsigned(seconds)
158 } else {
159 i64::try_from(seconds).ok()
160 }
161 .ok_or_else(overflow)?;
162 Ok(Duration {
163 seconds,
164 nanosecond: nanosecond as i32,
165 })
166 }
167}
168
169impl Extension for Duration {
170 fn name(&self) -> &str {
171 "duration"
172 }
173
174 fn fallback(&self) -> Atom<'_> {
175 Atom::Str(self.to_string().into())
176 }
177}
178
179impl WellKnown for Duration {
180 const EXPECTING: &'static str = "duration";
181
182 fn from_atom(atom: &Atom) -> Result<Option<Duration>, Error> {
184 Ok(Some(match *atom {
185 Atom::Ext(ref ext) => match ext.downcast_ref::<Duration>() {
186 Some(value) => *value,
187 None => return Ok(None),
188 },
189 Atom::Str(ref value) => value.parse()?,
190 Atom::U64(value) => Duration {
191 seconds: i64::try_from(value).map_err(|_| out_of_range("duration out of range"))?,
192 nanosecond: 0,
193 },
194 Atom::I64(value) => Duration {
195 seconds: value,
196 nanosecond: 0,
197 },
198 Atom::F64(value) => {
199 if !value.is_finite() || value.abs() >= 9.2e18 {
200 return Err(out_of_range("duration out of range"));
201 }
202 let whole = trunc(value);
203 let mut seconds = whole as i64;
204 let mut nanosecond = round((value - whole) * 1e9) as i32;
205 if nanosecond.unsigned_abs() == 1_000_000_000 {
207 seconds += i64::from(nanosecond.signum());
208 nanosecond = 0;
209 }
210 Duration {
211 seconds,
212 nanosecond,
213 }
214 }
215 _ => return Ok(None),
216 }))
217 }
218}
219
220impl_well_known!(Duration);
221
222#[test]
223fn test_duration_format() {
224 let cases = [
225 (0, 0, "PT0S"),
226 (1, 500_000_000, "PT1.5S"),
227 (-1, -500_000_000, "-PT1.5S"),
228 (0, -1, "-PT0.000000001S"),
229 (3600, 0, "PT1H"),
230 (3661, 0, "PT1H1M1S"),
231 (90, 0, "PT1M30S"),
232 (i64::MAX, 999_999_999, "PT2562047788015215H30M7.999999999S"),
233 (
234 -i64::MAX,
235 -999_999_999,
236 "-PT2562047788015215H30M7.999999999S",
237 ),
238 ];
239 for (seconds, nanosecond, expected) in cases {
240 let duration = Duration {
241 seconds,
242 nanosecond,
243 };
244 assert_eq!(duration.to_string(), expected);
245 assert_eq!(expected.parse::<Duration>().unwrap(), duration);
246 }
247 assert_eq!(
248 "pt1m".parse::<Duration>().unwrap(),
249 Duration {
250 seconds: 60,
251 nanosecond: 0
252 }
253 );
254 for invalid in [
255 "", "P", "PT", "P1D", "PT1S1M", "PT1.5M", "PT.5S", "PT1", "PTS", "1S",
256 ] {
257 assert!(invalid.parse::<Duration>().is_err(), "{}", invalid);
258 }
259}