]> git.proxmox.com Git - proxmox.git/blame - proxmox-time/src/parse_time.rs
proxmox-time: calendar-events: implement repeated ranges
[proxmox.git] / proxmox-time / src / parse_time.rs
CommitLineData
4a5dbd21
DM
1use std::collections::HashMap;
2
3use anyhow::{bail, Error};
4use lazy_static::lazy_static;
5
6use super::time::*;
7use super::daily_duration::*;
8
9use 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
18type IResult<I, O, E = VerboseError<I>> = Result<(I, O), nom::Err<E>>;
19
20fn 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
27fn 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).
32fn 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
51lazy_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
120struct TimeSpec {
121 hour: Vec<DateTimeValue>,
122 minute: Vec<DateTimeValue>,
123 second: Vec<DateTimeValue>,
124}
125
126struct DateSpec {
127 year: Vec<DateTimeValue>,
128 month: Vec<DateTimeValue>,
129 day: Vec<DateTimeValue>,
130}
131
132fn 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
142fn 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
157fn 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
186fn 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
210fn 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
226fn 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
241fn 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]
261pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
262 parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
263}
264
265fn 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
395fn 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]
406pub fn parse_time_span(i: &str) -> Result<TimeSpan, Error> {
407 parse_complete_line("time span", i, parse_time_span_incomplete)
408}
409
410fn 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]
464pub fn parse_daily_duration(i: &str) -> Result<DailyDuration, Error> {
465 parse_complete_line("daily duration", i, parse_daily_duration_incomplete)
466}
467
468fn 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
506fn 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}