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: i64 = 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: i64 = 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 if negative {
156 seconds = -seconds;
157 nanosecond = -nanosecond;
158 }
159 Ok(Duration {
160 seconds,
161 nanosecond: nanosecond as i32,
162 })
163 }
164}
165
166impl Extension for Duration {
167 fn name(&self) -> &str {
168 "duration"
169 }
170
171 fn fallback(&self) -> Atom<'_> {
172 Atom::Str(self.to_string().into())
173 }
174}
175
176impl WellKnown for Duration {
177 const EXPECTING: &'static str = "duration";
178
179 fn from_atom(atom: &Atom) -> Result<Option<Duration>, Error> {
181 Ok(Some(match *atom {
182 Atom::Ext(ref ext) => match ext.downcast_ref::<Duration>() {
183 Some(value) => *value,
184 None => return Ok(None),
185 },
186 Atom::Str(ref value) => value.parse()?,
187 Atom::U64(value) => Duration {
188 seconds: i64::try_from(value).map_err(|_| out_of_range("duration out of range"))?,
189 nanosecond: 0,
190 },
191 Atom::I64(value) => Duration {
192 seconds: value,
193 nanosecond: 0,
194 },
195 Atom::F64(value) => {
196 if !value.is_finite() || value.abs() >= 9.2e18 {
197 return Err(out_of_range("duration out of range"));
198 }
199 let seconds = trunc(value);
200 Duration {
201 seconds: seconds as i64,
202 nanosecond: round((value - seconds) * 1e9) as i32,
203 }
204 }
205 _ => return Ok(None),
206 }))
207 }
208}
209
210impl_well_known!(Duration);
211
212#[test]
213fn test_duration_format() {
214 let cases = [
215 (0, 0, "PT0S"),
216 (1, 500_000_000, "PT1.5S"),
217 (-1, -500_000_000, "-PT1.5S"),
218 (0, -1, "-PT0.000000001S"),
219 (3600, 0, "PT1H"),
220 (3661, 0, "PT1H1M1S"),
221 (90, 0, "PT1M30S"),
222 (i64::MAX, 999_999_999, "PT2562047788015215H30M7.999999999S"),
223 (
224 -i64::MAX,
225 -999_999_999,
226 "-PT2562047788015215H30M7.999999999S",
227 ),
228 ];
229 for (seconds, nanosecond, expected) in cases {
230 let duration = Duration {
231 seconds,
232 nanosecond,
233 };
234 assert_eq!(duration.to_string(), expected);
235 assert_eq!(expected.parse::<Duration>().unwrap(), duration);
236 }
237 assert_eq!(
238 "pt1m".parse::<Duration>().unwrap(),
239 Duration {
240 seconds: 60,
241 nanosecond: 0
242 }
243 );
244 for invalid in [
245 "", "P", "PT", "P1D", "PT1S1M", "PT1.5M", "PT.5S", "PT1", "PTS", "1S",
246 ] {
247 assert!(invalid.parse::<Duration>().is_err(), "{}", invalid);
248 }
249}