1const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
10const MICROSECONDS_PER_MINUTE: i64 = 60 * MICROSECONDS_PER_SECOND;
11const MICROSECONDS_PER_HOUR: i64 = 60 * MICROSECONDS_PER_MINUTE;
12const MILLISECONDS_PER_DAY: i64 = 24 * 60 * 60 * 1000;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DateTimeParseError {
17 pub message: String,
18}
19
20impl std::fmt::Display for DateTimeParseError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{}", self.message)
23 }
24}
25
26impl std::error::Error for DateTimeParseError {}
27
28fn parse_timezone_offset(offset: &str) -> Result<i16, DateTimeParseError> {
30 if offset == "Z" || offset == "z" {
31 return Ok(0);
32 }
33
34 if offset.len() != 6 {
35 return Err(DateTimeParseError {
36 message: format!("Invalid timezone offset: {}", offset),
37 });
38 }
39
40 let sign = match offset.chars().next() {
41 Some('+') => 1i16,
42 Some('-') => -1i16,
43 _ => {
44 return Err(DateTimeParseError {
45 message: format!("Invalid timezone offset: {}", offset),
46 })
47 }
48 };
49
50 if offset.chars().nth(3) != Some(':') {
51 return Err(DateTimeParseError {
52 message: format!("Invalid timezone offset: {}", offset),
53 });
54 }
55
56 let hours: i16 = offset[1..3].parse().map_err(|_| DateTimeParseError {
57 message: format!("Invalid timezone offset: {}", offset),
58 })?;
59
60 let minutes: i16 = offset[4..6].parse().map_err(|_| DateTimeParseError {
61 message: format!("Invalid timezone offset: {}", offset),
62 })?;
63
64 if hours > 24 || (hours == 24 && minutes != 0) || minutes > 59 {
66 return Err(DateTimeParseError {
67 message: format!("Invalid timezone offset: {}", offset),
68 });
69 }
70
71 let total_minutes = sign * (hours * 60 + minutes);
72 if total_minutes < -1440 || total_minutes > 1440 {
73 return Err(DateTimeParseError {
74 message: format!("Timezone offset out of range [-24:00, +24:00]: {}", offset),
75 });
76 }
77
78 Ok(total_minutes)
79}
80
81fn format_timezone_offset(offset_min: i16) -> String {
83 if offset_min == 0 {
84 return "Z".to_string();
85 }
86
87 let sign = if offset_min >= 0 { '+' } else { '-' };
88 let abs_offset = offset_min.abs();
89 let hours = abs_offset / 60;
90 let minutes = abs_offset % 60;
91
92 format!("{}{:02}:{:02}", sign, hours, minutes)
93}
94
95fn parse_fractional_seconds(frac: Option<&str>) -> i64 {
97 match frac {
98 None => 0,
99 Some(s) if s.is_empty() => 0,
100 Some(s) => {
101 let mut padded = s.to_string();
103 while padded.len() < 6 {
104 padded.push('0');
105 }
106 padded.truncate(6);
107 padded.parse().unwrap_or(0)
108 }
109 }
110}
111
112fn format_fractional_seconds(us: i64) -> String {
114 if us == 0 {
115 return String::new();
116 }
117
118 let str = format!("{:06}", us);
120 let trimmed = str.trim_end_matches('0');
121 format!(".{}", trimmed)
122}
123
124fn is_leap_year(year: i32) -> bool {
126 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
127}
128
129fn days_in_month(year: i32, month: u32) -> u32 {
131 match month {
132 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
133 4 | 6 | 9 | 11 => 30,
134 2 => {
135 if is_leap_year(year) {
136 29
137 } else {
138 28
139 }
140 }
141 _ => 0,
142 }
143}
144
145fn date_to_days(year: i32, month: u32, day: u32) -> i32 {
147 let y = if month <= 2 {
150 year - 1
151 } else {
152 year
153 } as i64;
154
155 let m = if month <= 2 {
156 month as i64 + 9
157 } else {
158 month as i64 - 3
159 };
160
161 let era = if y >= 0 { y } else { y - 399 } / 400;
162 let yoe = (y - era * 400) as u32; let doy = (153 * m as u32 + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; (era * 146097 + doe as i64 - 719468) as i32
167}
168
169fn days_to_date(days: i32) -> (i32, u32, u32) {
171 let z = days as i64 + 719468;
173 let era = if z >= 0 { z } else { z - 146096 } / 146097;
174 let doe = (z - era * 146097) as u32; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe as i64 + era * 400;
177 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 }; let year = if m <= 2 { y + 1 } else { y } as i32;
183 (year, m, d)
184}
185
186pub fn parse_date_rfc3339(date_str: &str) -> Result<(i32, i16), DateTimeParseError> {
193 let (date_part, offset_str) = if date_str.len() >= 10 {
195 let date = &date_str[..10];
196 let rest = &date_str[10..];
197 if rest.is_empty() {
198 (date, None)
199 } else {
200 (date, Some(rest))
201 }
202 } else {
203 return Err(DateTimeParseError {
204 message: format!("Invalid RFC 3339 date: {}", date_str),
205 });
206 };
207
208 if date_part.len() != 10
210 || date_part.chars().nth(4) != Some('-')
211 || date_part.chars().nth(7) != Some('-')
212 {
213 return Err(DateTimeParseError {
214 message: format!("Invalid RFC 3339 date: {}", date_str),
215 });
216 }
217
218 let year: i32 = date_part[..4].parse().map_err(|_| DateTimeParseError {
219 message: format!("Invalid year in date: {}", date_str),
220 })?;
221
222 let month: u32 = date_part[5..7].parse().map_err(|_| DateTimeParseError {
223 message: format!("Invalid month in date: {}", date_str),
224 })?;
225
226 let day: u32 = date_part[8..10].parse().map_err(|_| DateTimeParseError {
227 message: format!("Invalid day in date: {}", date_str),
228 })?;
229
230 if month < 1 || month > 12 {
232 return Err(DateTimeParseError {
233 message: format!("Invalid month in date: {}", date_str),
234 });
235 }
236 if day < 1 || day > days_in_month(year, month) {
237 return Err(DateTimeParseError {
238 message: format!("Invalid day in date: {}", date_str),
239 });
240 }
241
242 let days = date_to_days(year, month, day);
243 let offset_min = match offset_str {
244 Some(s) => parse_timezone_offset(s)?,
245 None => 0,
246 };
247
248 Ok((days, offset_min))
249}
250
251pub fn format_date_rfc3339(days: i32, offset_min: i16) -> String {
253 let (year, month, day) = days_to_date(days);
254 let offset = format_timezone_offset(offset_min);
255 format!("{:04}-{:02}-{:02}{}", year, month, day, offset)
256}
257
258pub fn parse_time_rfc3339(time_str: &str) -> Result<(i64, i16), DateTimeParseError> {
265 if time_str.len() < 8 {
267 return Err(DateTimeParseError {
268 message: format!("Invalid RFC 3339 time: {}", time_str),
269 });
270 }
271
272 if time_str.chars().nth(2) != Some(':') || time_str.chars().nth(5) != Some(':') {
274 return Err(DateTimeParseError {
275 message: format!("Invalid RFC 3339 time: {}", time_str),
276 });
277 }
278
279 let hours: i64 = time_str[..2].parse().map_err(|_| DateTimeParseError {
280 message: format!("Invalid hours in time: {}", time_str),
281 })?;
282
283 let minutes: i64 = time_str[3..5].parse().map_err(|_| DateTimeParseError {
284 message: format!("Invalid minutes in time: {}", time_str),
285 })?;
286
287 let seconds: i64 = time_str[6..8].parse().map_err(|_| DateTimeParseError {
288 message: format!("Invalid seconds in time: {}", time_str),
289 })?;
290
291 if hours > 23 {
293 return Err(DateTimeParseError {
294 message: format!("Invalid hours in time: {}", time_str),
295 });
296 }
297 if minutes > 59 {
298 return Err(DateTimeParseError {
299 message: format!("Invalid minutes in time: {}", time_str),
300 });
301 }
302 if seconds > 59 {
303 return Err(DateTimeParseError {
304 message: format!("Invalid seconds in time: {}", time_str),
305 });
306 }
307
308 let rest = &time_str[8..];
310 let (fractional, offset_str) = if rest.starts_with('.') {
311 let frac_end = rest[1..]
313 .find(|c: char| !c.is_ascii_digit())
314 .map(|i| i + 1)
315 .unwrap_or(rest.len());
316
317 let frac = &rest[1..frac_end];
318 let tz = if frac_end < rest.len() {
319 Some(&rest[frac_end..])
320 } else {
321 None
322 };
323 (Some(frac), tz)
324 } else if rest.is_empty() {
325 (None, None)
326 } else {
327 (None, Some(rest))
328 };
329
330 let microseconds = parse_fractional_seconds(fractional);
331 let time_micros = hours * MICROSECONDS_PER_HOUR
332 + minutes * MICROSECONDS_PER_MINUTE
333 + seconds * MICROSECONDS_PER_SECOND
334 + microseconds;
335
336 if time_micros > 86_399_999_999 {
338 return Err(DateTimeParseError {
339 message: format!("Time exceeds maximum (23:59:59.999999): {}", time_str),
340 });
341 }
342
343 let offset_min = match offset_str {
344 Some(s) => parse_timezone_offset(s)?,
345 None => 0,
346 };
347
348 Ok((time_micros, offset_min))
349}
350
351pub fn format_time_rfc3339(time_micros: i64, offset_min: i16) -> String {
353 let hours = time_micros / MICROSECONDS_PER_HOUR;
354 let remaining1 = time_micros % MICROSECONDS_PER_HOUR;
355 let minutes = remaining1 / MICROSECONDS_PER_MINUTE;
356 let remaining2 = remaining1 % MICROSECONDS_PER_MINUTE;
357 let seconds = remaining2 / MICROSECONDS_PER_SECOND;
358 let microseconds = remaining2 % MICROSECONDS_PER_SECOND;
359
360 let frac = format_fractional_seconds(microseconds);
361 let offset = format_timezone_offset(offset_min);
362
363 format!("{:02}:{:02}:{:02}{}{}", hours, minutes, seconds, frac, offset)
364}
365
366pub fn parse_datetime_rfc3339(datetime_str: &str) -> Result<(i64, i16), DateTimeParseError> {
373 if datetime_str.len() < 19 {
375 return Err(DateTimeParseError {
376 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
377 });
378 }
379
380 let sep = datetime_str.chars().nth(10);
382 if sep != Some('T') && sep != Some(' ') {
383 return Err(DateTimeParseError {
384 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
385 });
386 }
387
388 let date_part = &datetime_str[..10];
390 if date_part.chars().nth(4) != Some('-') || date_part.chars().nth(7) != Some('-') {
391 return Err(DateTimeParseError {
392 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
393 });
394 }
395
396 let year: i32 = date_part[..4].parse().map_err(|_| DateTimeParseError {
397 message: format!("Invalid year in datetime: {}", datetime_str),
398 })?;
399
400 let month: u32 = date_part[5..7].parse().map_err(|_| DateTimeParseError {
401 message: format!("Invalid month in datetime: {}", datetime_str),
402 })?;
403
404 let day: u32 = date_part[8..10].parse().map_err(|_| DateTimeParseError {
405 message: format!("Invalid day in datetime: {}", datetime_str),
406 })?;
407
408 if month < 1 || month > 12 {
410 return Err(DateTimeParseError {
411 message: format!("Invalid month in datetime: {}", datetime_str),
412 });
413 }
414 if day < 1 || day > days_in_month(year, month) {
415 return Err(DateTimeParseError {
416 message: format!("Invalid day in datetime: {}", datetime_str),
417 });
418 }
419
420 let time_part = &datetime_str[11..];
422 if time_part.len() < 8
423 || time_part.chars().nth(2) != Some(':')
424 || time_part.chars().nth(5) != Some(':')
425 {
426 return Err(DateTimeParseError {
427 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
428 });
429 }
430
431 let hours: i64 = time_part[..2].parse().map_err(|_| DateTimeParseError {
432 message: format!("Invalid hours in datetime: {}", datetime_str),
433 })?;
434
435 let minutes: i64 = time_part[3..5].parse().map_err(|_| DateTimeParseError {
436 message: format!("Invalid minutes in datetime: {}", datetime_str),
437 })?;
438
439 let seconds: i64 = time_part[6..8].parse().map_err(|_| DateTimeParseError {
440 message: format!("Invalid seconds in datetime: {}", datetime_str),
441 })?;
442
443 if hours > 23 {
445 return Err(DateTimeParseError {
446 message: format!("Invalid hours in datetime: {}", datetime_str),
447 });
448 }
449 if minutes > 59 {
450 return Err(DateTimeParseError {
451 message: format!("Invalid minutes in datetime: {}", datetime_str),
452 });
453 }
454 if seconds > 59 {
455 return Err(DateTimeParseError {
456 message: format!("Invalid seconds in datetime: {}", datetime_str),
457 });
458 }
459
460 let rest = &time_part[8..];
462 let (fractional, offset_str) = if rest.starts_with('.') {
463 let frac_end = rest[1..]
465 .find(|c: char| !c.is_ascii_digit())
466 .map(|i| i + 1)
467 .unwrap_or(rest.len());
468
469 let frac = &rest[1..frac_end];
470 let tz = if frac_end < rest.len() {
471 Some(&rest[frac_end..])
472 } else {
473 None
474 };
475 (Some(frac), tz)
476 } else if rest.is_empty() {
477 (None, None)
478 } else {
479 (None, Some(rest))
480 };
481
482 let offset_min = match offset_str {
483 Some(s) => parse_timezone_offset(s)?,
484 None => 0,
485 };
486
487 let microseconds = parse_fractional_seconds(fractional);
488
489 let days = date_to_days(year, month, day) as i64;
492
493 let epoch_micros_utc = days * MILLISECONDS_PER_DAY * 1000
495 + hours * MICROSECONDS_PER_HOUR
496 + minutes * MICROSECONDS_PER_MINUTE
497 + seconds * MICROSECONDS_PER_SECOND
498 + microseconds;
499
500 let offset_us = offset_min as i64 * MICROSECONDS_PER_MINUTE;
502 let epoch_micros = epoch_micros_utc - offset_us;
503
504 Ok((epoch_micros, offset_min))
505}
506
507pub fn format_datetime_rfc3339(epoch_micros: i64, offset_min: i16) -> String {
509 let offset_us = offset_min as i64 * MICROSECONDS_PER_MINUTE;
511 let local_us = epoch_micros + offset_us;
512
513 let us_per_day = MILLISECONDS_PER_DAY * 1000;
515
516 let (days, time_micros) = if local_us >= 0 {
518 let days = (local_us / us_per_day) as i32;
519 let time_micros = local_us % us_per_day;
520 (days, time_micros)
521 } else {
522 let days = ((local_us + 1) / us_per_day - 1) as i32;
524 let time_micros = ((local_us % us_per_day) + us_per_day) % us_per_day;
525 (days, time_micros)
526 };
527
528 let (year, month, day) = days_to_date(days);
529
530 let hours = time_micros / MICROSECONDS_PER_HOUR;
531 let remaining1 = time_micros % MICROSECONDS_PER_HOUR;
532 let minutes = remaining1 / MICROSECONDS_PER_MINUTE;
533 let remaining2 = remaining1 % MICROSECONDS_PER_MINUTE;
534 let seconds = remaining2 / MICROSECONDS_PER_SECOND;
535 let microseconds = remaining2 % MICROSECONDS_PER_SECOND;
536
537 let frac = format_fractional_seconds(microseconds);
538 let offset = format_timezone_offset(offset_min);
539
540 format!(
541 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{}",
542 year, month, day, hours, minutes, seconds, frac, offset
543 )
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn test_parse_date_basic() {
552 let (days, offset) = parse_date_rfc3339("1970-01-01").unwrap();
553 assert_eq!(days, 0);
554 assert_eq!(offset, 0);
555
556 let (days, offset) = parse_date_rfc3339("1970-01-01Z").unwrap();
557 assert_eq!(days, 0);
558 assert_eq!(offset, 0);
559
560 let (days, offset) = parse_date_rfc3339("2024-03-15").unwrap();
561 assert_eq!(days, 19797);
562 assert_eq!(offset, 0);
563
564 let (days, offset) = parse_date_rfc3339("2024-03-15+05:30").unwrap();
565 assert_eq!(days, 19797);
566 assert_eq!(offset, 330);
567 }
568
569 #[test]
570 fn test_format_date() {
571 assert_eq!(format_date_rfc3339(0, 0), "1970-01-01Z");
572 assert_eq!(format_date_rfc3339(19797, 0), "2024-03-15Z");
573 assert_eq!(format_date_rfc3339(19797, 330), "2024-03-15+05:30");
574 assert_eq!(format_date_rfc3339(19797, -300), "2024-03-15-05:00");
575 }
576
577 #[test]
578 fn test_date_roundtrip() {
579 let dates = [
580 "1970-01-01Z",
581 "2024-03-15Z",
582 "2024-03-15+05:30",
583 "2024-12-31-08:00",
584 "2000-02-29Z", ];
586
587 for date in dates {
588 let (days, offset) = parse_date_rfc3339(date).unwrap();
589 let formatted = format_date_rfc3339(days, offset);
590 assert_eq!(date, formatted, "Roundtrip failed for {}", date);
591 }
592 }
593
594 #[test]
595 fn test_parse_time_basic() {
596 let (time_micros, offset) = parse_time_rfc3339("00:00:00").unwrap();
597 assert_eq!(time_micros, 0);
598 assert_eq!(offset, 0);
599
600 let (time_micros, offset) = parse_time_rfc3339("14:30:00Z").unwrap();
601 assert_eq!(time_micros, 52_200_000_000);
602 assert_eq!(offset, 0);
603
604 let (time_micros, offset) = parse_time_rfc3339("14:30:00.5Z").unwrap();
605 assert_eq!(time_micros, 52_200_500_000);
606 assert_eq!(offset, 0);
607
608 let (time_micros, offset) = parse_time_rfc3339("14:30:00.123456+05:30").unwrap();
609 assert_eq!(time_micros, 52_200_123_456);
610 assert_eq!(offset, 330);
611 }
612
613 #[test]
614 fn test_format_time() {
615 assert_eq!(format_time_rfc3339(0, 0), "00:00:00Z");
616 assert_eq!(format_time_rfc3339(52_200_000_000, 0), "14:30:00Z");
617 assert_eq!(format_time_rfc3339(52_200_500_000, 0), "14:30:00.5Z");
618 assert_eq!(format_time_rfc3339(52_200_123_456, 330), "14:30:00.123456+05:30");
619 }
620
621 #[test]
622 fn test_time_roundtrip() {
623 let times = [
624 "00:00:00Z",
625 "14:30:00Z",
626 "14:30:00.5Z",
627 "14:30:00.123456Z",
628 "23:59:59.999999Z",
629 "14:30:00+05:30",
630 "14:30:00-08:00",
631 ];
632
633 for time in times {
634 let (time_micros, offset) = parse_time_rfc3339(time).unwrap();
635 let formatted = format_time_rfc3339(time_micros, offset);
636 assert_eq!(time, formatted, "Roundtrip failed for {}", time);
637 }
638 }
639
640 #[test]
641 fn test_parse_datetime_basic() {
642 let (epoch_micros, offset) = parse_datetime_rfc3339("1970-01-01T00:00:00Z").unwrap();
643 assert_eq!(epoch_micros, 0);
644 assert_eq!(offset, 0);
645
646 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00Z").unwrap();
647 assert_eq!(epoch_micros, 1710513000000000);
648 assert_eq!(offset, 0);
649
650 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00.123456Z").unwrap();
651 assert_eq!(epoch_micros, 1710513000123456);
652 assert_eq!(offset, 0);
653 }
654
655 #[test]
656 fn test_format_datetime() {
657 assert_eq!(format_datetime_rfc3339(0, 0), "1970-01-01T00:00:00Z");
658 assert_eq!(
659 format_datetime_rfc3339(1710513000000000, 0),
660 "2024-03-15T14:30:00Z"
661 );
662 assert_eq!(
663 format_datetime_rfc3339(1710513000123456, 0),
664 "2024-03-15T14:30:00.123456Z"
665 );
666 }
667
668 #[test]
669 fn test_datetime_roundtrip() {
670 let datetimes = [
671 "1970-01-01T00:00:00Z",
672 "2024-03-15T14:30:00Z",
673 "2024-03-15T14:30:00.5Z",
674 "2024-03-15T14:30:00.123456Z",
675 "2024-12-31T23:59:59.999999Z",
676 ];
677
678 for datetime in datetimes {
679 let (epoch_micros, offset) = parse_datetime_rfc3339(datetime).unwrap();
680 let formatted = format_datetime_rfc3339(epoch_micros, offset);
681 assert_eq!(datetime, formatted, "Roundtrip failed for {}", datetime);
682 }
683 }
684
685 #[test]
686 fn test_datetime_with_offset() {
687 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00+05:30").unwrap();
689 assert_eq!(offset, 330);
690 let (utc_epoch_micros, _) = parse_datetime_rfc3339("2024-03-15T09:00:00Z").unwrap();
692 assert_eq!(epoch_micros, utc_epoch_micros);
693
694 let formatted = format_datetime_rfc3339(epoch_micros, offset);
696 assert_eq!(formatted, "2024-03-15T14:30:00+05:30");
697 }
698
699 #[test]
700 fn test_negative_epoch() {
701 let (epoch_micros, offset) = parse_datetime_rfc3339("1969-12-31T23:59:59Z").unwrap();
703 assert_eq!(epoch_micros, -1_000_000);
704 assert_eq!(offset, 0);
705
706 let formatted = format_datetime_rfc3339(epoch_micros, offset);
707 assert_eq!(formatted, "1969-12-31T23:59:59Z");
708 }
709
710 #[test]
711 fn test_invalid_dates() {
712 assert!(parse_date_rfc3339("2024-13-01").is_err()); assert!(parse_date_rfc3339("2024-00-01").is_err()); assert!(parse_date_rfc3339("2024-02-30").is_err()); assert!(parse_date_rfc3339("2023-02-29").is_err()); assert!(parse_date_rfc3339("not-a-date").is_err());
717 }
718
719 #[test]
720 fn test_invalid_times() {
721 assert!(parse_time_rfc3339("24:00:00").is_err()); assert!(parse_time_rfc3339("14:60:00").is_err()); assert!(parse_time_rfc3339("14:30:60").is_err()); assert!(parse_time_rfc3339("not:a:time").is_err());
725 }
726
727 #[test]
728 fn test_timezone_offset_edge_cases() {
729 assert!(parse_timezone_offset("+24:00").is_ok());
730 assert!(parse_timezone_offset("-24:00").is_ok());
731 assert!(parse_timezone_offset("+24:01").is_err()); assert!(parse_timezone_offset("-24:01").is_err()); }
734}