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> {
268 if time_str.len() < 8 {
270 return Err(DateTimeParseError {
271 message: format!("Invalid RFC 3339 time: {}", time_str),
272 });
273 }
274
275 if time_str.chars().nth(2) != Some(':') || time_str.chars().nth(5) != Some(':') {
277 return Err(DateTimeParseError {
278 message: format!("Invalid RFC 3339 time: {}", time_str),
279 });
280 }
281
282 let hours: i64 = time_str[..2].parse().map_err(|_| DateTimeParseError {
283 message: format!("Invalid hours in time: {}", time_str),
284 })?;
285
286 let minutes: i64 = time_str[3..5].parse().map_err(|_| DateTimeParseError {
287 message: format!("Invalid minutes in time: {}", time_str),
288 })?;
289
290 let seconds: i64 = time_str[6..8].parse().map_err(|_| DateTimeParseError {
291 message: format!("Invalid seconds in time: {}", time_str),
292 })?;
293
294 if hours > 23 {
296 return Err(DateTimeParseError {
297 message: format!("Invalid hours in time: {}", time_str),
298 });
299 }
300 if minutes > 59 {
301 return Err(DateTimeParseError {
302 message: format!("Invalid minutes in time: {}", time_str),
303 });
304 }
305 if seconds > 59 {
306 return Err(DateTimeParseError {
307 message: format!("Invalid seconds in time: {}", time_str),
308 });
309 }
310
311 let rest = &time_str[8..];
313 let (fractional, offset_str) = if rest.starts_with('.') {
314 let frac_end = rest[1..]
316 .find(|c: char| !c.is_ascii_digit())
317 .map(|i| i + 1)
318 .unwrap_or(rest.len());
319
320 let frac = &rest[1..frac_end];
321 let tz = if frac_end < rest.len() {
322 Some(&rest[frac_end..])
323 } else {
324 None
325 };
326 (Some(frac), tz)
327 } else if rest.is_empty() {
328 (None, None)
329 } else {
330 (None, Some(rest))
331 };
332
333 let microseconds = parse_fractional_seconds(fractional);
334 let time_micros = hours * MICROSECONDS_PER_HOUR
335 + minutes * MICROSECONDS_PER_MINUTE
336 + seconds * MICROSECONDS_PER_SECOND
337 + microseconds;
338
339 if time_micros > 86_399_999_999 {
341 return Err(DateTimeParseError {
342 message: format!("Time exceeds maximum (23:59:59.999999): {}", time_str),
343 });
344 }
345
346 let offset_min = match offset_str {
347 Some(s) => parse_timezone_offset(s)?,
348 None => {
349 return Err(DateTimeParseError {
350 message: format!("Timezone offset required in time: {}", time_str),
351 });
352 }
353 };
354
355 Ok((time_micros, offset_min))
356}
357
358pub fn format_time_rfc3339(time_micros: i64, offset_min: i16) -> String {
360 let hours = time_micros / MICROSECONDS_PER_HOUR;
361 let remaining1 = time_micros % MICROSECONDS_PER_HOUR;
362 let minutes = remaining1 / MICROSECONDS_PER_MINUTE;
363 let remaining2 = remaining1 % MICROSECONDS_PER_MINUTE;
364 let seconds = remaining2 / MICROSECONDS_PER_SECOND;
365 let microseconds = remaining2 % MICROSECONDS_PER_SECOND;
366
367 let frac = format_fractional_seconds(microseconds);
368 let offset = format_timezone_offset(offset_min);
369
370 format!("{:02}:{:02}:{:02}{}{}", hours, minutes, seconds, frac, offset)
371}
372
373pub fn parse_datetime_rfc3339(datetime_str: &str) -> Result<(i64, i16), DateTimeParseError> {
383 if datetime_str.len() < 19 {
385 return Err(DateTimeParseError {
386 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
387 });
388 }
389
390 let sep = datetime_str.chars().nth(10);
392 if sep != Some('T') && sep != Some(' ') {
393 return Err(DateTimeParseError {
394 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
395 });
396 }
397
398 let date_part = &datetime_str[..10];
400 if date_part.chars().nth(4) != Some('-') || date_part.chars().nth(7) != Some('-') {
401 return Err(DateTimeParseError {
402 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
403 });
404 }
405
406 let year: i32 = date_part[..4].parse().map_err(|_| DateTimeParseError {
407 message: format!("Invalid year in datetime: {}", datetime_str),
408 })?;
409
410 let month: u32 = date_part[5..7].parse().map_err(|_| DateTimeParseError {
411 message: format!("Invalid month in datetime: {}", datetime_str),
412 })?;
413
414 let day: u32 = date_part[8..10].parse().map_err(|_| DateTimeParseError {
415 message: format!("Invalid day in datetime: {}", datetime_str),
416 })?;
417
418 if month < 1 || month > 12 {
420 return Err(DateTimeParseError {
421 message: format!("Invalid month in datetime: {}", datetime_str),
422 });
423 }
424 if day < 1 || day > days_in_month(year, month) {
425 return Err(DateTimeParseError {
426 message: format!("Invalid day in datetime: {}", datetime_str),
427 });
428 }
429
430 let time_part = &datetime_str[11..];
432 if time_part.len() < 8
433 || time_part.chars().nth(2) != Some(':')
434 || time_part.chars().nth(5) != Some(':')
435 {
436 return Err(DateTimeParseError {
437 message: format!("Invalid RFC 3339 datetime: {}", datetime_str),
438 });
439 }
440
441 let hours: i64 = time_part[..2].parse().map_err(|_| DateTimeParseError {
442 message: format!("Invalid hours in datetime: {}", datetime_str),
443 })?;
444
445 let minutes: i64 = time_part[3..5].parse().map_err(|_| DateTimeParseError {
446 message: format!("Invalid minutes in datetime: {}", datetime_str),
447 })?;
448
449 let seconds: i64 = time_part[6..8].parse().map_err(|_| DateTimeParseError {
450 message: format!("Invalid seconds in datetime: {}", datetime_str),
451 })?;
452
453 if hours > 23 {
455 return Err(DateTimeParseError {
456 message: format!("Invalid hours in datetime: {}", datetime_str),
457 });
458 }
459 if minutes > 59 {
460 return Err(DateTimeParseError {
461 message: format!("Invalid minutes in datetime: {}", datetime_str),
462 });
463 }
464 if seconds > 59 {
465 return Err(DateTimeParseError {
466 message: format!("Invalid seconds in datetime: {}", datetime_str),
467 });
468 }
469
470 let rest = &time_part[8..];
472 let (fractional, offset_str) = if rest.starts_with('.') {
473 let frac_end = rest[1..]
475 .find(|c: char| !c.is_ascii_digit())
476 .map(|i| i + 1)
477 .unwrap_or(rest.len());
478
479 let frac = &rest[1..frac_end];
480 let tz = if frac_end < rest.len() {
481 Some(&rest[frac_end..])
482 } else {
483 None
484 };
485 (Some(frac), tz)
486 } else if rest.is_empty() {
487 (None, None)
488 } else {
489 (None, Some(rest))
490 };
491
492 let offset_min = match offset_str {
493 Some(s) => parse_timezone_offset(s)?,
494 None => {
495 return Err(DateTimeParseError {
496 message: format!("Timezone offset required in datetime: {}", datetime_str),
497 });
498 }
499 };
500
501 let microseconds = parse_fractional_seconds(fractional);
502
503 let days = date_to_days(year, month, day) as i64;
506
507 let epoch_micros_utc = days * MILLISECONDS_PER_DAY * 1000
509 + hours * MICROSECONDS_PER_HOUR
510 + minutes * MICROSECONDS_PER_MINUTE
511 + seconds * MICROSECONDS_PER_SECOND
512 + microseconds;
513
514 let offset_us = offset_min as i64 * MICROSECONDS_PER_MINUTE;
516 let epoch_micros = epoch_micros_utc - offset_us;
517
518 Ok((epoch_micros, offset_min))
519}
520
521pub fn format_datetime_rfc3339(epoch_micros: i64, offset_min: i16) -> String {
523 let offset_us = offset_min as i64 * MICROSECONDS_PER_MINUTE;
525 let local_us = epoch_micros + offset_us;
526
527 let us_per_day = MILLISECONDS_PER_DAY * 1000;
529
530 let (days, time_micros) = if local_us >= 0 {
532 let days = (local_us / us_per_day) as i32;
533 let time_micros = local_us % us_per_day;
534 (days, time_micros)
535 } else {
536 let days = ((local_us + 1) / us_per_day - 1) as i32;
538 let time_micros = ((local_us % us_per_day) + us_per_day) % us_per_day;
539 (days, time_micros)
540 };
541
542 let (year, month, day) = days_to_date(days);
543
544 let hours = time_micros / MICROSECONDS_PER_HOUR;
545 let remaining1 = time_micros % MICROSECONDS_PER_HOUR;
546 let minutes = remaining1 / MICROSECONDS_PER_MINUTE;
547 let remaining2 = remaining1 % MICROSECONDS_PER_MINUTE;
548 let seconds = remaining2 / MICROSECONDS_PER_SECOND;
549 let microseconds = remaining2 % MICROSECONDS_PER_SECOND;
550
551 let frac = format_fractional_seconds(microseconds);
552 let offset = format_timezone_offset(offset_min);
553
554 format!(
555 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{}",
556 year, month, day, hours, minutes, seconds, frac, offset
557 )
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn test_parse_date_basic() {
566 let (days, offset) = parse_date_rfc3339("1970-01-01").unwrap();
567 assert_eq!(days, 0);
568 assert_eq!(offset, 0);
569
570 let (days, offset) = parse_date_rfc3339("1970-01-01Z").unwrap();
571 assert_eq!(days, 0);
572 assert_eq!(offset, 0);
573
574 let (days, offset) = parse_date_rfc3339("2024-03-15").unwrap();
575 assert_eq!(days, 19797);
576 assert_eq!(offset, 0);
577
578 let (days, offset) = parse_date_rfc3339("2024-03-15+05:30").unwrap();
579 assert_eq!(days, 19797);
580 assert_eq!(offset, 330);
581 }
582
583 #[test]
584 fn test_format_date() {
585 assert_eq!(format_date_rfc3339(0, 0), "1970-01-01Z");
586 assert_eq!(format_date_rfc3339(19797, 0), "2024-03-15Z");
587 assert_eq!(format_date_rfc3339(19797, 330), "2024-03-15+05:30");
588 assert_eq!(format_date_rfc3339(19797, -300), "2024-03-15-05:00");
589 }
590
591 #[test]
592 fn test_date_roundtrip() {
593 let dates = [
594 "1970-01-01Z",
595 "2024-03-15Z",
596 "2024-03-15+05:30",
597 "2024-12-31-08:00",
598 "2000-02-29Z", ];
600
601 for date in dates {
602 let (days, offset) = parse_date_rfc3339(date).unwrap();
603 let formatted = format_date_rfc3339(days, offset);
604 assert_eq!(date, formatted, "Roundtrip failed for {}", date);
605 }
606 }
607
608 #[test]
609 fn test_parse_time_basic() {
610 let (time_micros, offset) = parse_time_rfc3339("14:30:00Z").unwrap();
611 assert_eq!(time_micros, 52_200_000_000);
612 assert_eq!(offset, 0);
613
614 let (time_micros, offset) = parse_time_rfc3339("14:30:00.5Z").unwrap();
615 assert_eq!(time_micros, 52_200_500_000);
616 assert_eq!(offset, 0);
617
618 let (time_micros, offset) = parse_time_rfc3339("14:30:00.123456+05:30").unwrap();
619 assert_eq!(time_micros, 52_200_123_456);
620 assert_eq!(offset, 330);
621 }
622
623 #[test]
624 fn test_format_time() {
625 assert_eq!(format_time_rfc3339(0, 0), "00:00:00Z");
626 assert_eq!(format_time_rfc3339(52_200_000_000, 0), "14:30:00Z");
627 assert_eq!(format_time_rfc3339(52_200_500_000, 0), "14:30:00.5Z");
628 assert_eq!(format_time_rfc3339(52_200_123_456, 330), "14:30:00.123456+05:30");
629 }
630
631 #[test]
632 fn test_time_roundtrip() {
633 let times = [
634 "00:00:00Z",
635 "14:30:00Z",
636 "14:30:00.5Z",
637 "14:30:00.123456Z",
638 "23:59:59.999999Z",
639 "14:30:00+05:30",
640 "14:30:00-08:00",
641 ];
642
643 for time in times {
644 let (time_micros, offset) = parse_time_rfc3339(time).unwrap();
645 let formatted = format_time_rfc3339(time_micros, offset);
646 assert_eq!(time, formatted, "Roundtrip failed for {}", time);
647 }
648 }
649
650 #[test]
651 fn test_parse_datetime_basic() {
652 let (epoch_micros, offset) = parse_datetime_rfc3339("1970-01-01T00:00:00Z").unwrap();
653 assert_eq!(epoch_micros, 0);
654 assert_eq!(offset, 0);
655
656 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00Z").unwrap();
657 assert_eq!(epoch_micros, 1710513000000000);
658 assert_eq!(offset, 0);
659
660 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00.123456Z").unwrap();
661 assert_eq!(epoch_micros, 1710513000123456);
662 assert_eq!(offset, 0);
663 }
664
665 #[test]
666 fn test_format_datetime() {
667 assert_eq!(format_datetime_rfc3339(0, 0), "1970-01-01T00:00:00Z");
668 assert_eq!(
669 format_datetime_rfc3339(1710513000000000, 0),
670 "2024-03-15T14:30:00Z"
671 );
672 assert_eq!(
673 format_datetime_rfc3339(1710513000123456, 0),
674 "2024-03-15T14:30:00.123456Z"
675 );
676 }
677
678 #[test]
679 fn test_datetime_roundtrip() {
680 let datetimes = [
681 "1970-01-01T00:00:00Z",
682 "2024-03-15T14:30:00Z",
683 "2024-03-15T14:30:00.5Z",
684 "2024-03-15T14:30:00.123456Z",
685 "2024-12-31T23:59:59.999999Z",
686 ];
687
688 for datetime in datetimes {
689 let (epoch_micros, offset) = parse_datetime_rfc3339(datetime).unwrap();
690 let formatted = format_datetime_rfc3339(epoch_micros, offset);
691 assert_eq!(datetime, formatted, "Roundtrip failed for {}", datetime);
692 }
693 }
694
695 #[test]
696 fn test_datetime_with_offset() {
697 let (epoch_micros, offset) = parse_datetime_rfc3339("2024-03-15T14:30:00+05:30").unwrap();
699 assert_eq!(offset, 330);
700 let (utc_epoch_micros, _) = parse_datetime_rfc3339("2024-03-15T09:00:00Z").unwrap();
702 assert_eq!(epoch_micros, utc_epoch_micros);
703
704 let formatted = format_datetime_rfc3339(epoch_micros, offset);
706 assert_eq!(formatted, "2024-03-15T14:30:00+05:30");
707 }
708
709 #[test]
710 fn test_negative_epoch() {
711 let (epoch_micros, offset) = parse_datetime_rfc3339("1969-12-31T23:59:59Z").unwrap();
713 assert_eq!(epoch_micros, -1_000_000);
714 assert_eq!(offset, 0);
715
716 let formatted = format_datetime_rfc3339(epoch_micros, offset);
717 assert_eq!(formatted, "1969-12-31T23:59:59Z");
718 }
719
720 #[test]
721 fn test_invalid_dates() {
722 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());
727 }
728
729 #[test]
730 fn test_invalid_times() {
731 assert!(parse_time_rfc3339("00:00:00").is_err()); 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());
736 }
737
738 #[test]
739 fn test_invalid_datetimes() {
740 assert!(parse_datetime_rfc3339("2024-03-15T14:30:00").is_err()); assert!(parse_datetime_rfc3339("not-a-datetime").is_err());
742 }
743
744 #[test]
745 fn test_timezone_offset_edge_cases() {
746 assert!(parse_timezone_offset("+24:00").is_ok());
747 assert!(parse_timezone_offset("-24:00").is_ok());
748 assert!(parse_timezone_offset("+24:01").is_err()); assert!(parse_timezone_offset("-24:01").is_err()); }
751}