easy_schedule/
task.rs

1use async_trait::async_trait;
2use std::fmt::Debug;
3use time::{Date, OffsetDateTime, Time, UtcOffset, macros::format_description};
4use tokio_util::sync::CancellationToken;
5
6/// a task that can be scheduled
7#[async_trait]
8pub trait Notifiable: Sync + Send + Debug {
9    /// get the schedule type
10    fn get_task(&self) -> Task;
11
12    /// called when the task is scheduled
13    ///
14    /// Default cancel on first trigger
15    async fn on_time(&self, cancel: CancellationToken) {
16        cancel.cancel();
17    }
18
19    /// called when the task is skipped
20    async fn on_skip(&self, _cancel: CancellationToken) {
21        // do nothing
22    }
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum Skip {
27    /// skip fixed date
28    Date(Date),
29    /// skip date range
30    DateRange(Date, Date),
31    /// skip days
32    ///
33    /// 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday, 7: Sunday
34    Day(Vec<u8>),
35    /// skip days range
36    ///
37    /// 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday, 7: Sunday
38    DayRange(usize, usize),
39    /// skip fixed time
40    Time(Time),
41    /// skip time range
42    ///
43    /// end must be greater than start
44    TimeRange(Time, Time),
45    /// no skip
46    None,
47}
48
49impl Default for Skip {
50    fn default() -> Self {
51        Self::None
52    }
53}
54
55impl std::fmt::Display for Skip {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Skip::Date(date) => write!(f, "date: {date}"),
59            Skip::DateRange(start, end) => write!(f, "date range: {start} - {end}"),
60            Skip::Day(day) => write!(f, "day: {day:?}"),
61            Skip::DayRange(start, end) => write!(f, "day range: {start} - {end}"),
62            Skip::Time(time) => write!(f, "time: {time}"),
63            Skip::TimeRange(start, end) => write!(f, "time range: {start} - {end}"),
64            Skip::None => write!(f, "none"),
65        }
66    }
67}
68
69impl Skip {
70    /// check if the time is skipped
71    pub fn is_skip(&self, time: OffsetDateTime) -> bool {
72        match self {
73            Skip::Date(date) => time.date() == *date,
74            Skip::DateRange(start, end) => time.date() >= *start && time.date() <= *end,
75            Skip::Day(day) => day.contains(&(time.weekday().number_from_monday())),
76            Skip::DayRange(start, end) => {
77                let weekday = time.weekday().number_from_monday() as usize;
78                weekday >= *start && weekday <= *end
79            }
80            Skip::Time(skip_time) => time.time() == *skip_time,
81            Skip::TimeRange(start, end) => {
82                let current_time = time.time();
83                if start <= end {
84                    // 同一天内的时间范围
85                    current_time >= *start && current_time <= *end
86                } else {
87                    // 跨日期的时间范围 (如 22:00 - 06:00)
88                    current_time >= *start || current_time <= *end
89                }
90            }
91            Skip::None => false,
92        }
93    }
94}
95
96#[derive(Debug, Clone)]
97pub enum Task {
98    /// wait seconds
99    Wait(u64, Option<Vec<Skip>>),
100    /// interval seconds
101    Interval(u64, Option<Vec<Skip>>),
102    /// at time
103    At(Time, Option<Vec<Skip>>),
104    /// exact time
105    Once(OffsetDateTime, Option<Vec<Skip>>),
106}
107
108impl PartialEq for Task {
109    fn eq(&self, other: &Self) -> bool {
110        match (self, other) {
111            (Task::Wait(a, skip_a), Task::Wait(b, skip_b)) => a == b && skip_a == skip_b,
112            (Task::Interval(a, skip_a), Task::Interval(b, skip_b)) => a == b && skip_a == skip_b,
113            (Task::At(a, skip_a), Task::At(b, skip_b)) => a == b && skip_a == skip_b,
114            (Task::Once(a, skip_a), Task::Once(b, skip_b)) => a == b && skip_a == skip_b,
115            _ => false,
116        }
117    }
118}
119
120impl Task {
121    /// get the next run time for the scheduled task
122    pub fn get_next_run_time<T: Notifiable + 'static>(
123        &self,
124        timezone_minutes: i16,
125    ) -> Option<OffsetDateTime> {
126        let now = get_now(timezone_minutes).unwrap_or_else(|_| OffsetDateTime::now_utc());
127
128        match self.clone() {
129            Task::Wait(wait, skip) => {
130                let mut next_time = now + time::Duration::seconds(wait as i64);
131
132                if let Some(skip_rules) = skip {
133                    let mut attempts = 0;
134                    const MAX_ATTEMPTS: u32 = 1000;
135
136                    while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
137                    {
138                        next_time += time::Duration::seconds(wait as i64);
139                        attempts += 1;
140                    }
141
142                    if attempts >= MAX_ATTEMPTS {
143                        return None;
144                    }
145                }
146
147                Some(next_time)
148            }
149            Task::Interval(interval, skip) => {
150                let mut next_time = now + time::Duration::seconds(interval as i64);
151
152                if let Some(skip_rules) = skip {
153                    let mut attempts = 0;
154                    const MAX_ATTEMPTS: u32 = 1000;
155
156                    while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
157                    {
158                        next_time += time::Duration::seconds(interval as i64);
159                        attempts += 1;
160                    }
161
162                    if attempts >= MAX_ATTEMPTS {
163                        return None;
164                    }
165                }
166
167                Some(next_time)
168            }
169            Task::At(time, skip) => {
170                let mut next_time = get_next_time(now, time);
171
172                if let Some(skip_rules) = skip {
173                    let mut attempts = 0;
174                    const MAX_ATTEMPTS: u32 = 365;
175
176                    while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
177                    {
178                        next_time += time::Duration::days(1);
179                        attempts += 1;
180                    }
181
182                    if attempts >= MAX_ATTEMPTS {
183                        return None;
184                    }
185                }
186
187                Some(next_time)
188            }
189            Task::Once(once_time, skip) => {
190                if once_time <= now {
191                    return None;
192                }
193
194                if let Some(skip_rules) = skip {
195                    if skip_rules.iter().any(|s| s.is_skip(once_time)) {
196                        return None;
197                    }
198                }
199
200                Some(once_time)
201            }
202        }
203    }
204}
205
206impl Task {
207    /// Parse a task from a string with detailed error reporting.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use easy_schedule::Task;
213    ///
214    /// let task = Task::parse("wait(10)").unwrap();
215    ///
216    /// match Task::parse("invalid") {
217    ///     Ok(task) => println!("Success: {}", task),
218    ///     Err(err) => println!("Error: {}", err),
219    /// }
220    /// ```
221    pub fn parse(s: &str) -> Result<Self, String> {
222        let s = s.trim();
223
224        // Find the function name and arguments
225        let open_paren = s.find('(').ok_or_else(|| {
226            format!("Invalid task format: '{s}'. Expected format like 'wait(10)'")
227        })?;
228
229        let close_paren = s
230            .rfind(')')
231            .ok_or_else(|| format!("Missing closing parenthesis in: '{s}'"))?;
232
233        if close_paren <= open_paren {
234            return Err(format!("Invalid parentheses in: '{s}'"));
235        }
236
237        let function_name = s[..open_paren].trim();
238        let args = s[open_paren + 1..close_paren].trim();
239
240        // Parse arguments - check if there are skip conditions
241        let (primary_arg, skip_conditions) = Self::parse_arguments(args)?;
242
243        match function_name {
244            "wait" => {
245                let seconds = primary_arg.parse::<u64>().map_err(|_| {
246                    format!("Invalid seconds value '{primary_arg}' in wait({primary_arg})")
247                })?;
248                Ok(Task::Wait(seconds, skip_conditions))
249            }
250            "interval" => {
251                let seconds = primary_arg.parse::<u64>().map_err(|_| {
252                    format!("Invalid seconds value '{primary_arg}' in interval({primary_arg})")
253                })?;
254                Ok(Task::Interval(seconds, skip_conditions))
255            }
256            "at" => {
257                let format = format_description!("[hour]:[minute]");
258                let time = Time::parse(&primary_arg, &format).map_err(|_| {
259                    format!("Invalid time format '{primary_arg}' in at({primary_arg}). Expected format: HH:MM")
260                })?;
261                Ok(Task::At(time, skip_conditions))
262            }
263            "once" => {
264                let format = format_description!(
265                    "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]"
266                );
267                let datetime = OffsetDateTime::parse(&primary_arg, &format)
268                    .map_err(|_| format!("Invalid datetime format '{primary_arg}' in once({primary_arg}). Expected format: YYYY-MM-DD HH:MM:SS +HH"))?;
269                Ok(Task::Once(datetime, skip_conditions))
270            }
271            _ => Err(format!(
272                "Unknown task type '{function_name}'. Supported types: wait, interval, at, once"
273            )),
274        }
275    }
276
277    fn parse_arguments(args: &str) -> Result<(String, Option<Vec<Skip>>), String> {
278        let args = args.trim();
279
280        // Check if there's a comma, indicating skip conditions
281        if let Some(comma_pos) = args.find(',') {
282            let primary_arg = args[..comma_pos].trim().to_string();
283            let skip_part = args[comma_pos + 1..].trim();
284
285            let skip_conditions = Self::parse_skip_conditions(skip_part)?;
286            Ok((primary_arg, Some(skip_conditions)))
287        } else {
288            Ok((args.to_string(), None))
289        }
290    }
291
292    fn parse_skip_conditions(skip_str: &str) -> Result<Vec<Skip>, String> {
293        let skip_str = skip_str.trim();
294
295        // Check if it's a list format [...]
296        if skip_str.starts_with('[') && skip_str.ends_with(']') {
297            let list_content = &skip_str[1..skip_str.len() - 1];
298            Self::parse_skip_list(list_content)
299        } else {
300            // Single skip condition
301            let skip = Self::parse_single_skip(skip_str)?;
302            Ok(vec![skip])
303        }
304    }
305
306    fn parse_skip_list(list_str: &str) -> Result<Vec<Skip>, String> {
307        let mut skips = Vec::new();
308        let list_str = list_str.trim();
309
310        if list_str.is_empty() {
311            return Ok(skips);
312        }
313
314        // Split by comma and parse each skip condition
315        for part in list_str.split(',') {
316            let part = part.trim();
317            if !part.is_empty() {
318                let skip = Self::parse_single_skip(part)?;
319                skips.push(skip);
320            }
321        }
322
323        Ok(skips)
324    }
325
326    fn parse_single_skip(skip_str: &str) -> Result<Skip, String> {
327        let skip_str = skip_str.trim();
328        let parts: Vec<&str> = skip_str.split_whitespace().collect();
329
330        if parts.is_empty() {
331            return Err("Empty skip condition".to_string());
332        }
333
334        match parts[0] {
335            "weekday" => {
336                if parts.len() != 2 {
337                    return Err(format!(
338                        "Invalid weekday format: '{skip_str}'. Expected 'weekday N'"
339                    ));
340                }
341                let day = parts[1]
342                    .parse::<u8>()
343                    .map_err(|_| format!("Invalid weekday number: '{}'", parts[1]))?;
344                if !(1..=7).contains(&day) {
345                    return Err(format!("Weekday must be between 1-7, got: {day}"));
346                }
347                Ok(Skip::Day(vec![day]))
348            }
349            "date" => {
350                if parts.len() != 2 {
351                    return Err(format!(
352                        "Invalid date format: '{skip_str}'. Expected 'date YYYY-MM-DD'"
353                    ));
354                }
355                let date_str = parts[1];
356                let date_parts: Vec<&str> = date_str.split('-').collect();
357                if date_parts.len() != 3 {
358                    return Err(format!(
359                        "Invalid date format: '{date_str}'. Expected 'YYYY-MM-DD'"
360                    ));
361                }
362
363                let year = date_parts[0]
364                    .parse::<i32>()
365                    .map_err(|_| format!("Invalid year: '{}'", date_parts[0]))?;
366                let month = date_parts[1]
367                    .parse::<u8>()
368                    .map_err(|_| format!("Invalid month: '{}'", date_parts[1]))?;
369                let day = date_parts[2]
370                    .parse::<u8>()
371                    .map_err(|_| format!("Invalid day: '{}'", date_parts[2]))?;
372
373                let month_enum =
374                    time::Month::try_from(month).map_err(|_| format!("Invalid month: {month}"))?;
375                let date = time::Date::from_calendar_date(year, month_enum, day)
376                    .map_err(|_| format!("Invalid date: {year}-{month}-{day}"))?;
377
378                Ok(Skip::Date(date))
379            }
380            "time" => {
381                if parts.len() != 2 {
382                    return Err(format!(
383                        "Invalid time format: '{skip_str}'. Expected 'time HH:MM..HH:MM'"
384                    ));
385                }
386                let time_range = parts[1];
387                if let Some(range_pos) = time_range.find("..") {
388                    let start_str = &time_range[..range_pos];
389                    let end_str = &time_range[range_pos + 2..];
390
391                    let format = format_description!("[hour]:[minute]");
392                    let start_time = Time::parse(start_str, &format)
393                        .map_err(|_| format!("Invalid start time: '{start_str}'"))?;
394                    let end_time = Time::parse(end_str, &format)
395                        .map_err(|_| format!("Invalid end time: '{end_str}'"))?;
396
397                    Ok(Skip::TimeRange(start_time, end_time))
398                } else {
399                    // Single time
400                    let format = format_description!("[hour]:[minute]");
401                    let time = Time::parse(time_range, &format)
402                        .map_err(|_| format!("Invalid time: '{time_range}'"))?;
403                    Ok(Skip::Time(time))
404                }
405            }
406            _ => Err(format!(
407                "Unknown skip type: '{}'. Supported types: weekday, date, time",
408                parts[0]
409            )),
410        }
411    }
412}
413
414impl From<&str> for Task {
415    /// Parse a task from a string, panicking on parse errors.
416    ///
417    /// For better error handling, consider using `Task::parse()` instead.
418    ///
419    /// # Panics
420    ///
421    /// Panics if the string cannot be parsed as a valid task.
422    fn from(s: &str) -> Self {
423        Task::parse(s).unwrap_or_else(|err| {
424            panic!("Failed to parse task from string '{s}': {err}");
425        })
426    }
427}
428
429impl From<String> for Task {
430    fn from(s: String) -> Self {
431        Self::from(s.as_str())
432    }
433}
434
435impl From<&String> for Task {
436    fn from(s: &String) -> Self {
437        Self::from(s.as_str())
438    }
439}
440
441#[macro_export]
442macro_rules! task {
443    // 基础任务,无skip
444    (wait $seconds:tt) => {
445        $crate::Task::Wait($seconds, None)
446    };
447    (interval $seconds:tt) => {
448        $crate::Task::Interval($seconds, None)
449    };
450    (at $hour:tt : $minute:tt) => {
451        $crate::Task::At(
452            time::Time::from_hms($hour, $minute, 0).unwrap(),
453            None
454        )
455    };
456
457    // 带单个skip条件
458    (wait $seconds:tt, weekday $day:tt) => {
459        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
460    };
461    (wait $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
462        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Date(
463            time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
464        )]))
465    };
466    (wait $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
467        $crate::Task::Wait($seconds, Some(vec![$crate::Skip::TimeRange(
468            time::Time::from_hms($start_h, $start_m, 0).unwrap(),
469            time::Time::from_hms($end_h, $end_m, 0).unwrap()
470        )]))
471    };
472
473    (interval $seconds:tt, weekday $day:tt) => {
474        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
475    };
476    (interval $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
477        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Date(
478            time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
479        )]))
480    };
481    (interval $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
482        $crate::Task::Interval($seconds, Some(vec![$crate::Skip::TimeRange(
483            time::Time::from_hms($start_h, $start_m, 0).unwrap(),
484            time::Time::from_hms($end_h, $end_m, 0).unwrap()
485        )]))
486    };
487
488    (at $hour:tt : $minute:tt, weekday $day:tt) => {
489        $crate::Task::At(
490            time::Time::from_hms($hour, $minute, 0).unwrap(),
491            Some(vec![$crate::Skip::Day(vec![$day])])
492        )
493    };
494    (at $hour:tt : $minute:tt, date $year:tt - $month:tt - $day:tt) => {
495        $crate::Task::At(
496            time::Time::from_hms($hour, $minute, 0).unwrap(),
497            Some(vec![$crate::Skip::Date(
498                time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
499            )])
500        )
501    };
502    (at $hour:tt : $minute:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
503        $crate::Task::At(
504            time::Time::from_hms($hour, $minute, 0).unwrap(),
505            Some(vec![$crate::Skip::TimeRange(
506                time::Time::from_hms($start_h, $start_m, 0).unwrap(),
507                time::Time::from_hms($end_h, $end_m, 0).unwrap()
508            )])
509        )
510    };
511
512    // 带多个skip条件列表
513    (wait $seconds:tt, [$($skip:tt)*]) => {
514        $crate::Task::Wait($seconds, Some($crate::task!(@build_skips $($skip)*)))
515    };
516    (interval $seconds:tt, [$($skip:tt)*]) => {
517        $crate::Task::Interval($seconds, Some($crate::task!(@build_skips $($skip)*)))
518    };
519    (at $hour:tt : $minute:tt, [$($skip:tt)*]) => {
520        $crate::Task::At(
521            time::Time::from_hms($hour, $minute, 0).unwrap(),
522            Some($crate::task!(@build_skips $($skip)*))
523        )
524    };
525
526    // 辅助宏:构建skip列表
527    (@build_skips) => { vec![] };
528    (@build_skips weekday $day:tt $(, $($rest:tt)*)?) => {
529        {
530            let mut skips = vec![$crate::Skip::Day(vec![$day])];
531            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
532            skips
533        }
534    };
535    (@build_skips date $year:tt - $month:tt - $day:tt $(, $($rest:tt)*)?) => {
536        {
537            let mut skips = vec![$crate::Skip::Date(
538                time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
539            )];
540            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
541            skips
542        }
543    };
544    (@build_skips time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt $(, $($rest:tt)*)?) => {
545        {
546            let mut skips = vec![$crate::Skip::TimeRange(
547                time::Time::from_hms($start_h, $start_m, 0).unwrap(),
548                time::Time::from_hms($end_h, $end_m, 0).unwrap()
549            )];
550            $(skips.extend($crate::task!(@build_skips $($rest)*));)?
551            skips
552        }
553    };
554}
555
556impl std::fmt::Display for Task {
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        match self {
559            Task::Wait(wait, skip) => {
560                let skip = skip
561                    .clone()
562                    .unwrap_or_default()
563                    .into_iter()
564                    .map(|s| s.to_string())
565                    .collect::<Vec<String>>()
566                    .join(", ");
567                write!(f, "wait: {wait} {skip}")
568            }
569            Task::Interval(interval, skip) => {
570                let skip = skip
571                    .clone()
572                    .unwrap_or_default()
573                    .into_iter()
574                    .map(|s| s.to_string())
575                    .collect::<Vec<String>>()
576                    .join(", ");
577                write!(f, "interval: {interval} {skip}")
578            }
579            Task::At(time, skip) => {
580                let skip = skip
581                    .clone()
582                    .unwrap_or_default()
583                    .into_iter()
584                    .map(|s| s.to_string())
585                    .collect::<Vec<String>>()
586                    .join(", ");
587                write!(f, "at: {time} {skip}")
588            }
589            Task::Once(time, skip) => {
590                let skip = skip
591                    .clone()
592                    .unwrap_or_default()
593                    .into_iter()
594                    .map(|s| s.to_string())
595                    .collect::<Vec<String>>()
596                    .join(", ");
597                write!(f, "once: {time} {skip}")
598            }
599        }
600    }
601}
602
603pub fn get_next_time(now: OffsetDateTime, time: Time) -> OffsetDateTime {
604    let mut next = now.replace_time(time);
605    if next < now {
606        next += time::Duration::days(1);
607    }
608    next
609}
610
611pub fn get_now(timezone_minutes: i16) -> Result<OffsetDateTime, time::error::ComponentRange> {
612    let hours = timezone_minutes / 60;
613    let minutes = timezone_minutes % 60;
614    let offset = UtcOffset::from_hms(hours as i8, minutes as i8, 0)?;
615    Ok(OffsetDateTime::now_utc().to_offset(offset))
616}