easy_schedule/
task.rs

1use std::fmt::{self, Debug};
2use time::{Date, OffsetDateTime, Time, macros::format_description};
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum Skip {
6    /// skip fixed date
7    Date(Date),
8    /// skip date range
9    DateRange(Date, Date),
10    /// skip days
11    ///
12    /// 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday, 7: Sunday
13    Day(Vec<u8>),
14    /// skip days range
15    ///
16    /// 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday, 7: Sunday
17    DayRange(usize, usize),
18    /// skip fixed time
19    Time(Time),
20    /// skip time range
21    ///
22    /// end must be greater than start
23    TimeRange(Time, Time),
24    /// no skip
25    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    /// check if the time is skipped
50    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                    // 同一天内的时间范围
64                    current_time >= *start && current_time <= *end
65                } else {
66                    // 跨日期的时间范围 (如 22:00 - 06:00)
67                    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 seconds
78    Wait(u64, Option<Vec<Skip>>),
79    /// interval seconds
80    Interval(u64, Option<Vec<Skip>>),
81    /// at time
82    At(Time, Option<Vec<Skip>>),
83    /// exact time
84    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    /// Parse a task from a string with detailed error reporting.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use easy_schedule::Task;
106    ///
107    /// let task = Task::parse("wait(10)").unwrap();
108    ///
109    /// match Task::parse("invalid") {
110    ///     Ok(task) => println!("Success: {}", task),
111    ///     Err(err) => println!("Error: {}", err),
112    /// }
113    /// ```
114    pub fn parse(s: &str) -> Result<Self, String> {
115        let s = s.trim();
116
117        // Find the function name and arguments
118        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        // Parse arguments - check if there are skip conditions
134        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        // Check if there's a comma, indicating skip conditions
174        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        // Check if it's a list format [...]
189        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            // Single skip condition
194            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        // Split by comma and parse each skip condition
208        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                    // Single time
293                    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    /// Parse a task from a string, panicking on parse errors.
309    ///
310    /// For better error handling, consider using `Task::parse()` instead.
311    ///
312    /// # Panics
313    ///
314    /// Panics if the string cannot be parsed as a valid task.
315    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    // 基础任务,无skip
337    (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    // 带单个skip条件
351    (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    // 带多个skip条件列表
406    (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    // 辅助宏:构建skip列表
420    (@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}