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 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
159fn parse_date(s: &str) -> Option<i64> {
161 let s = s.trim();
162 if s.is_empty() {
163 return None;
164 }
165
166 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 if let Ok(ts) = s.parse::<i64>() {
178 if ts > 946684800 {
179 return Some(ts);
181 }
182 }
183
184 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 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 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}