1const MILLIS_PER_SECOND: i64 = 1_000;
14const MILLIS_PER_MINUTE: i64 = 60 * MILLIS_PER_SECOND;
15const MILLIS_PER_HOUR: i64 = 60 * MILLIS_PER_MINUTE;
16const MILLIS_PER_DAY: i64 = 24 * MILLIS_PER_HOUR;
17
18const DAYS_IN_MONTH: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
20
21pub fn datetime_now() -> i64 {
28 std::time::SystemTime::now()
29 .duration_since(std::time::UNIX_EPOCH)
30 .unwrap_or_default()
31 .as_millis() as i64
32}
33
34pub fn datetime_from_epoch(millis: i64) -> i64 {
36 millis
37}
38
39pub fn datetime_from_parts(year: i64, month: i64, day: i64, hour: i64, min: i64, sec: i64) -> i64 {
42 let days = days_from_civil(year, month, day);
43 days * MILLIS_PER_DAY + hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND
44}
45
46pub fn datetime_year(millis: i64) -> i64 {
52 let (y, _, _) = civil_from_days(millis.div_euclid(MILLIS_PER_DAY));
53 y
54}
55
56pub fn datetime_month(millis: i64) -> i64 {
58 let (_, m, _) = civil_from_days(millis.div_euclid(MILLIS_PER_DAY));
59 m
60}
61
62pub fn datetime_day(millis: i64) -> i64 {
64 let (_, _, d) = civil_from_days(millis.div_euclid(MILLIS_PER_DAY));
65 d
66}
67
68pub fn datetime_hour(millis: i64) -> i64 {
70 let day_millis = millis.rem_euclid(MILLIS_PER_DAY);
71 day_millis / MILLIS_PER_HOUR
72}
73
74pub fn datetime_minute(millis: i64) -> i64 {
76 let day_millis = millis.rem_euclid(MILLIS_PER_DAY);
77 (day_millis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE
78}
79
80pub fn datetime_second(millis: i64) -> i64 {
82 let day_millis = millis.rem_euclid(MILLIS_PER_DAY);
83 (day_millis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND
84}
85
86pub fn datetime_diff(a: i64, b: i64) -> i64 {
92 a - b
93}
94
95pub fn datetime_add_millis(dt: i64, millis: i64) -> i64 {
97 dt + millis
98}
99
100pub fn datetime_format(millis: i64) -> String {
106 let days = millis.div_euclid(MILLIS_PER_DAY);
107 let (year, month, day) = civil_from_days(days);
108 let day_millis = millis.rem_euclid(MILLIS_PER_DAY);
109 let hour = day_millis / MILLIS_PER_HOUR;
110 let minute = (day_millis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
111 let second = (day_millis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
112 format!(
113 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
114 year, month, day, hour, minute, second
115 )
116}
117
118fn is_leap_year(year: i64) -> bool {
124 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
125}
126
127fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
133 let (y, m) = if month <= 2 {
135 (year - 1, month + 9)
136 } else {
137 (year, month - 3)
138 };
139 let era = y.div_euclid(400);
140 let yoe = y.rem_euclid(400); let doy = (153 * m + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468 }
145
146fn civil_from_days(days: i64) -> (i64, i64, i64) {
150 let z = days + 719468;
151 let era = z.div_euclid(146097);
152 let doe = z.rem_euclid(146097); let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
155 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 };
159 let y = if m <= 2 { y + 1 } else { y };
160 (y, m, d)
161}
162
163pub fn days_in_month(year: i64, month: i64) -> i64 {
167 if month == 2 && is_leap_year(year) {
168 29
169 } else if month >= 1 && month <= 12 {
170 DAYS_IN_MONTH[(month - 1) as usize]
171 } else {
172 0
173 }
174}
175
176pub fn parse_date(s: &str, fmt: &str) -> i64 {
187 let mut year: i64 = 1970;
188 let mut month: i64 = 1;
189 let mut day: i64 = 1;
190 let mut hour: i64 = 0;
191 let mut minute: i64 = 0;
192 let mut second: i64 = 0;
193
194 let sbytes = s.as_bytes();
195 let fbytes = fmt.as_bytes();
196 let mut si = 0usize;
197 let mut fi = 0usize;
198
199 while fi < fbytes.len() && si < sbytes.len() {
200 if fbytes[fi] == b'%' && fi + 1 < fbytes.len() {
201 let spec = fbytes[fi + 1];
202 fi += 2;
203 match spec {
204 b'Y' => {
205 if let Some((val, consumed)) = parse_int_n(sbytes, si, 4) {
206 year = val;
207 si += consumed;
208 } else {
209 return 0;
210 }
211 }
212 b'm' => {
213 if let Some((val, consumed)) = parse_int_max(sbytes, si, 2) {
214 month = val;
215 si += consumed;
216 } else {
217 return 0;
218 }
219 }
220 b'd' => {
221 if let Some((val, consumed)) = parse_int_max(sbytes, si, 2) {
222 day = val;
223 si += consumed;
224 } else {
225 return 0;
226 }
227 }
228 b'H' => {
229 if let Some((val, consumed)) = parse_int_max(sbytes, si, 2) {
230 hour = val;
231 si += consumed;
232 } else {
233 return 0;
234 }
235 }
236 b'M' => {
237 if let Some((val, consumed)) = parse_int_max(sbytes, si, 2) {
238 minute = val;
239 si += consumed;
240 } else {
241 return 0;
242 }
243 }
244 b'S' => {
245 if let Some((val, consumed)) = parse_int_max(sbytes, si, 2) {
246 second = val;
247 si += consumed;
248 } else {
249 return 0;
250 }
251 }
252 _ => {
253 if si < sbytes.len() && sbytes[si] == spec {
255 si += 1;
256 } else {
257 return 0;
258 }
259 }
260 }
261 } else {
262 if sbytes[si] == fbytes[fi] {
264 si += 1;
265 fi += 1;
266 } else {
267 return 0;
268 }
269 }
270 }
271
272 datetime_from_parts(year, month, day, hour, minute, second)
273}
274
275pub fn date_format_custom(ts: i64, fmt: &str) -> String {
281 let days = ts.div_euclid(MILLIS_PER_DAY);
282 let (year, month, day) = civil_from_days(days);
283 let day_millis = ts.rem_euclid(MILLIS_PER_DAY);
284 let hour = day_millis / MILLIS_PER_HOUR;
285 let minute = (day_millis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
286 let second = (day_millis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
287
288 let fbytes = fmt.as_bytes();
289 let mut result = String::new();
290 let mut i = 0;
291 while i < fbytes.len() {
292 if fbytes[i] == b'%' && i + 1 < fbytes.len() {
293 match fbytes[i + 1] {
294 b'Y' => { result.push_str(&format!("{:04}", year)); i += 2; }
295 b'm' => { result.push_str(&format!("{:02}", month)); i += 2; }
296 b'd' => { result.push_str(&format!("{:02}", day)); i += 2; }
297 b'H' => { result.push_str(&format!("{:02}", hour)); i += 2; }
298 b'M' => { result.push_str(&format!("{:02}", minute)); i += 2; }
299 b'S' => { result.push_str(&format!("{:02}", second)); i += 2; }
300 _ => { result.push('%'); i += 1; }
301 }
302 } else {
303 result.push(fbytes[i] as char);
304 i += 1;
305 }
306 }
307 result
308}
309
310pub fn date_diff_units(ts1: i64, ts2: i64, unit: &str) -> Result<i64, String> {
315 let diff_ms = ts2 - ts1;
316 match unit {
317 "ms" => Ok(diff_ms),
318 "s" => Ok(diff_ms / MILLIS_PER_SECOND),
319 "min" => Ok(diff_ms / MILLIS_PER_MINUTE),
320 "h" => Ok(diff_ms / MILLIS_PER_HOUR),
321 "d" => Ok(diff_ms / MILLIS_PER_DAY),
322 _ => Err(format!("date_diff: unknown unit '{}', expected ms|s|min|h|d", unit)),
323 }
324}
325
326pub fn date_add_units(ts: i64, amount: i64, unit: &str) -> Result<i64, String> {
330 let millis = match unit {
331 "ms" => amount,
332 "s" => amount * MILLIS_PER_SECOND,
333 "min" => amount * MILLIS_PER_MINUTE,
334 "h" => amount * MILLIS_PER_HOUR,
335 "d" => amount * MILLIS_PER_DAY,
336 _ => return Err(format!("date_add: unknown unit '{}', expected ms|s|min|h|d", unit)),
337 };
338 Ok(ts + millis)
339}
340
341fn parse_int_n(bytes: &[u8], pos: usize, n: usize) -> Option<(i64, usize)> {
343 if pos + n > bytes.len() { return None; }
344 let mut val: i64 = 0;
345 for i in 0..n {
346 let b = bytes[pos + i];
347 if !b.is_ascii_digit() { return None; }
348 val = val * 10 + (b - b'0') as i64;
349 }
350 Some((val, n))
351}
352
353fn parse_int_max(bytes: &[u8], pos: usize, max_digits: usize) -> Option<(i64, usize)> {
355 let mut val: i64 = 0;
356 let mut count = 0;
357 while count < max_digits && pos + count < bytes.len() && bytes[pos + count].is_ascii_digit() {
358 val = val * 10 + (bytes[pos + count] - b'0') as i64;
359 count += 1;
360 }
361 if count == 0 { None } else { Some((val, count)) }
362}
363
364#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_epoch_origin() {
374 let dt = datetime_from_parts(1970, 1, 1, 0, 0, 0);
376 assert_eq!(dt, 0);
377 }
378
379 #[test]
380 fn test_known_date() {
381 let dt = datetime_from_parts(2000, 1, 1, 0, 0, 0);
383 assert_eq!(datetime_year(dt), 2000);
384 assert_eq!(datetime_month(dt), 1);
385 assert_eq!(datetime_day(dt), 1);
386 assert_eq!(datetime_hour(dt), 0);
387 }
388
389 #[test]
390 fn test_extraction_roundtrip() {
391 let dt = datetime_from_parts(2024, 6, 15, 14, 30, 45);
392 assert_eq!(datetime_year(dt), 2024);
393 assert_eq!(datetime_month(dt), 6);
394 assert_eq!(datetime_day(dt), 15);
395 assert_eq!(datetime_hour(dt), 14);
396 assert_eq!(datetime_minute(dt), 30);
397 assert_eq!(datetime_second(dt), 45);
398 }
399
400 #[test]
401 fn test_leap_year() {
402 assert!(is_leap_year(2000));
403 assert!(is_leap_year(2024));
404 assert!(!is_leap_year(1900));
405 assert!(!is_leap_year(2023));
406 }
407
408 #[test]
409 fn test_days_in_feb_leap() {
410 assert_eq!(days_in_month(2024, 2), 29);
411 assert_eq!(days_in_month(2023, 2), 28);
412 }
413
414 #[test]
415 fn test_format_iso8601() {
416 let dt = datetime_from_parts(2024, 3, 14, 9, 26, 53);
417 let s = datetime_format(dt);
418 assert_eq!(s, "2024-03-14T09:26:53Z");
419 }
420
421 #[test]
422 fn test_diff() {
423 let a = datetime_from_parts(2024, 1, 2, 0, 0, 0);
424 let b = datetime_from_parts(2024, 1, 1, 0, 0, 0);
425 assert_eq!(datetime_diff(a, b), MILLIS_PER_DAY);
426 }
427
428 #[test]
429 fn test_add_millis() {
430 let dt = datetime_from_parts(2024, 1, 1, 0, 0, 0);
431 let dt2 = datetime_add_millis(dt, MILLIS_PER_HOUR);
432 assert_eq!(datetime_hour(dt2), 1);
433 }
434
435 #[test]
436 fn test_format_epoch() {
437 assert_eq!(datetime_format(0), "1970-01-01T00:00:00Z");
438 }
439
440 #[test]
441 fn test_determinism() {
442 let a = datetime_from_parts(2024, 12, 31, 23, 59, 59);
444 let b = datetime_from_parts(2024, 12, 31, 23, 59, 59);
445 assert_eq!(a, b);
446 assert_eq!(datetime_format(a), datetime_format(b));
447 }
448}