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        match function_name {
134            "wait" => {
135                let seconds = args
136                    .parse::<u64>()
137                    .map_err(|_| format!("Invalid seconds value '{args}' in wait({args})"))?;
138                Ok(Task::Wait(seconds, None))
139            }
140            "interval" => {
141                let seconds = args
142                    .parse::<u64>()
143                    .map_err(|_| format!("Invalid seconds value '{args}' in interval({args})"))?;
144                Ok(Task::Interval(seconds, None))
145            }
146            "at" => {
147                let format = format_description!("[hour]:[minute]");
148                let time = Time::parse(args, &format).map_err(|_| {
149                    format!("Invalid time format '{args}' in at({args}). Expected format: HH:MM")
150                })?;
151                Ok(Task::At(time, None))
152            }
153            "once" => {
154                let format = format_description!(
155                    "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]"
156                );
157                let datetime = OffsetDateTime::parse(args, &format)
158                    .map_err(|_| format!("Invalid datetime format '{args}' in once({args}). Expected format: YYYY-MM-DD HH:MM:SS +HH"))?;
159                Ok(Task::Once(datetime, None))
160            }
161            _ => Err(format!(
162                "Unknown task type '{function_name}'. Supported types: wait, interval, at, once"
163            )),
164        }
165    }
166}
167
168impl From<&str> for Task {
169    /// Parse a task from a string, panicking on parse errors.
170    ///
171    /// For better error handling, consider using `Task::parse()` instead.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the string cannot be parsed as a valid task.
176    fn from(s: &str) -> Self {
177        Task::parse(s).unwrap_or_else(|err| {
178            panic!("Failed to parse task from string '{s}': {err}");
179        })
180    }
181}
182
183impl From<String> for Task {
184    fn from(s: String) -> Self {
185        Self::from(s.as_str())
186    }
187}
188
189impl From<&String> for Task {
190    fn from(s: &String) -> Self {
191        Self::from(s.as_str())
192    }
193}
194
195#[macro_export]
196macro_rules! task {
197    // 基础任务,无skip
198    (wait $seconds:tt) => {
199        $crate::Task::Wait($seconds, None)
200    };
201    (interval $seconds:tt) => {
202        $crate::Task::Interval($seconds, None)
203    };
204    (at $hour:tt : $minute:tt) => {
205        $crate::Task::At(
206            time::Time::from_hms($hour, $minute, 0).unwrap(),
207            None
208        )
209    };
210
211    // 带单个skip条件
212    (wait $seconds:tt, weekday $day:tt) => {
213        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
214    };
215    (wait $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
216        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Date(
217            time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
218        )]))
219    };
220    (wait $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
221        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::TimeRange(
222            time::Time::from_hms($start_h, $start_m, 0).unwrap(),
223            time::Time::from_hms($end_h, $end_m, 0).unwrap()
224        )]))
225    };
226
227    (interval $seconds:tt, weekday $day:tt) => {
228        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
229    };
230    (interval $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
231        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Date(
232            time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
233        )]))
234    };
235    (interval $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
236        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::TimeRange(
237            time::Time::from_hms($start_h, $start_m, 0).unwrap(),
238            time::Time::from_hms($end_h, $end_m, 0).unwrap()
239        )]))
240    };
241
242    (at $hour:tt : $minute:tt, weekday $day:tt) => {
243        $crate::Task::At(
244            time::Time::from_hms($hour, $minute, 0).unwrap(),
245            Some(vec![$crate::Skip::Day(vec![$day])])
246        )
247    };
248    (at $hour:tt : $minute:tt, date $year:tt - $month:tt - $day:tt) => {
249        $crate::Task::At(
250            time::Time::from_hms($hour, $minute, 0).unwrap(),
251            Some(vec![$crate::Skip::Date(
252                time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
253            )])
254        )
255    };
256    (at $hour:tt : $minute:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
257        $crate::Task::At(
258            time::Time::from_hms($hour, $minute, 0).unwrap(),
259            Some(vec![$crate::Skip::TimeRange(
260                time::Time::from_hms($start_h, $start_m, 0).unwrap(),
261                time::Time::from_hms($end_h, $end_m, 0).unwrap()
262            )])
263        )
264    };
265
266    // 带多个skip条件列表
267    (wait $seconds:tt, [$($skip:tt)*]) => {
268        $crate::Task::Wait($seconds, Some($crate::task!(@build_skips $($skip)*)))
269    };
270    (interval $seconds:tt, [$($skip:tt)*]) => {
271        $crate::Task::Interval($seconds, Some($crate::task!(@build_skips $($skip)*)))
272    };
273    (at $hour:tt : $minute:tt, [$($skip:tt)*]) => {
274        $crate::Task::At(
275            time::Time::from_hms($hour, $minute, 0).unwrap(),
276            Some($crate::task!(@build_skips $($skip)*))
277        )
278    };
279
280    // 辅助宏:构建skip列表
281    (@build_skips) => { vec![] };
282    (@build_skips weekday $day:tt $(, $($rest:tt)*)?) => {
283        {
284            let mut skips = vec![$crate::Skip::Day(vec![$day])];
285            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
286            skips
287        }
288    };
289    (@build_skips date $year:tt - $month:tt - $day:tt $(, $($rest:tt)*)?) => {
290        {
291            let mut skips = vec![$crate::Skip::Date(
292                time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
293            )];
294            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
295            skips
296        }
297    };
298    (@build_skips time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt $(, $($rest:tt)*)?) => {
299        {
300            let mut skips = vec![$crate::Skip::TimeRange(
301                time::Time::from_hms($start_h, $start_m, 0).unwrap(),
302                time::Time::from_hms($end_h, $end_m, 0).unwrap()
303            )];
304            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
305            skips
306        }
307    };
308}
309
310impl fmt::Display for Task {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        match self {
313            Task::Wait(wait, skip) => {
314                let skip = skip
315                    .clone()
316                    .unwrap_or_default()
317                    .into_iter()
318                    .map(|s| s.to_string())
319                    .collect::<Vec<String>>()
320                    .join(", ");
321                write!(f, "wait: {wait} {skip}")
322            }
323            Task::Interval(interval, skip) => {
324                let skip = skip
325                    .clone()
326                    .unwrap_or_default()
327                    .into_iter()
328                    .map(|s| s.to_string())
329                    .collect::<Vec<String>>()
330                    .join(", ");
331                write!(f, "interval: {interval} {skip}")
332            }
333            Task::At(time, skip) => {
334                let skip = skip
335                    .clone()
336                    .unwrap_or_default()
337                    .into_iter()
338                    .map(|s| s.to_string())
339                    .collect::<Vec<String>>()
340                    .join(", ");
341                write!(f, "at: {time} {skip}")
342            }
343            Task::Once(time, skip) => {
344                let skip = skip
345                    .clone()
346                    .unwrap_or_default()
347                    .into_iter()
348                    .map(|s| s.to_string())
349                    .collect::<Vec<String>>()
350                    .join(", ");
351                write!(f, "once: {time} {skip}")
352            }
353        }
354    }
355}