1use std::fmt::{self, Debug};
2use time::{Date, OffsetDateTime, Time, macros::format_description};
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum Skip {
6 Date(Date),
8 DateRange(Date, Date),
10 Day(Vec<u8>),
14 DayRange(usize, usize),
18 Time(Time),
20 TimeRange(Time, Time),
24 None,
26}
27
28impl Default for Skip {
29 fn default() -> Self {
30 Self::None
31 }
32}
33
34impl fmt::Display for Skip {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Skip::Date(date) => write!(f, "date: {date}"),
38 Skip::DateRange(start, end) => write!(f, "date range: {start} - {end}"),
39 Skip::Day(day) => write!(f, "day: {day:?}"),
40 Skip::DayRange(start, end) => write!(f, "day range: {start} - {end}"),
41 Skip::Time(time) => write!(f, "time: {time}"),
42 Skip::TimeRange(start, end) => write!(f, "time range: {start} - {end}"),
43 Skip::None => write!(f, "none"),
44 }
45 }
46}
47
48impl Skip {
49 pub fn is_skip(&self, time: OffsetDateTime) -> bool {
51 match self {
52 Skip::Date(date) => time.date() == *date,
53 Skip::DateRange(start, end) => time.date() >= *start && time.date() <= *end,
54 Skip::Day(day) => day.contains(&(time.weekday().number_from_monday())),
55 Skip::DayRange(start, end) => {
56 let weekday = time.weekday().number_from_monday() as usize;
57 weekday >= *start && weekday <= *end
58 }
59 Skip::Time(skip_time) => time.time() == *skip_time,
60 Skip::TimeRange(start, end) => {
61 let current_time = time.time();
62 if start <= end {
63 current_time >= *start && current_time <= *end
65 } else {
66 current_time >= *start || current_time <= *end
68 }
69 }
70 Skip::None => false,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
76pub enum Task {
77 Wait(u64, Option<Vec<Skip>>),
79 Interval(u64, Option<Vec<Skip>>),
81 At(Time, Option<Vec<Skip>>),
83 Once(OffsetDateTime, Option<Vec<Skip>>),
85}
86
87impl PartialEq for Task {
88 fn eq(&self, other: &Self) -> bool {
89 match (self, other) {
90 (Task::Wait(a, skip_a), Task::Wait(b, skip_b)) => a == b && skip_a == skip_b,
91 (Task::Interval(a, skip_a), Task::Interval(b, skip_b)) => a == b && skip_a == skip_b,
92 (Task::At(a, skip_a), Task::At(b, skip_b)) => a == b && skip_a == skip_b,
93 (Task::Once(a, skip_a), Task::Once(b, skip_b)) => a == b && skip_a == skip_b,
94 _ => false,
95 }
96 }
97}
98
99impl Task {
100 pub fn parse(s: &str) -> Result<Self, String> {
115 let s = s.trim();
116
117 let open_paren = s.find('(').ok_or_else(|| {
119 format!("Invalid task format: '{s}'. Expected format like 'wait(10)'")
120 })?;
121
122 let close_paren = s
123 .rfind(')')
124 .ok_or_else(|| format!("Missing closing parenthesis in: '{s}'"))?;
125
126 if close_paren <= open_paren {
127 return Err(format!("Invalid parentheses in: '{s}'"));
128 }
129
130 let function_name = s[..open_paren].trim();
131 let args = s[open_paren + 1..close_paren].trim();
132
133 let (primary_arg, skip_conditions) = Self::parse_arguments(args)?;
135
136 match function_name {
137 "wait" => {
138 let seconds = primary_arg.parse::<u64>().map_err(|_| {
139 format!("Invalid seconds value '{primary_arg}' in wait({primary_arg})")
140 })?;
141 Ok(Task::Wait(seconds, skip_conditions))
142 }
143 "interval" => {
144 let seconds = primary_arg.parse::<u64>().map_err(|_| {
145 format!("Invalid seconds value '{primary_arg}' in interval({primary_arg})")
146 })?;
147 Ok(Task::Interval(seconds, skip_conditions))
148 }
149 "at" => {
150 let format = format_description!("[hour]:[minute]");
151 let time = Time::parse(&primary_arg, &format).map_err(|_| {
152 format!("Invalid time format '{primary_arg}' in at({primary_arg}). Expected format: HH:MM")
153 })?;
154 Ok(Task::At(time, skip_conditions))
155 }
156 "once" => {
157 let format = format_description!(
158 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]"
159 );
160 let datetime = OffsetDateTime::parse(&primary_arg, &format)
161 .map_err(|_| format!("Invalid datetime format '{primary_arg}' in once({primary_arg}). Expected format: YYYY-MM-DD HH:MM:SS +HH"))?;
162 Ok(Task::Once(datetime, skip_conditions))
163 }
164 _ => Err(format!(
165 "Unknown task type '{function_name}'. Supported types: wait, interval, at, once"
166 )),
167 }
168 }
169
170 fn parse_arguments(args: &str) -> Result<(String, Option<Vec<Skip>>), String> {
171 let args = args.trim();
172
173 if let Some(comma_pos) = args.find(',') {
175 let primary_arg = args[..comma_pos].trim().to_string();
176 let skip_part = args[comma_pos + 1..].trim();
177
178 let skip_conditions = Self::parse_skip_conditions(skip_part)?;
179 Ok((primary_arg, Some(skip_conditions)))
180 } else {
181 Ok((args.to_string(), None))
182 }
183 }
184
185 fn parse_skip_conditions(skip_str: &str) -> Result<Vec<Skip>, String> {
186 let skip_str = skip_str.trim();
187
188 if skip_str.starts_with('[') && skip_str.ends_with(']') {
190 let list_content = &skip_str[1..skip_str.len() - 1];
191 Self::parse_skip_list(list_content)
192 } else {
193 let skip = Self::parse_single_skip(skip_str)?;
195 Ok(vec![skip])
196 }
197 }
198
199 fn parse_skip_list(list_str: &str) -> Result<Vec<Skip>, String> {
200 let mut skips = Vec::new();
201 let list_str = list_str.trim();
202
203 if list_str.is_empty() {
204 return Ok(skips);
205 }
206
207 for part in list_str.split(',') {
209 let part = part.trim();
210 if !part.is_empty() {
211 let skip = Self::parse_single_skip(part)?;
212 skips.push(skip);
213 }
214 }
215
216 Ok(skips)
217 }
218
219 fn parse_single_skip(skip_str: &str) -> Result<Skip, String> {
220 let skip_str = skip_str.trim();
221 let parts: Vec<&str> = skip_str.split_whitespace().collect();
222
223 if parts.is_empty() {
224 return Err("Empty skip condition".to_string());
225 }
226
227 match parts[0] {
228 "weekday" => {
229 if parts.len() != 2 {
230 return Err(format!(
231 "Invalid weekday format: '{skip_str}'. Expected 'weekday N'"
232 ));
233 }
234 let day = parts[1]
235 .parse::<u8>()
236 .map_err(|_| format!("Invalid weekday number: '{}'", parts[1]))?;
237 if !(1..=7).contains(&day) {
238 return Err(format!("Weekday must be between 1-7, got: {day}"));
239 }
240 Ok(Skip::Day(vec![day]))
241 }
242 "date" => {
243 if parts.len() != 2 {
244 return Err(format!(
245 "Invalid date format: '{skip_str}'. Expected 'date YYYY-MM-DD'"
246 ));
247 }
248 let date_str = parts[1];
249 let date_parts: Vec<&str> = date_str.split('-').collect();
250 if date_parts.len() != 3 {
251 return Err(format!(
252 "Invalid date format: '{date_str}'. Expected 'YYYY-MM-DD'"
253 ));
254 }
255
256 let year = date_parts[0]
257 .parse::<i32>()
258 .map_err(|_| format!("Invalid year: '{}'", date_parts[0]))?;
259 let month = date_parts[1]
260 .parse::<u8>()
261 .map_err(|_| format!("Invalid month: '{}'", date_parts[1]))?;
262 let day = date_parts[2]
263 .parse::<u8>()
264 .map_err(|_| format!("Invalid day: '{}'", date_parts[2]))?;
265
266 let month_enum =
267 time::Month::try_from(month).map_err(|_| format!("Invalid month: {month}"))?;
268 let date = time::Date::from_calendar_date(year, month_enum, day)
269 .map_err(|_| format!("Invalid date: {year}-{month}-{day}"))?;
270
271 Ok(Skip::Date(date))
272 }
273 "time" => {
274 if parts.len() != 2 {
275 return Err(format!(
276 "Invalid time format: '{skip_str}'. Expected 'time HH:MM..HH:MM'"
277 ));
278 }
279 let time_range = parts[1];
280 if let Some(range_pos) = time_range.find("..") {
281 let start_str = &time_range[..range_pos];
282 let end_str = &time_range[range_pos + 2..];
283
284 let format = format_description!("[hour]:[minute]");
285 let start_time = Time::parse(start_str, &format)
286 .map_err(|_| format!("Invalid start time: '{start_str}'"))?;
287 let end_time = Time::parse(end_str, &format)
288 .map_err(|_| format!("Invalid end time: '{end_str}'"))?;
289
290 Ok(Skip::TimeRange(start_time, end_time))
291 } else {
292 let format = format_description!("[hour]:[minute]");
294 let time = Time::parse(time_range, &format)
295 .map_err(|_| format!("Invalid time: '{time_range}'"))?;
296 Ok(Skip::Time(time))
297 }
298 }
299 _ => Err(format!(
300 "Unknown skip type: '{}'. Supported types: weekday, date, time",
301 parts[0]
302 )),
303 }
304 }
305}
306
307impl From<&str> for Task {
308 fn from(s: &str) -> Self {
316 Task::parse(s).unwrap_or_else(|err| {
317 panic!("Failed to parse task from string '{s}': {err}");
318 })
319 }
320}
321
322impl From<String> for Task {
323 fn from(s: String) -> Self {
324 Self::from(s.as_str())
325 }
326}
327
328impl From<&String> for Task {
329 fn from(s: &String) -> Self {
330 Self::from(s.as_str())
331 }
332}
333
334#[macro_export]
335macro_rules! task {
336 (wait $seconds:tt) => {
338 $crate::Task::Wait($seconds, None)
339 };
340 (interval $seconds:tt) => {
341 $crate::Task::Interval($seconds, None)
342 };
343 (at $hour:tt : $minute:tt) => {
344 $crate::Task::At(
345 time::Time::from_hms($hour, $minute, 0).unwrap(),
346 None
347 )
348 };
349
350 (wait $seconds:tt, weekday $day:tt) => {
352 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
353 };
354 (wait $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
355 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Date(
356 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
357 )]))
358 };
359 (wait $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
360 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::TimeRange(
361 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
362 time::Time::from_hms($end_h, $end_m, 0).unwrap()
363 )]))
364 };
365
366 (interval $seconds:tt, weekday $day:tt) => {
367 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
368 };
369 (interval $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
370 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Date(
371 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
372 )]))
373 };
374 (interval $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
375 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::TimeRange(
376 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
377 time::Time::from_hms($end_h, $end_m, 0).unwrap()
378 )]))
379 };
380
381 (at $hour:tt : $minute:tt, weekday $day:tt) => {
382 $crate::Task::At(
383 time::Time::from_hms($hour, $minute, 0).unwrap(),
384 Some(vec![$crate::Skip::Day(vec![$day])])
385 )
386 };
387 (at $hour:tt : $minute:tt, date $year:tt - $month:tt - $day:tt) => {
388 $crate::Task::At(
389 time::Time::from_hms($hour, $minute, 0).unwrap(),
390 Some(vec![$crate::Skip::Date(
391 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
392 )])
393 )
394 };
395 (at $hour:tt : $minute:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
396 $crate::Task::At(
397 time::Time::from_hms($hour, $minute, 0).unwrap(),
398 Some(vec![$crate::Skip::TimeRange(
399 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
400 time::Time::from_hms($end_h, $end_m, 0).unwrap()
401 )])
402 )
403 };
404
405 (wait $seconds:tt, [$($skip:tt)*]) => {
407 $crate::Task::Wait($seconds, Some($crate::task!(@build_skips $($skip)*)))
408 };
409 (interval $seconds:tt, [$($skip:tt)*]) => {
410 $crate::Task::Interval($seconds, Some($crate::task!(@build_skips $($skip)*)))
411 };
412 (at $hour:tt : $minute:tt, [$($skip:tt)*]) => {
413 $crate::Task::At(
414 time::Time::from_hms($hour, $minute, 0).unwrap(),
415 Some($crate::task!(@build_skips $($skip)*))
416 )
417 };
418
419 (@build_skips) => { vec![] };
421 (@build_skips weekday $day:tt $(, $($rest:tt)*)?) => {
422 {
423 let mut skips = vec![$crate::Skip::Day(vec![$day])];
424 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
425 skips
426 }
427 };
428 (@build_skips date $year:tt - $month:tt - $day:tt $(, $($rest:tt)*)?) => {
429 {
430 let mut skips = vec![$crate::Skip::Date(
431 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
432 )];
433 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
434 skips
435 }
436 };
437 (@build_skips time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt $(, $($rest:tt)*)?) => {
438 {
439 let mut skips = vec![$crate::Skip::TimeRange(
440 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
441 time::Time::from_hms($end_h, $end_m, 0).unwrap()
442 )];
443 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
444 skips
445 }
446 };
447}
448
449impl fmt::Display for Task {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 match self {
452 Task::Wait(wait, skip) => {
453 let skip = skip
454 .clone()
455 .unwrap_or_default()
456 .into_iter()
457 .map(|s| s.to_string())
458 .collect::<Vec<String>>()
459 .join(", ");
460 write!(f, "wait: {wait} {skip}")
461 }
462 Task::Interval(interval, skip) => {
463 let skip = skip
464 .clone()
465 .unwrap_or_default()
466 .into_iter()
467 .map(|s| s.to_string())
468 .collect::<Vec<String>>()
469 .join(", ");
470 write!(f, "interval: {interval} {skip}")
471 }
472 Task::At(time, skip) => {
473 let skip = skip
474 .clone()
475 .unwrap_or_default()
476 .into_iter()
477 .map(|s| s.to_string())
478 .collect::<Vec<String>>()
479 .join(", ");
480 write!(f, "at: {time} {skip}")
481 }
482 Task::Once(time, skip) => {
483 let skip = skip
484 .clone()
485 .unwrap_or_default()
486 .into_iter()
487 .map(|s| s.to_string())
488 .collect::<Vec<String>>()
489 .join(", ");
490 write!(f, "once: {time} {skip}")
491 }
492 }
493 }
494}