1use std::sync::OnceLock;
2
3use chrono::{DateTime, Duration, Months, Utc};
4
5pub use errors::ParseError;
6use parse_bytes::ParseBytes;
7
8mod errors;
9mod parse_bytes;
10mod time_units;
11
12pub fn simple(interval: &str) -> Result<Duration, ParseError> {
14 parse_interval(interval, None)
15}
16
17pub fn with_date(interval: &str, date: DateTime<Utc>) -> Result<Duration, ParseError> {
25 parse_interval(interval, Some(Box::new(move || date)))
26}
27
28pub fn with_lazy_date<D>(interval: &str, get_date: D) -> Result<Duration, ParseError>
34where
35 D: FnOnce() -> DateTime<Utc> + 'static,
36{
37 parse_interval(interval, Some(Box::new(get_date)))
38}
39
40pub fn with_now(interval: &str) -> Result<Duration, ParseError> {
46 with_lazy_date(interval, Utc::now)
47}
48
49fn parse_interval(
55 interval: &str,
56 mut get_date: Option<Box<dyn FnOnce() -> DateTime<Utc>>>,
57) -> Result<Duration, ParseError> {
58 static PATTERNS: OnceLock<[time_units::TimeUnit; 7]> = OnceLock::new();
59 let units = PATTERNS.get_or_init(|| time_units::UNITS.map(|unit| unit.compile()));
60
61 let allow_inconstant = get_date.is_some();
62
63 let mut date = None;
64 let mut bytes = ParseBytes::from_str(interval);
65 let mut duration = Duration::seconds(0);
66 let mut offset_date = None;
67 let mut is_subtracting = false;
68 let mut unit_cursor = if allow_inconstant {
69 0
70 } else {
71 2 };
73 bytes.skip_spaces();
74 if bytes.is_empty() {
75 return Err(ParseError::Empty);
76 }
77 'outer: while !bytes.is_empty() {
78 if bytes.parse_minus() {
79 is_subtracting = !is_subtracting;
80 bytes.skip_spaces();
81 }
82 let (number, fraction) = bytes.parse_number()?;
83 bytes.skip_spaces();
84 for (unit_index, unit) in units.iter().enumerate().skip(unit_cursor) {
85 unit_cursor += 1;
86 if bytes.parse_regex(&unit.regex) {
87 match unit_index {
88 0 => {
90 if fraction > 0.0 {
91 return Err(ParseError::InconstantUnitWithFraction);
92 }
93 let date =
94 date.get_or_insert_with(|| get_date.take().map(|f| f()).unwrap());
95 let offset_date = offset_date.get_or_insert(*date);
96 let months = Months::new(
97 number
98 .checked_mul(12)
99 .ok_or(ParseError::NumberOutOfRange)?
100 .try_into()?,
101 );
102 *offset_date = if is_subtracting {
103 offset_date.checked_sub_months(months)
104 } else {
105 offset_date.checked_add_months(months)
106 }
107 .ok_or(ParseError::DateOutOfRange)?;
108 }
109 1 => {
111 if fraction > 0.0 {
112 return Err(ParseError::InconstantUnitWithFraction);
113 }
114 let date =
115 date.get_or_insert_with(|| get_date.take().map(|f| f()).unwrap());
116 let offset_date = offset_date.get_or_insert(*date);
117 let months = Months::new(number.try_into()?);
118 *offset_date = if is_subtracting {
119 offset_date.checked_sub_months(months)
120 } else {
121 offset_date.checked_add_months(months)
122 }
123 .ok_or(ParseError::DateOutOfRange)?;
124 }
125 _ => {
127 let fraction_part =
128 Duration::seconds((fraction * unit.seconds as f32) as i64);
129 duration = number
130 .checked_mul(unit.seconds)
131 .map(Duration::seconds)
132 .and_then(|d| {
133 if is_subtracting {
134 duration
135 .checked_sub(&d)
136 .and_then(|d| d.checked_sub(&fraction_part))
137 } else {
138 duration
139 .checked_add(&d)
140 .and_then(|d| d.checked_add(&fraction_part))
141 }
142 })
143 .ok_or(ParseError::NumberOutOfRange)?;
144 }
145 }
146 bytes.skip_spaces();
147 continue 'outer;
148 }
149 }
150 return Err(ParseError::diagnose_unit_error(
151 &bytes,
152 units,
153 unit_cursor,
154 allow_inconstant,
155 ));
156 }
157
158 if let (Some(date), Some(offset_date)) = (date, offset_date) {
159 duration = duration
160 .checked_add(&(offset_date - date))
161 .ok_or(ParseError::NumberOutOfRange)?;
162 }
163 Ok(duration)
164}
165
166const _PATTERN: &str = r"^(?:(?:(-) ?)?(\d+) ?y(?:ears?)?\s?)?(?:(?:(-) ?)?(\d+) ?mo(?:nths?)?\s?)?(?:(?:(-) ?)?(\d+(?:\.\d+)?|\.\d+) ?w(?:eeks?)?\s?)?(?:(?:(-) ?)?(\d+(?:\.\d+)?|\.\d+) ?d(?:ays?)?\s?)?(?:(?:(-) ?)?(\d+(?:\.\d+)?|\.\d+) ?h(?:(?:ou)?rs?)?\s?)?(?:(?:(-) ?)?(\d+(?:\.\d+)?|\.\d+) ?m(?:in(?:ute)?s?)?\s?)?(?:(?:(-) ?)?(\d+(?:\.\d+)?|\.\d+) ?s(?:ec(?:ond)?s?)?\s?)?$/i";
167
168#[cfg(test)]
169mod tests {
170 use chrono::{NaiveDate, NaiveTime};
171
172 use super::*;
173
174 #[test]
176 fn overflow_date() {
177 let _ = DateTime::<Utc>::MIN_UTC - DateTime::<Utc>::MAX_UTC;
178 }
179 #[test]
180 fn simple_case() {
181 assert_eq!(simple("5 weeks 3 days"), Ok(Duration::seconds(3283200)));
182 }
183 #[test]
184 fn short() {
185 assert_eq!(simple("5w3d1h30m30s"), Ok(Duration::seconds(3288630)));
186 }
187 #[test]
188 fn subtraction() {
189 assert_eq!(simple("5 weeks -3 days"), Ok(Duration::seconds(2764800)));
190 }
191 #[test]
192 fn negative_duration() {
193 assert_eq!(simple("-5 weeks 3 days"), Ok(Duration::seconds(-3283200)));
194 }
195 #[test]
196 fn double_subtraction() {
197 assert_eq!(simple("-5 weeks -3 days"), Ok(Duration::seconds(-2764800)));
198 }
199 #[test]
200 fn space_mess() {
201 assert_eq!(
202 simple(" - 5 weeks - 3 days "),
203 Ok(Duration::seconds(-2764800))
204 );
205 }
206 #[test]
207 fn ignore_case() {
208 assert_eq!(simple("5 WEEKS 3 days"), Ok(Duration::seconds(3283200)));
209 }
210 #[test]
211 fn fractions() {
212 assert_eq!(
213 simple("0.5 week 2.5 days 3.55 hours .5 minutes 1 second"),
214 Ok(Duration::seconds(531211))
215 );
216 }
217 #[test]
219 fn fraction_rounding() {
220 assert_eq!(simple("0.1s"), Ok(Duration::seconds(0)));
221 assert_eq!(simple("0.017m"), Ok(Duration::seconds(1)));
222 }
223 #[test]
224 fn invalid_fraction() {
225 assert_eq!(simple("0.5.0d"), Err(ParseError::NoUnit(3)));
226 }
227 #[test]
228 fn lone_period() {
229 assert_eq!(simple(".d"), Err(ParseError::NoNumber(0)));
230 }
231 #[test]
232 fn inconstant_fraction() {
233 assert_eq!(
234 with_date("0.5y", date_year_month_day(2020, 6, 20)),
235 Err(ParseError::InconstantUnitWithFraction)
236 );
237 }
238 #[test]
239 fn empty_input() {
240 assert_eq!(simple(""), Err(ParseError::Empty));
241 }
242 #[test]
243 fn spaces_input() {
244 assert_eq!(simple(" "), Err(ParseError::Empty));
245 }
246 #[test]
247 fn duplicate_units() {
248 assert_eq!(
249 simple("5 days 3 days"),
250 Err(ParseError::UnitOutOfSequence(9))
251 );
252 }
253 #[test]
254 fn out_of_order_units() {
255 assert_eq!(
256 simple("5 days 3 weeks"),
257 Err(ParseError::UnitOutOfSequence(9))
258 );
259 }
260 #[test]
261 fn non_units() {
262 assert_eq!(simple("5 days 3 apples"), Err(ParseError::NoUnit(9)));
263 }
264 #[test]
265 fn missing_number() {
266 assert_eq!(simple("5 days weeks"), Err(ParseError::NoNumber(7)));
267 }
268 #[test]
269 fn years_without_date() {
270 assert_eq!(
271 simple("5 years 3 days"),
272 Err(ParseError::InconstantUnitWithoutDate)
273 );
274 }
275 #[test]
276 fn out_of_range() {
277 assert_eq!(
278 with_date("-1 year - 12 months", DateTime::<Utc>::MIN_UTC),
279 Err(ParseError::DateOutOfRange)
280 );
281 }
282 fn date_year_month_day(year: i32, month: u32, day: u32) -> DateTime<Utc> {
283 NaiveDate::from_ymd_opt(year, month, day)
284 .unwrap()
285 .and_time(NaiveTime::default())
286 .and_utc()
287 }
288 #[test]
289 fn leap_year_forward() {
290 assert_eq!(
291 with_date("1 month", date_year_month_day(2000, 2, 1)),
292 Ok(Duration::days(29))
293 );
294 }
295 #[test]
296 fn leap_year_backward() {
297 assert_eq!(
298 with_date("-1 month", date_year_month_day(2000, 2, 1)),
299 Ok(Duration::days(-31))
300 );
301 }
302 #[test]
303 fn year_equals_twelve_months_forwards() {
304 assert_eq!(
305 with_date("1 year -12 months", date_year_month_day(2000, 2, 1)),
306 Ok(Duration::default())
307 );
308 }
309 #[test]
310 fn year_equals_twelve_months_backwards() {
311 assert_eq!(
312 with_date("-1 year -12 months", date_year_month_day(2000, 2, 1)),
313 Ok(Duration::default())
314 );
315 }
316 #[test]
317 fn lazy_eager_same_outcome() {
318 let date = date_year_month_day(2000, 2, 1);
319 let interval = "1 year 3 months 15 minutes";
320 assert_eq!(
321 with_date(interval, date),
322 with_lazy_date(interval, move || date)
323 );
324 }
325 #[test]
326 fn doc_examples() {
327 let duration = self::with_now("2 days 15 hours 15 mins");
328 assert_eq!(duration, Ok(chrono::Duration::seconds(227700)));
329
330 let duration = self::with_lazy_date("1 month", || {
331 NaiveDate::from_ymd_opt(2000, 2, 1)
332 .unwrap()
333 .and_time(NaiveTime::default())
334 .and_utc()
335 });
336 assert_eq!(duration, Ok(chrono::Duration::days(29)));
337 }
338}