1pub mod approxidate;
11
12pub fn is_leap_year(year: i64) -> bool {
15 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
16}
17
18pub fn days_in_month(year: i64, month: u32) -> u32 {
20 match month {
21 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
22 4 | 6 | 9 | 11 => 30,
23 2 if is_leap_year(year) => 29,
24 2 => 28,
25 _ => 0,
26 }
27}
28
29pub fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
33 let year = year - i64::from(month <= 2);
34 let era = if year >= 0 { year } else { year - 399 } / 400;
35 let year_of_era = year - era * 400;
36 let month = i64::from(month);
37 let day = i64::from(day);
38 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1;
39 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
40 era * 146_097 + day_of_era - 719_468
41}
42
43pub fn civil_from_days(days: i64) -> (i64, u32, u32) {
45 let z = days + 719_468;
46 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
47 let day_of_era = z - era * 146_097;
48 let year_of_era =
49 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
50 let year = year_of_era + era * 400;
51 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
52 let month_prime = (5 * day_of_year + 2) / 153;
53 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
54 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
55 let year = year + i64::from(month <= 2);
56 (year, month as u32, day as u32)
57}
58
59pub fn parse_date_ymd(value: &str) -> Option<(i64, u32, u32)> {
62 let mut parts = value.split('-');
63 let year = parts.next()?.parse::<i64>().ok()?;
64 let month = parts.next()?.parse::<u32>().ok()?;
65 let day = parts.next()?.parse::<u32>().ok()?;
66 if parts.next().is_some() || !(1..=12).contains(&month) {
67 return None;
68 }
69 let max_day = days_in_month(year, month);
70 if !(1..=max_day).contains(&day) {
71 return None;
72 }
73 Some((year, month, day))
74}
75
76pub fn parse_time_hms(value: &str) -> Option<(u32, u32, u32)> {
80 let mut parts = value.split(':');
81 let hour = parts.next()?.parse::<u32>().ok()?;
82 let minute = parts.next()?.parse::<u32>().ok()?;
83 let second = parts.next()?.parse::<u32>().ok()?;
84 if parts.next().is_some() || hour > 23 || minute > 59 || second > 59 {
85 return None;
86 }
87 Some((hour, minute, second))
88}
89
90pub fn parse_tz_offset(value: &str) -> Option<i64> {
95 let bytes = value.as_bytes();
96 if bytes.len() != 5
97 || !matches!(bytes.first(), Some(b'+' | b'-'))
98 || !bytes[1..].iter().all(|byte| byte.is_ascii_digit())
99 {
100 return None;
101 }
102 let hours = value[1..3].parse::<i64>().ok()?;
103 let minutes = value[3..5].parse::<i64>().ok()?;
104 if hours > 23 || minutes > 59 {
105 return None;
106 }
107 let offset = hours * 3_600 + minutes * 60;
108 if bytes[0] == b'-' {
109 Some(-offset)
110 } else {
111 Some(offset)
112 }
113}
114
115pub fn split_embedded_timezone(rest: &str) -> (&str, Option<&str>) {
120 if let Some(time) = rest.strip_suffix('Z') {
121 return (time, Some("+0000"));
122 }
123 let bytes = rest.as_bytes();
124 if bytes.len() >= 5 {
125 let tz_start = bytes.len() - 5;
126 if matches!(bytes[tz_start], b'+' | b'-')
127 && bytes[tz_start + 1..]
128 .iter()
129 .all(|byte| byte.is_ascii_digit())
130 {
131 return (&rest[..tz_start], Some(&rest[tz_start..]));
132 }
133 }
134 (rest, None)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn civil_round_trip() {
143 assert_eq!(days_from_civil(1970, 1, 1), 0);
144 assert_eq!(civil_from_days(0), (1970, 1, 1));
145 assert_eq!(days_from_civil(2000, 3, 1), 11017);
146 assert_eq!(civil_from_days(11_017), (2000, 3, 1));
147 assert_eq!(days_in_month(2000, 2), 29);
149 assert_eq!(days_in_month(1900, 2), 28);
150 assert_eq!(days_in_month(2024, 2), 29);
151 assert_eq!(days_in_month(2023, 13), 0);
152 assert!(!is_leap_year(1900));
153 assert!(is_leap_year(2000));
154 }
155
156 #[test]
157 fn ymd_and_time_validation() {
158 assert_eq!(parse_date_ymd("2024-02-29"), Some((2024, 2, 29)));
159 assert_eq!(parse_date_ymd("2023-02-29"), None);
160 assert_eq!(parse_date_ymd("2024-13-01"), None);
161 assert_eq!(parse_date_ymd("2024-01-02T00:00:00"), None);
162 assert_eq!(parse_time_hms("23:59:59"), Some((23, 59, 59)));
163 assert_eq!(parse_time_hms("24:00:00"), None);
164 assert_eq!(parse_time_hms("12:60:00"), None);
165 assert_eq!(parse_time_hms("12:00"), None);
166 }
167
168 #[test]
169 fn tz_offsets() {
170 assert_eq!(parse_tz_offset("+0000"), Some(0));
171 assert_eq!(parse_tz_offset("-0530"), Some(-19_800));
172 assert_eq!(parse_tz_offset("+2400"), None);
173 assert_eq!(parse_tz_offset("+0060"), None);
174 assert_eq!(parse_tz_offset("+000a"), None);
175 assert_eq!(parse_tz_offset("+000"), None);
176 }
177
178 #[test]
179 fn embedded_timezone_suffixes() {
180 assert_eq!(split_embedded_timezone("00:00:01Z"), ("00:00:01", Some("+0000")));
181 assert_eq!(
182 split_embedded_timezone("03:04:05+0100"),
183 ("03:04:05", Some("+0100"))
184 );
185 assert_eq!(split_embedded_timezone("03:04:05"), ("03:04:05", None));
186 }
187}