]>
Commit | Line | Data |
---|---|---|
4a5dbd21 DM |
1 | use std::collections::HashMap; |
2 | ||
3 | use anyhow::{bail, Error}; | |
4 | use lazy_static::lazy_static; | |
5 | ||
6 | use super::time::*; | |
7 | use super::daily_duration::*; | |
8 | ||
9 | use nom::{ | |
10 | error::{context, ParseError, VerboseError}, | |
11 | bytes::complete::{tag, take_while1}, | |
12 | combinator::{map_res, all_consuming, opt, recognize}, | |
13 | sequence::{pair, preceded, tuple}, | |
14 | character::complete::{alpha1, space0, digit1}, | |
15 | multi::separated_nonempty_list, | |
16 | }; | |
17 | ||
18 | type IResult<I, O, E = VerboseError<I>> = Result<(I, O), nom::Err<E>>; | |
19 | ||
20 | fn parse_error<'a>(i: &'a str, context: &'static str) -> nom::Err<VerboseError<&'a str>> { | |
21 | let err = VerboseError { errors: Vec::new() }; | |
22 | let err = VerboseError::add_context(i, context, err); | |
23 | nom::Err::Error(err) | |
24 | } | |
25 | ||
26 | // Parse a 64 bit unsigned integer | |
27 | fn parse_u64(i: &str) -> IResult<&str, u64> { | |
28 | map_res(recognize(digit1), str::parse)(i) | |
29 | } | |
30 | ||
31 | // Parse complete input, generate simple error message (use this for sinple line input). | |
32 | fn parse_complete_line<'a, F, O>(what: &str, i: &'a str, parser: F) -> Result<O, Error> | |
33 | where F: Fn(&'a str) -> IResult<&'a str, O>, | |
34 | { | |
35 | match all_consuming(parser)(i) { | |
36 | Err(nom::Err::Error(VerboseError { errors })) | | |
37 | Err(nom::Err::Failure(VerboseError { errors })) => { | |
38 | if errors.is_empty() { | |
39 | bail!("unable to parse {}", what); | |
40 | } else { | |
41 | bail!("unable to parse {} at '{}' - {:?}", what, errors[0].0, errors[0].1); | |
42 | } | |
43 | } | |
44 | Err(err) => { | |
45 | bail!("unable to parse {} - {}", what, err); | |
46 | } | |
47 | Ok((_, data)) => Ok(data), | |
48 | } | |
49 | } | |
50 | ||
51 | lazy_static! { | |
52 | static ref TIME_SPAN_UNITS: HashMap<&'static str, f64> = { | |
53 | let mut map = HashMap::new(); | |
54 | ||
55 | let second = 1.0; | |
56 | ||
57 | map.insert("seconds", second); | |
58 | map.insert("second", second); | |
59 | map.insert("sec", second); | |
60 | map.insert("s", second); | |
61 | ||
62 | let msec = second / 1000.0; | |
63 | ||
64 | map.insert("msec", msec); | |
65 | map.insert("ms", msec); | |
66 | ||
67 | let usec = msec / 1000.0; | |
68 | ||
69 | map.insert("usec", usec); | |
70 | map.insert("us", usec); | |
71 | map.insert("µs", usec); | |
72 | ||
73 | let nsec = usec / 1000.0; | |
74 | ||
75 | map.insert("nsec", nsec); | |
76 | map.insert("ns", nsec); | |
77 | ||
78 | let minute = second * 60.0; | |
79 | ||
80 | map.insert("minutes", minute); | |
81 | map.insert("minute", minute); | |
82 | map.insert("min", minute); | |
83 | map.insert("m", minute); | |
84 | ||
85 | let hour = minute * 60.0; | |
86 | ||
87 | map.insert("hours", hour); | |
88 | map.insert("hour", hour); | |
89 | map.insert("hr", hour); | |
90 | map.insert("h", hour); | |
91 | ||
92 | let day = hour * 24.0 ; | |
93 | ||
94 | map.insert("days", day); | |
95 | map.insert("day", day); | |
96 | map.insert("d", day); | |
97 | ||
98 | let week = day * 7.0; | |
99 | ||
100 | map.insert("weeks", week); | |
101 | map.insert("week", week); | |
102 | map.insert("w", week); | |
103 | ||
104 | let month = 30.44 * day; | |
105 | ||
106 | map.insert("months", month); | |
107 | map.insert("month", month); | |
108 | map.insert("M", month); | |
109 | ||
110 | let year = 365.25 * day; | |
111 | ||
112 | map.insert("years", year); | |
113 | map.insert("year", year); | |
114 | map.insert("y", year); | |
115 | ||
116 | map | |
117 | }; | |
118 | } | |
119 | ||
120 | struct TimeSpec { | |
121 | hour: Vec<DateTimeValue>, | |
122 | minute: Vec<DateTimeValue>, | |
123 | second: Vec<DateTimeValue>, | |
124 | } | |
125 | ||
126 | struct DateSpec { | |
127 | year: Vec<DateTimeValue>, | |
128 | month: Vec<DateTimeValue>, | |
129 | day: Vec<DateTimeValue>, | |
130 | } | |
131 | ||
132 | fn parse_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, u32> { | |
133 | move |i: &str| { | |
134 | let (i, v) = map_res(recognize(digit1), str::parse)(i)?; | |
135 | if (v as usize) >= max { | |
136 | return Err(parse_error(i, "time value too large")); | |
137 | } | |
138 | Ok((i, v)) | |
139 | } | |
140 | } | |
141 | ||
142 | fn parse_weekday(i: &str) -> IResult<&str, WeekDays> { | |
143 | let (i, text) = alpha1(i)?; | |
144 | ||
145 | match text.to_ascii_lowercase().as_str() { | |
146 | "monday" | "mon" => Ok((i, WeekDays::MONDAY)), | |
147 | "tuesday" | "tue" => Ok((i, WeekDays::TUESDAY)), | |
148 | "wednesday" | "wed" => Ok((i, WeekDays::WEDNESDAY)), | |
149 | "thursday" | "thu" => Ok((i, WeekDays::THURSDAY)), | |
150 | "friday" | "fri" => Ok((i, WeekDays::FRIDAY)), | |
151 | "saturday" | "sat" => Ok((i, WeekDays::SATURDAY)), | |
152 | "sunday" | "sun" => Ok((i, WeekDays::SUNDAY)), | |
153 | _ => return Err(parse_error(text, "weekday")), | |
154 | } | |
155 | } | |
156 | ||
157 | fn parse_weekdays_range(i: &str) -> IResult<&str, WeekDays> { | |
158 | let (i, startday) = parse_weekday(i)?; | |
159 | ||
160 | let generate_range = |start, end| { | |
161 | let mut res = 0; | |
162 | let mut pos = start; | |
163 | loop { | |
164 | res |= pos; | |
165 | if pos >= end { break; } | |
166 | pos <<= 1; | |
167 | } | |
168 | WeekDays::from_bits(res).unwrap() | |
169 | }; | |
170 | ||
171 | if let (i, Some((_, endday))) = opt(pair(tag(".."),parse_weekday))(i)? { | |
172 | let start = startday.bits(); | |
173 | let end = endday.bits(); | |
174 | if start > end { | |
175 | let set1 = generate_range(start, WeekDays::SUNDAY.bits()); | |
176 | let set2 = generate_range(WeekDays::MONDAY.bits(), end); | |
177 | Ok((i, set1 | set2)) | |
178 | } else { | |
179 | Ok((i, generate_range(start, end))) | |
180 | } | |
181 | } else { | |
182 | Ok((i, startday)) | |
183 | } | |
184 | } | |
185 | ||
186 | fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeValue> { | |
187 | move |i: &str| { | |
188 | let (i, value) = parse_time_comp(max)(i)?; | |
189 | ||
190 | if let (i, Some(end)) = opt(preceded(tag(".."), parse_time_comp(max)))(i)? { | |
191 | if value > end { | |
192 | return Err(parse_error(i, "range start is bigger than end")); | |
193 | } | |
8480b7b4 DC |
194 | if let Some(time) = i.strip_prefix('/') { |
195 | let (time, repeat) = parse_time_comp(max)(time)?; | |
196 | return Ok((time, DateTimeValue::Repeated(value, repeat, Some(end)))); | |
197 | } | |
198 | return Ok((i, DateTimeValue::Range(value, end))); | |
4a5dbd21 DM |
199 | } |
200 | ||
201 | if let Some(time) = i.strip_prefix('/') { | |
202 | let (time, repeat) = parse_time_comp(max)(time)?; | |
8480b7b4 | 203 | Ok((time, DateTimeValue::Repeated(value, repeat, None))) |
4a5dbd21 DM |
204 | } else { |
205 | Ok((i, DateTimeValue::Single(value))) | |
206 | } | |
207 | } | |
208 | } | |
209 | ||
210 | fn parse_date_time_comp_list(start: u32, max: usize) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> { | |
211 | move |i: &str| { | |
212 | if let Some(rest) = i.strip_prefix('*') { | |
213 | if let Some(time) = rest.strip_prefix('/') { | |
214 | let (n, repeat) = parse_time_comp(max)(time)?; | |
215 | if repeat > 0 { | |
8480b7b4 | 216 | return Ok((n, vec![DateTimeValue::Repeated(start, repeat, None)])); |
4a5dbd21 DM |
217 | } |
218 | } | |
219 | return Ok((rest, Vec::new())); | |
220 | } | |
221 | ||
222 | separated_nonempty_list(tag(","), parse_date_time_comp(max))(i) | |
223 | } | |
224 | } | |
225 | ||
226 | fn parse_time_spec(i: &str) -> IResult<&str, TimeSpec> { | |
227 | ||
228 | let (i, (hour, minute, opt_second)) = tuple(( | |
229 | parse_date_time_comp_list(0, 24), | |
230 | preceded(tag(":"), parse_date_time_comp_list(0, 60)), | |
231 | opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))), | |
232 | ))(i)?; | |
233 | ||
234 | if let Some(second) = opt_second { | |
235 | Ok((i, TimeSpec { hour, minute, second })) | |
236 | } else { | |
237 | Ok((i, TimeSpec { hour, minute, second: vec![DateTimeValue::Single(0)] })) | |
238 | } | |
239 | } | |
240 | ||
241 | fn parse_date_spec(i: &str) -> IResult<&str, DateSpec> { | |
242 | ||
243 | // TODO: implement ~ for days (man systemd.time) | |
244 | if let Ok((i, (year, month, day))) = tuple(( | |
245 | parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible | |
246 | preceded(tag("-"), parse_date_time_comp_list(1, 13)), | |
247 | preceded(tag("-"), parse_date_time_comp_list(1, 32)), | |
248 | ))(i) { | |
249 | Ok((i, DateSpec { year, month, day })) | |
250 | } else if let Ok((i, (month, day))) = tuple(( | |
251 | parse_date_time_comp_list(1, 13), | |
252 | preceded(tag("-"), parse_date_time_comp_list(1, 32)), | |
253 | ))(i) { | |
254 | Ok((i, DateSpec { year: Vec::new(), month, day })) | |
255 | } else { | |
256 | Err(parse_error(i, "invalid date spec")) | |
257 | } | |
258 | } | |
259 | ||
260 | /// Parse a [CalendarEvent] | |
261 | pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> { | |
262 | parse_complete_line("calendar event", i, parse_calendar_event_incomplete) | |
263 | } | |
264 | ||
265 | fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> { | |
266 | ||
267 | let mut has_dayspec = false; | |
268 | let mut has_timespec = false; | |
269 | let mut has_datespec = false; | |
270 | ||
271 | let mut event = CalendarEvent::default(); | |
272 | ||
273 | if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) { | |
274 | ||
275 | match i { | |
276 | "minutely" => { | |
277 | return Ok(("", CalendarEvent { | |
278 | second: vec![DateTimeValue::Single(0)], | |
279 | ..Default::default() | |
280 | })); | |
281 | } | |
282 | "hourly" => { | |
283 | return Ok(("", CalendarEvent { | |
284 | minute: vec![DateTimeValue::Single(0)], | |
285 | second: vec![DateTimeValue::Single(0)], | |
286 | ..Default::default() | |
287 | })); | |
288 | } | |
289 | "daily" => { | |
290 | return Ok(("", CalendarEvent { | |
291 | hour: vec![DateTimeValue::Single(0)], | |
292 | minute: vec![DateTimeValue::Single(0)], | |
293 | second: vec![DateTimeValue::Single(0)], | |
294 | ..Default::default() | |
295 | })); | |
296 | } | |
297 | "weekly" => { | |
298 | return Ok(("", CalendarEvent { | |
299 | hour: vec![DateTimeValue::Single(0)], | |
300 | minute: vec![DateTimeValue::Single(0)], | |
301 | second: vec![DateTimeValue::Single(0)], | |
302 | days: WeekDays::MONDAY, | |
303 | ..Default::default() | |
304 | })); | |
305 | } | |
306 | "monthly" => { | |
307 | return Ok(("", CalendarEvent { | |
308 | hour: vec![DateTimeValue::Single(0)], | |
309 | minute: vec![DateTimeValue::Single(0)], | |
310 | second: vec![DateTimeValue::Single(0)], | |
311 | day: vec![DateTimeValue::Single(1)], | |
312 | ..Default::default() | |
313 | })); | |
314 | } | |
315 | "yearly" | "annually" => { | |
316 | return Ok(("", CalendarEvent { | |
317 | hour: vec![DateTimeValue::Single(0)], | |
318 | minute: vec![DateTimeValue::Single(0)], | |
319 | second: vec![DateTimeValue::Single(0)], | |
320 | day: vec![DateTimeValue::Single(1)], | |
321 | month: vec![DateTimeValue::Single(1)], | |
322 | ..Default::default() | |
323 | })); | |
324 | } | |
325 | "quarterly" => { | |
326 | return Ok(("", CalendarEvent { | |
327 | hour: vec![DateTimeValue::Single(0)], | |
328 | minute: vec![DateTimeValue::Single(0)], | |
329 | second: vec![DateTimeValue::Single(0)], | |
330 | day: vec![DateTimeValue::Single(1)], | |
331 | month: vec![ | |
332 | DateTimeValue::Single(1), | |
333 | DateTimeValue::Single(4), | |
334 | DateTimeValue::Single(7), | |
335 | DateTimeValue::Single(10), | |
336 | ], | |
337 | ..Default::default() | |
338 | })); | |
339 | } | |
340 | "semiannually" | "semi-annually" => { | |
341 | return Ok(("", CalendarEvent { | |
342 | hour: vec![DateTimeValue::Single(0)], | |
343 | minute: vec![DateTimeValue::Single(0)], | |
344 | second: vec![DateTimeValue::Single(0)], | |
345 | day: vec![DateTimeValue::Single(1)], | |
346 | month: vec![ | |
347 | DateTimeValue::Single(1), | |
348 | DateTimeValue::Single(7), | |
349 | ], | |
350 | ..Default::default() | |
351 | })); | |
352 | } | |
353 | _ => { /* continue */ } | |
354 | } | |
355 | ||
356 | let (n, range_list) = context( | |
357 | "weekday range list", | |
358 | separated_nonempty_list(tag(","), parse_weekdays_range) | |
359 | )(i)?; | |
360 | ||
361 | has_dayspec = true; | |
362 | ||
363 | i = space0(n)?.0; | |
364 | ||
365 | for range in range_list { event.days.insert(range); } | |
366 | } | |
367 | ||
368 | if let (n, Some(date)) = opt(parse_date_spec)(i)? { | |
369 | event.year = date.year; | |
370 | event.month = date.month; | |
371 | event.day = date.day; | |
372 | has_datespec = true; | |
373 | i = space0(n)?.0; | |
374 | } | |
375 | ||
376 | if let (n, Some(time)) = opt(parse_time_spec)(i)? { | |
377 | event.hour = time.hour; | |
378 | event.minute = time.minute; | |
379 | event.second = time.second; | |
380 | has_timespec = true; | |
381 | i = n; | |
382 | } else { | |
383 | event.hour = vec![DateTimeValue::Single(0)]; | |
384 | event.minute = vec![DateTimeValue::Single(0)]; | |
385 | event.second = vec![DateTimeValue::Single(0)]; | |
386 | } | |
387 | ||
388 | if !(has_dayspec || has_timespec || has_datespec) { | |
389 | return Err(parse_error(i, "date or time specification")); | |
390 | } | |
391 | ||
392 | Ok((i, event)) | |
393 | } | |
394 | ||
395 | fn parse_time_unit(i: &str) -> IResult<&str, &str> { | |
396 | let (n, text) = take_while1(|c: char| char::is_ascii_alphabetic(&c) || c == 'µ')(i)?; | |
397 | if TIME_SPAN_UNITS.contains_key(&text) { | |
398 | Ok((n, text)) | |
399 | } else { | |
400 | Err(parse_error(text, "time unit")) | |
401 | } | |
402 | } | |
403 | ||
404 | ||
405 | /// Parse a [TimeSpan] | |
406 | pub fn parse_time_span(i: &str) -> Result<TimeSpan, Error> { | |
407 | parse_complete_line("time span", i, parse_time_span_incomplete) | |
408 | } | |
409 | ||
410 | fn parse_time_span_incomplete(mut i: &str) -> IResult<&str, TimeSpan> { | |
411 | ||
412 | let mut ts = TimeSpan::default(); | |
413 | ||
414 | loop { | |
415 | i = space0(i)?.0; | |
416 | if i.is_empty() { break; } | |
417 | let (n, num) = parse_u64(i)?; | |
418 | i = space0(n)?.0; | |
419 | ||
420 | if let (n, Some(unit)) = opt(parse_time_unit)(i)? { | |
421 | i = n; | |
422 | match unit { | |
423 | "seconds" | "second" | "sec" | "s" => { | |
424 | ts.seconds += num; | |
425 | } | |
426 | "msec" | "ms" => { | |
427 | ts.msec += num; | |
428 | } | |
429 | "usec" | "us" | "µs" => { | |
430 | ts.usec += num; | |
431 | } | |
432 | "nsec" | "ns" => { | |
433 | ts.nsec += num; | |
434 | } | |
435 | "minutes" | "minute" | "min" | "m" => { | |
436 | ts.minutes += num; | |
437 | } | |
438 | "hours" | "hour" | "hr" | "h" => { | |
439 | ts.hours += num; | |
440 | } | |
441 | "days" | "day" | "d" => { | |
442 | ts.days += num; | |
443 | } | |
444 | "weeks" | "week" | "w" => { | |
445 | ts.weeks += num; | |
446 | } | |
447 | "months" | "month" | "M" => { | |
448 | ts.months += num; | |
449 | } | |
450 | "years" | "year" | "y" => { | |
451 | ts.years += num; | |
452 | } | |
453 | _ => return Err(parse_error(unit, "internal error")), | |
454 | } | |
455 | } else { | |
456 | ts.seconds += num; | |
457 | } | |
458 | } | |
459 | ||
460 | Ok((i, ts)) | |
461 | } | |
462 | ||
463 | /// Parse a [DailyDuration] | |
464 | pub fn parse_daily_duration(i: &str) -> Result<DailyDuration, Error> { | |
465 | parse_complete_line("daily duration", i, parse_daily_duration_incomplete) | |
466 | } | |
467 | ||
468 | fn parse_daily_duration_incomplete(mut i: &str) -> IResult<&str, DailyDuration> { | |
469 | ||
470 | let mut duration = DailyDuration::default(); | |
471 | ||
472 | if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) { | |
473 | ||
474 | let (n, range_list) = context( | |
475 | "weekday range list", | |
476 | separated_nonempty_list(tag(","), parse_weekdays_range) | |
477 | )(i)?; | |
478 | ||
479 | i = space0(n)?.0; | |
480 | ||
481 | for range in range_list { duration.days.insert(range); } | |
482 | } | |
483 | ||
484 | let (i, start) = parse_hm_time(i)?; | |
485 | ||
486 | let i = space0(i)?.0; | |
487 | ||
488 | let (i, _) = tag("-")(i)?; | |
489 | ||
490 | let i = space0(i)?.0; | |
491 | ||
492 | let end_time_start = i; | |
493 | ||
494 | let (i, end) = parse_hm_time(i)?; | |
495 | ||
496 | if start > end { | |
497 | return Err(parse_error(end_time_start, "end time before start time")); | |
498 | } | |
499 | ||
500 | duration.start = start; | |
501 | duration.end = end; | |
502 | ||
503 | Ok((i, duration)) | |
504 | } | |
505 | ||
506 | fn parse_hm_time(i: &str) -> IResult<&str, HmTime> { | |
507 | ||
508 | let (i, (hour, opt_minute)) = tuple(( | |
509 | parse_time_comp(24), | |
510 | opt(preceded(tag(":"), parse_time_comp(60))), | |
511 | ))(i)?; | |
512 | ||
513 | match opt_minute { | |
514 | Some(minute) => Ok((i, HmTime { hour, minute })), | |
515 | None => Ok((i, HmTime { hour, minute: 0})), | |
516 | } | |
517 | } |