robinpath_modules/modules/
cron_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 rp.register_builtin("cron.isValid", |args, _| {
6 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7 Ok(Value::Bool(parse_cron(&expr).is_some()))
8 });
9
10 rp.register_builtin("cron.parse", |args, _| {
12 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
13 match parse_cron(&expr) {
14 Some(fields) => {
15 let mut obj = indexmap::IndexMap::new();
16 obj.insert("minute".to_string(), vec_to_value(&fields.minutes));
17 obj.insert("hour".to_string(), vec_to_value(&fields.hours));
18 obj.insert("dayOfMonth".to_string(), vec_to_value(&fields.days));
19 obj.insert("month".to_string(), vec_to_value(&fields.months));
20 obj.insert("dayOfWeek".to_string(), vec_to_value(&fields.weekdays));
21 Ok(Value::Object(obj))
22 }
23 None => Err(format!("Invalid cron expression: {}", expr)),
24 }
25 });
26
27 rp.register_builtin("cron.next", |args, _| {
29 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
30 let from = args.get(1).map(|v| v.to_display_string());
31 let fields = parse_cron(&expr).ok_or_else(|| format!("Invalid cron: {}", expr))?;
32 let start = parse_datetime(&from);
33 match next_occurrence(&fields, start) {
34 Some(dt) => Ok(Value::String(format_datetime(dt))),
35 None => Ok(Value::Null),
36 }
37 });
38
39 rp.register_builtin("cron.nextN", |args, _| {
41 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
42 let count = args.get(1).map(|v| v.to_number() as usize).unwrap_or(5);
43 let from = args.get(2).map(|v| v.to_display_string());
44 let fields = parse_cron(&expr).ok_or_else(|| format!("Invalid cron: {}", expr))?;
45 let mut current = parse_datetime(&from);
46 let mut results = Vec::new();
47 for _ in 0..count {
48 match next_occurrence(&fields, current) {
49 Some(dt) => {
50 results.push(Value::String(format_datetime(dt)));
51 current = dt + 60; }
53 None => break,
54 }
55 }
56 Ok(Value::Array(results))
57 });
58
59 rp.register_builtin("cron.describe", |args, _| {
61 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
62 let fields = parse_cron(&expr).ok_or_else(|| format!("Invalid cron: {}", expr))?;
63 Ok(Value::String(describe_cron(&fields)))
64 });
65
66 rp.register_builtin("cron.matches", |args, _| {
68 let expr = args.first().map(|v| v.to_display_string()).unwrap_or_default();
69 let date = args.get(1).map(|v| v.to_display_string());
70 let fields = parse_cron(&expr).ok_or_else(|| format!("Invalid cron: {}", expr))?;
71 let ts = parse_datetime(&date);
72 let (_, mon, day, hour, min, wday) = timestamp_to_parts(ts);
73 let matches = fields.minutes.contains(&min)
74 && fields.hours.contains(&hour)
75 && fields.days.contains(&day)
76 && fields.months.contains(&mon)
77 && fields.weekdays.contains(&wday);
78 Ok(Value::Bool(matches))
79 });
80}
81
82struct CronFields {
83 minutes: Vec<u32>,
84 hours: Vec<u32>,
85 days: Vec<u32>,
86 months: Vec<u32>,
87 weekdays: Vec<u32>,
88}
89
90fn parse_cron(expr: &str) -> Option<CronFields> {
91 let parts: Vec<&str> = expr.trim().split_whitespace().collect();
92 if parts.len() != 5 { return None; }
93 Some(CronFields {
94 minutes: parse_field(parts[0], 0, 59)?,
95 hours: parse_field(parts[1], 0, 23)?,
96 days: parse_field(parts[2], 1, 31)?,
97 months: parse_field(parts[3], 1, 12)?,
98 weekdays: parse_field(parts[4], 0, 6)?,
99 })
100}
101
102fn parse_field(field: &str, min: u32, max: u32) -> Option<Vec<u32>> {
103 let mut values = Vec::new();
104 for part in field.split(',') {
105 let part = part.trim();
106 if part == "*" {
107 return Some((min..=max).collect());
108 }
109 if let Some(slash) = part.find('/') {
110 let range_part = &part[..slash];
111 let step: u32 = part[slash + 1..].parse().ok()?;
112 let start = if range_part == "*" { min } else { range_part.parse().ok()? };
113 let mut v = start;
114 while v <= max {
115 values.push(v);
116 v += step;
117 }
118 } else if let Some(dash) = part.find('-') {
119 let start: u32 = part[..dash].parse().ok()?;
120 let end: u32 = part[dash + 1..].parse().ok()?;
121 for v in start..=end {
122 values.push(v);
123 }
124 } else {
125 values.push(part.parse().ok()?);
126 }
127 }
128 values.sort();
129 values.dedup();
130 Some(values)
131}
132
133fn vec_to_value(v: &[u32]) -> Value {
134 Value::Array(v.iter().map(|&n| Value::Number(n as f64)).collect())
135}
136
137fn parse_datetime(s: &Option<String>) -> i64 {
139 if let Some(date_str) = s {
140 let trimmed = date_str.trim();
142 if trimmed.len() >= 10 {
143 let parts: Vec<&str> = trimmed[..10].split('-').collect();
144 if parts.len() == 3 {
145 if let (Ok(y), Ok(m), Ok(d)) = (parts[0].parse::<i64>(), parts[1].parse::<u32>(), parts[2].parse::<u32>()) {
146 let (hour, min) = if trimmed.len() >= 16 {
147 let time_part = &trimmed[11..];
148 let time_parts: Vec<&str> = time_part.split(':').collect();
149 (time_parts.first().and_then(|s| s.parse().ok()).unwrap_or(0u32),
150 time_parts.get(1).and_then(|s| s[..2.min(s.len())].parse().ok()).unwrap_or(0u32))
151 } else {
152 (0, 0)
153 };
154 return date_to_timestamp(y, m, d, hour, min);
155 }
156 }
157 }
158 }
159 std::time::SystemTime::now()
160 .duration_since(std::time::UNIX_EPOCH)
161 .unwrap_or_default()
162 .as_secs() as i64
163}
164
165fn date_to_timestamp(year: i64, month: u32, day: u32, hour: u32, min: u32) -> i64 {
166 let mut y = year;
168 let mut m = month as i64;
169 if m <= 2 { y -= 1; m += 12; }
170 let days = 365 * y + y / 4 - y / 100 + y / 400 + (153 * (m - 3) + 2) / 5 + day as i64 - 719469;
171 days * 86400 + hour as i64 * 3600 + min as i64 * 60
172}
173
174fn timestamp_to_parts(ts: i64) -> (i64, u32, u32, u32, u32, u32) {
175 let days = ts.div_euclid(86400);
176 let time_of_day = ts.rem_euclid(86400);
177 let hour = (time_of_day / 3600) as u32;
178 let min = ((time_of_day % 3600) / 60) as u32;
179 let wday = ((days + 4).rem_euclid(7)) as u32;
181 let z = days + 719468;
183 let era = z.div_euclid(146097);
184 let doe = z.rem_euclid(146097);
185 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
186 let y = yoe + era * 400;
187 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
188 let mp = (5 * doy + 2) / 153;
189 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
190 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
191 let y = if m <= 2 { y + 1 } else { y };
192 (y, m, d, hour, min, wday)
193}
194
195fn format_datetime(ts: i64) -> String {
196 let (y, m, d, h, min, _) = timestamp_to_parts(ts);
197 format!("{:04}-{:02}-{:02}T{:02}:{:02}:00Z", y, m, d, h, min)
198}
199
200fn next_occurrence(fields: &CronFields, after: i64) -> Option<i64> {
201 let mut ts = after + 60; ts -= ts % 60; let max_ts = ts + 366 * 86400; while ts < max_ts {
205 let (_, mon, day, hour, min, wday) = timestamp_to_parts(ts);
206 if fields.minutes.contains(&min)
207 && fields.hours.contains(&hour)
208 && fields.days.contains(&day)
209 && fields.months.contains(&mon)
210 && fields.weekdays.contains(&wday)
211 {
212 return Some(ts);
213 }
214 ts += 60;
215 }
216 None
217}
218
219fn describe_cron(fields: &CronFields) -> String {
220 let min_str = if fields.minutes.len() == 60 { "every minute".to_string() }
221 else { format!("at minute {}", join_nums(&fields.minutes)) };
222 let hour_str = if fields.hours.len() == 24 { "every hour".to_string() }
223 else { format!("at hour {}", join_nums(&fields.hours)) };
224 let day_str = if fields.days.len() == 31 { "every day".to_string() }
225 else { format!("on day {}", join_nums(&fields.days)) };
226 let mon_str = if fields.months.len() == 12 { "every month".to_string() }
227 else { format!("in month {}", join_nums(&fields.months)) };
228 format!("{}, {}, {}, {}", min_str, hour_str, day_str, mon_str)
229}
230
231fn join_nums(v: &[u32]) -> String {
232 v.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(",")
233}