Skip to main content

robinpath_modules/modules/
date_mod.rs

1use robinpath::{RobinPath, Value};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4pub fn register(rp: &mut RobinPath) {
5    rp.register_builtin("date.parse", |args, _| {
6        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7        match parse_date(&s) {
8            Some(ts) => Ok(Value::String(timestamp_to_iso(ts))),
9            None => Err(format!("date.parse: invalid date '{}'", s)),
10        }
11    });
12
13    rp.register_builtin("date.format", |args, _| {
14        let date_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
15        let pattern = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
16        match parse_date(&date_str) {
17            Some(ts) => Ok(Value::String(format_date(ts, &pattern))),
18            None => Err(format!("date.format: invalid date '{}'", date_str)),
19        }
20    });
21
22    rp.register_builtin("date.add", |args, _| {
23        let date_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
24        let amount = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0);
25        let unit = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
26        match parse_date(&date_str) {
27            Some(ts) => {
28                let new_ts = add_duration(ts, amount, &unit);
29                Ok(Value::String(timestamp_to_iso(new_ts)))
30            }
31            None => Err(format!("date.add: invalid date '{}'", date_str)),
32        }
33    });
34
35    rp.register_builtin("date.subtract", |args, _| {
36        let date_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
37        let amount = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0);
38        let unit = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
39        match parse_date(&date_str) {
40            Some(ts) => {
41                let new_ts = add_duration(ts, -amount, &unit);
42                Ok(Value::String(timestamp_to_iso(new_ts)))
43            }
44            None => Err(format!("date.subtract: invalid date '{}'", date_str)),
45        }
46    });
47
48    rp.register_builtin("date.diff", |args, _| {
49        let d1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50        let d2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
51        let unit = args.get(2).map(|v| v.to_display_string()).unwrap_or_else(|| "days".to_string());
52        match (parse_date(&d1), parse_date(&d2)) {
53            (Some(ts1), Some(ts2)) => {
54                let diff_secs = ts1 - ts2;
55                let result = match unit.as_str() {
56                    "seconds" | "second" => diff_secs,
57                    "minutes" | "minute" => diff_secs / 60,
58                    "hours" | "hour" => diff_secs / 3600,
59                    "days" | "day" => diff_secs / 86400,
60                    "weeks" | "week" => diff_secs / 604800,
61                    _ => diff_secs / 86400,
62                };
63                Ok(Value::Number(result as f64))
64            }
65            _ => Err("date.diff: invalid dates".to_string()),
66        }
67    });
68
69    rp.register_builtin("date.isAfter", |args, _| {
70        let d1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
71        let d2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
72        match (parse_date(&d1), parse_date(&d2)) {
73            (Some(ts1), Some(ts2)) => Ok(Value::Bool(ts1 > ts2)),
74            _ => Ok(Value::Bool(false)),
75        }
76    });
77
78    rp.register_builtin("date.isBefore", |args, _| {
79        let d1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
80        let d2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
81        match (parse_date(&d1), parse_date(&d2)) {
82            (Some(ts1), Some(ts2)) => Ok(Value::Bool(ts1 < ts2)),
83            _ => Ok(Value::Bool(false)),
84        }
85    });
86
87    rp.register_builtin("date.isBetween", |args, _| {
88        let d = args.first().map(|v| v.to_display_string()).unwrap_or_default();
89        let start = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
90        let end = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
91        match (parse_date(&d), parse_date(&start), parse_date(&end)) {
92            (Some(ts), Some(s), Some(e)) => Ok(Value::Bool(ts > s && ts < e)),
93            _ => Ok(Value::Bool(false)),
94        }
95    });
96
97    rp.register_builtin("date.toISO", |args, _| {
98        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
99        match parse_date(&s) {
100            Some(ts) => Ok(Value::String(timestamp_to_iso(ts))),
101            None => Err(format!("date.toISO: invalid date '{}'", s)),
102        }
103    });
104
105    rp.register_builtin("date.toUnix", |args, _| {
106        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
107        match parse_date(&s) {
108            Some(ts) => Ok(Value::Number(ts as f64)),
109            None => Err(format!("date.toUnix: invalid date '{}'", s)),
110        }
111    });
112
113    rp.register_builtin("date.fromUnix", |args, _| {
114        let ts = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
115        Ok(Value::String(timestamp_to_iso(ts)))
116    });
117
118    rp.register_builtin("date.now", |_args, _| {
119        let ts = SystemTime::now()
120            .duration_since(UNIX_EPOCH)
121            .unwrap_or_default()
122            .as_secs() as i64;
123        Ok(Value::String(timestamp_to_iso(ts)))
124    });
125
126    rp.register_builtin("date.nowUnix", |_args, _| {
127        let ts = SystemTime::now()
128            .duration_since(UNIX_EPOCH)
129            .unwrap_or_default()
130            .as_secs();
131        Ok(Value::Number(ts as f64))
132    });
133
134    rp.register_builtin("date.dayOfWeek", |args, _| {
135        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
136        match parse_date(&s) {
137            Some(ts) => {
138                // Days since epoch (Jan 1 1970 = Thursday = 4)
139                let days = ts / 86400;
140                let dow = ((days % 7 + 4) % 7) as f64;
141                Ok(Value::Number(dow))
142            }
143            None => Ok(Value::Null),
144        }
145    });
146
147    rp.register_builtin("date.daysInMonth", |args, _| {
148        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
149        match parse_date(&s) {
150            Some(ts) => {
151                let (year, month, _) = timestamp_to_ymd(ts);
152                Ok(Value::Number(days_in_month(year, month) as f64))
153            }
154            None => Ok(Value::Null),
155        }
156    });
157}
158
159// Simple date parser: handles ISO 8601 (YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS)
160fn parse_date(s: &str) -> Option<i64> {
161    let s = s.trim();
162    if s.is_empty() {
163        return None;
164    }
165
166    // Try "now"
167    if s == "now" {
168        return Some(
169            SystemTime::now()
170                .duration_since(UNIX_EPOCH)
171                .unwrap_or_default()
172                .as_secs() as i64,
173        );
174    }
175
176    // Try as unix timestamp
177    if let Ok(ts) = s.parse::<i64>() {
178        if ts > 946684800 {
179            // After year 2000
180            return Some(ts);
181        }
182    }
183
184    // Try ISO: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS
185    let parts: Vec<&str> = s.splitn(2, 'T').collect();
186    let date_part = parts[0];
187    let time_part = parts.get(1).unwrap_or(&"00:00:00");
188
189    let date_nums: Vec<i32> = date_part.split('-').filter_map(|p| p.parse().ok()).collect();
190    if date_nums.len() < 3 {
191        return None;
192    }
193    let (year, month, day) = (date_nums[0], date_nums[1], date_nums[2]);
194
195    let time_clean = time_part.trim_end_matches('Z');
196    let time_nums: Vec<i32> = time_clean
197        .split(':')
198        .filter_map(|p| p.split('.').next().and_then(|s| s.parse().ok()))
199        .collect();
200    let hour = *time_nums.first().unwrap_or(&0);
201    let minute = *time_nums.get(1).unwrap_or(&0);
202    let second = *time_nums.get(2).unwrap_or(&0);
203
204    Some(ymd_hms_to_timestamp(year, month, day, hour, minute, second))
205}
206
207fn ymd_hms_to_timestamp(year: i32, month: i32, day: i32, hour: i32, min: i32, sec: i32) -> i64 {
208    // Days from epoch to year
209    let mut days: i64 = 0;
210    if year >= 1970 {
211        for y in 1970..year {
212            days += if is_leap(y) { 366 } else { 365 };
213        }
214    } else {
215        for y in year..1970 {
216            days -= if is_leap(y) { 366 } else { 365 };
217        }
218    }
219    // Days from months
220    for m in 1..month {
221        days += days_in_month(year, m) as i64;
222    }
223    days += (day - 1) as i64;
224
225    days * 86400 + hour as i64 * 3600 + min as i64 * 60 + sec as i64
226}
227
228fn timestamp_to_ymd(ts: i64) -> (i32, i32, i32) {
229    let mut days = ts / 86400;
230    if ts < 0 && ts % 86400 != 0 {
231        days -= 1;
232    }
233    let mut year = 1970;
234    if days >= 0 {
235        loop {
236            let yd = if is_leap(year) { 366 } else { 365 };
237            if days < yd {
238                break;
239            }
240            days -= yd;
241            year += 1;
242        }
243    } else {
244        loop {
245            year -= 1;
246            let yd = if is_leap(year) { 366 } else { 365 };
247            days += yd;
248            if days >= 0 {
249                break;
250            }
251        }
252    }
253    let mut month = 1;
254    loop {
255        let md = days_in_month(year, month) as i64;
256        if days < md {
257            break;
258        }
259        days -= md;
260        month += 1;
261    }
262    (year, month, days as i32 + 1)
263}
264
265fn timestamp_to_iso(ts: i64) -> String {
266    let (year, month, day) = timestamp_to_ymd(ts);
267    let remainder = ((ts % 86400) + 86400) % 86400;
268    let hour = remainder / 3600;
269    let minute = (remainder % 3600) / 60;
270    let second = remainder % 60;
271    format!(
272        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
273        year, month, day, hour, minute, second
274    )
275}
276
277fn format_date(ts: i64, pattern: &str) -> String {
278    let (year, month, day) = timestamp_to_ymd(ts);
279    let remainder = ((ts % 86400) + 86400) % 86400;
280    let hour = remainder / 3600;
281    let minute = (remainder % 3600) / 60;
282    let second = remainder % 60;
283    let days_since_epoch = ts / 86400;
284    let dow = ((days_since_epoch % 7 + 4) % 7) as usize;
285
286    let day_names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
287    let day_short = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
288    let month_names = [
289        "", "January", "February", "March", "April", "May", "June",
290        "July", "August", "September", "October", "November", "December",
291    ];
292    let month_short = [
293        "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
294        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
295    ];
296
297    pattern
298        .replace("YYYY", &format!("{:04}", year))
299        .replace("YY", &format!("{:02}", year % 100))
300        .replace("MMMM", month_names.get(month as usize).unwrap_or(&""))
301        .replace("MMM", month_short.get(month as usize).unwrap_or(&""))
302        .replace("MM", &format!("{:02}", month))
303        .replace("DD", &format!("{:02}", day))
304        .replace("dddd", day_names.get(dow).unwrap_or(&""))
305        .replace("ddd", day_short.get(dow).unwrap_or(&""))
306        .replace("HH", &format!("{:02}", hour))
307        .replace("mm", &format!("{:02}", minute))
308        .replace("ss", &format!("{:02}", second))
309}
310
311fn add_duration(ts: i64, amount: i64, unit: &str) -> i64 {
312    match unit {
313        "seconds" | "second" => ts + amount,
314        "minutes" | "minute" => ts + amount * 60,
315        "hours" | "hour" => ts + amount * 3600,
316        "days" | "day" => ts + amount * 86400,
317        "weeks" | "week" => ts + amount * 604800,
318        "months" | "month" => {
319            let (mut year, mut month, day) = timestamp_to_ymd(ts);
320            let remainder = ((ts % 86400) + 86400) % 86400;
321            let total_months = year as i64 * 12 + (month as i64 - 1) + amount;
322            year = (total_months / 12) as i32;
323            month = (total_months % 12 + 1) as i32;
324            if month <= 0 {
325                month += 12;
326                year -= 1;
327            }
328            let max_day = days_in_month(year, month);
329            let day = day.min(max_day);
330            let hour = remainder / 3600;
331            let minute = (remainder % 3600) / 60;
332            let second = remainder % 60;
333            ymd_hms_to_timestamp(year, month, day, hour as i32, minute as i32, second as i32)
334        }
335        "years" | "year" => {
336            let (year, month, day) = timestamp_to_ymd(ts);
337            let remainder = ((ts % 86400) + 86400) % 86400;
338            let new_year = year + amount as i32;
339            let max_day = days_in_month(new_year, month);
340            let day = day.min(max_day);
341            let hour = remainder / 3600;
342            let minute = (remainder % 3600) / 60;
343            let second = remainder % 60;
344            ymd_hms_to_timestamp(new_year, month, day, hour as i32, minute as i32, second as i32)
345        }
346        _ => ts + amount * 86400,
347    }
348}
349
350fn is_leap(year: i32) -> bool {
351    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
352}
353
354fn days_in_month(year: i32, month: i32) -> i32 {
355    match month {
356        1 => 31,
357        2 => if is_leap(year) { 29 } else { 28 },
358        3 => 31,
359        4 => 30,
360        5 => 31,
361        6 => 30,
362        7 => 31,
363        8 => 31,
364        9 => 30,
365        10 => 31,
366        11 => 30,
367        12 => 31,
368        _ => 30,
369    }
370}