Files
notify/backend_rust/src/timer/recurrence.rs
Michael Dong a98e12f286 first commit
2026-02-05 11:24:40 +08:00

208 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use chrono::{DateTime, Datelike, Duration, FixedOffset, NaiveDateTime, TimeZone, Weekday};
use crate::entity::recurrence_rule;
use crate::entity::sea_orm_active_enums::RecurrenceType;
/// 根据循环规则计算下一次触发时间
pub fn calculate_next_due(
rule: &recurrence_rule::Model,
current_due: DateTime<FixedOffset>,
) -> Option<DateTime<FixedOffset>> {
let interval = rule.interval.max(1) as i64;
let offset = current_due.offset().clone();
match rule.r#type {
RecurrenceType::Hourly => {
// 每 N 小时
Some(current_due + Duration::hours(interval))
}
RecurrenceType::Daily => {
// 每 N 天
Some(current_due + Duration::days(interval))
}
RecurrenceType::Weekly => {
// 每 N 周,可选指定星期几
if let Some(weekday) = rule.by_weekday {
// 找到下一个指定的星期几
let target_weekday = num_to_weekday(weekday);
let next = current_due + Duration::weeks(interval);
// 调整到目标星期几
let current_weekday = next.weekday();
let days_ahead = (target_weekday.num_days_from_monday() as i64
- current_weekday.num_days_from_monday() as i64
+ 7)
% 7;
if days_ahead == 0 {
// 同一天,保持当前时间
Some(next)
} else {
Some(next + Duration::days(days_ahead))
}
} else {
Some(current_due + Duration::weeks(interval))
}
}
RecurrenceType::Monthly => {
// 每 N 月,可选指定几号
let target_day = rule.by_monthday.unwrap_or(current_due.day() as i32) as u32;
let mut year = current_due.year();
let mut month = current_due.month() as i32 + interval as i32;
// 处理年份进位
while month > 12 {
month -= 12;
year += 1;
}
// 处理月份天数不足的情况(如 2 月没有 31 号)
let day = target_day.min(days_in_month(year, month as u32));
let naive = NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(year, month as u32, day)?,
current_due.time(),
);
Some(offset.from_local_datetime(&naive).single()?)
}
RecurrenceType::Yearly => {
// 每 N 年
let year = current_due.year() + interval as i32;
let month = current_due.month();
let day = current_due.day().min(days_in_month(year, month));
let naive = NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(year, month, day)?,
current_due.time(),
);
Some(offset.from_local_datetime(&naive).single()?)
}
}
}
fn num_to_weekday(num: i32) -> Weekday {
match num % 7 {
0 => Weekday::Sun,
1 => Weekday::Mon,
2 => Weekday::Tue,
3 => Weekday::Wed,
4 => Weekday::Thu,
5 => Weekday::Fri,
6 => Weekday::Sat,
_ => Weekday::Mon,
}
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 30,
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, Timelike};
use uuid::Uuid;
fn make_rule(
rule_type: RecurrenceType,
interval: i32,
by_weekday: Option<i32>,
by_monthday: Option<i32>,
) -> recurrence_rule::Model {
recurrence_rule::Model {
id: Uuid::new_v4(),
r#type: rule_type,
interval,
by_weekday,
by_monthday,
timezone: "Asia/Shanghai".to_string(),
created_at: chrono::Utc::now().fixed_offset(),
updated_at: chrono::Utc::now().fixed_offset(),
}
}
fn make_datetime(year: i32, month: u32, day: u32, hour: u32, min: u32) -> DateTime<FixedOffset> {
let naive = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_opt(hour, min, 0)
.unwrap();
FixedOffset::east_opt(0).unwrap().from_local_datetime(&naive).unwrap()
}
#[test]
fn test_hourly() {
let rule = make_rule(RecurrenceType::Hourly, 2, None, None);
let current = make_datetime(2024, 1, 15, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.hour(), 12);
assert_eq!(next.minute(), 30);
}
#[test]
fn test_daily() {
let rule = make_rule(RecurrenceType::Daily, 3, None, None);
let current = make_datetime(2024, 1, 15, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.day(), 18);
}
#[test]
fn test_weekly() {
let rule = make_rule(RecurrenceType::Weekly, 1, None, None);
let current = make_datetime(2024, 1, 15, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.day(), 22);
}
#[test]
fn test_monthly() {
let rule = make_rule(RecurrenceType::Monthly, 1, None, Some(15));
let current = make_datetime(2024, 1, 15, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.month(), 2);
assert_eq!(next.day(), 15);
}
#[test]
fn test_monthly_overflow() {
// 1月31号 -> 2月没有31号应该是28号或29号
let rule = make_rule(RecurrenceType::Monthly, 1, None, Some(31));
let current = make_datetime(2024, 1, 31, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.month(), 2);
assert_eq!(next.day(), 29); // 2024 是闰年
}
#[test]
fn test_yearly() {
let rule = make_rule(RecurrenceType::Yearly, 1, None, None);
let current = make_datetime(2024, 6, 15, 10, 30);
let next = calculate_next_due(&rule, current).unwrap();
assert_eq!(next.year(), 2025);
assert_eq!(next.month(), 6);
assert_eq!(next.day(), 15);
}
}