1use crate::{error::DateTimeSyntaxError, utils::parse_date_time_bytes};
10use std::fmt::Display;
11
12#[macro_export]
94macro_rules! date_time {
95 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal) => {
96 date_time!($Y-$M-$D T $h:$m:$s 0:0)
97 };
98 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal $x:literal:$y:literal) => {{
99 const _: () = assert!($Y <= 9999, "Year must be at most 4 digits");
100 const _: () = assert!($M > 0, "Month must be greater than 0");
101 const _: () = assert!($M <= 12, "Month must be less than or equal to 12");
102 const _: () = assert!($D > 0, "Day must be greater than 0");
103 const _: () = assert!($D <= 31, "Day must be less than or equal to 31");
104 const _: () = assert!($h < 24, "Hour must be less than 24");
105 const _: () = assert!($m < 60, "Minute must be less than 60");
106 const _: () = assert!($s >= 0.0, "Seconds must be positive");
107 const _: () = assert!($s < 60.0, "Seconds must be less than 60.0");
108 const _: () = assert!($x > -24, "Hour offset must be greater than -24");
109 const _: () = assert!($x < 24, "Hour offset must be less than 24");
110 const _: () = assert!($y < 60, "Minute offset must be less than 60");
111 $crate::date::DateTime {
112 date_fullyear: $Y,
113 date_month: $M,
114 date_mday: $D,
115 time_hour: $h,
116 time_minute: $m,
117 time_second: $s,
118 timezone_offset: $crate::date::DateTimeTimezoneOffset {
119 time_hour: $x,
120 time_minute: $y,
121 },
122 }
123 }};
124}
125
126#[derive(Debug, PartialEq, Clone, Copy)]
130pub struct DateTime {
131 pub date_fullyear: u32,
133 pub date_month: u8,
135 pub date_mday: u8,
137 pub time_hour: u8,
139 pub time_minute: u8,
141 pub time_second: f64,
147 pub timezone_offset: DateTimeTimezoneOffset,
149}
150
151impl Display for DateTime {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(
154 f,
155 "{:04}-{:02}-{:02}T{:02}:{:02}:{:06.3}{}",
156 self.date_fullyear,
157 self.date_month,
158 self.date_mday,
159 self.time_hour,
160 self.time_minute,
161 self.time_second,
162 self.timezone_offset
163 )
164 }
165}
166
167impl From<DateTime> for String {
168 fn from(value: DateTime) -> Self {
169 format!("{value}")
170 }
171}
172
173impl Default for DateTime {
174 fn default() -> Self {
175 Self {
176 date_fullyear: 1970,
177 date_month: 1,
178 date_mday: 1,
179 time_hour: 0,
180 time_minute: 0,
181 time_second: 0.0,
182 timezone_offset: Default::default(),
183 }
184 }
185}
186
187#[derive(Debug, PartialEq, Clone, Copy, Default)]
189pub struct DateTimeTimezoneOffset {
190 pub time_hour: i8,
192 pub time_minute: u8,
194}
195
196impl Display for DateTimeTimezoneOffset {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 if self.time_hour == 0 && self.time_minute == 0 {
199 write!(f, "Z")
200 } else {
201 write!(f, "{:+03}:{:02}", self.time_hour, self.time_minute)
202 }
203 }
204}
205
206impl From<DateTimeTimezoneOffset> for String {
207 fn from(value: DateTimeTimezoneOffset) -> Self {
208 format!("{value}")
209 }
210}
211
212pub fn parse(input: &str) -> Result<DateTime, DateTimeSyntaxError> {
214 parse_bytes(input.as_bytes())
215}
216
217pub fn parse_bytes(input: &[u8]) -> Result<DateTime, DateTimeSyntaxError> {
219 Ok(parse_date_time_bytes(input)?.parsed)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use pretty_assertions::assert_eq;
226
227 #[test]
228 fn no_timezone() {
229 assert_eq!(
230 date_time!(2025-06-04 T 13:50:42.148),
231 parse("2025-06-04T13:50:42.148Z").unwrap()
232 );
233 }
234
235 #[test]
236 fn plus_timezone() {
237 assert_eq!(
238 date_time!(2025-06-04 T 13:50:42.148 03:00),
239 parse("2025-06-04T13:50:42.148+03:00").unwrap()
240 );
241 }
242
243 #[test]
244 fn negative_timezone() {
245 assert_eq!(
246 date_time!(2025-06-04 T 13:50:42.148 -01:30),
247 parse("2025-06-04T13:50:42.148-01:30").unwrap()
248 );
249 }
250
251 #[test]
252 fn no_fractional_seconds() {
253 assert_eq!(
254 date_time!(2025-06-04 T 13:50:42.0),
255 parse("2025-06-04T13:50:42Z").unwrap()
256 );
257 }
258
259 #[test]
260 fn string_from_single_digit_dates_should_be_valid() {
261 assert_eq!(
262 String::from("2025-06-04T13:50:42.123Z"),
263 String::from(date_time!(2025-06-04 T 13:50:42.123))
264 )
265 }
266
267 #[test]
268 fn string_from_no_fractional_seconds_should_still_be_3_decimals_precise() {
269 assert_eq!(
270 String::from("2025-06-04T13:50:42.000Z"),
271 String::from(date_time!(2025-06-04 T 13:50:42.0))
272 )
273 }
274
275 #[test]
276 fn string_from_single_digit_times_should_be_valid() {
277 assert_eq!(
278 String::from("2025-12-25T04:00:02.000Z"),
279 String::from(date_time!(2025-12-25 T 04:00:02.000))
280 )
281 }
282
283 #[test]
284 fn string_from_negative_time_offset_should_be_valid() {
285 assert_eq!(
286 String::from("2025-06-04T13:50:42.123-05:00"),
287 String::from(date_time!(2025-06-04 T 13:50:42.123 -05:00))
288 )
289 }
290
291 #[test]
292 fn string_from_positive_offset_should_be_valid() {
293 assert_eq!(
294 String::from("2025-06-04T13:50:42.100+01:00"),
295 String::from(date_time!(2025-06-04 T 13:50:42.100 01:00))
296 )
297 }
298
299 #[test]
300 fn string_from_positive_offset_non_zero_minutes_should_be_valid() {
301 assert_eq!(
302 String::from("2025-06-04T13:50:42.010+06:30"),
303 String::from(date_time!(2025-06-04 T 13:50:42.010 06:30))
304 )
305 }
306
307 #[test]
308 fn date_time_macro_should_work_with_no_offset() {
309 assert_eq!(
310 date_time!(2025-06-22 T 22:13:42.000),
311 DateTime {
312 date_fullyear: 2025,
313 date_month: 6,
314 date_mday: 22,
315 time_hour: 22,
316 time_minute: 13,
317 time_second: 42.0,
318 timezone_offset: DateTimeTimezoneOffset {
319 time_hour: 0,
320 time_minute: 0
321 }
322 }
323 );
324 }
325
326 #[test]
327 fn date_time_macro_should_work_with_positive_offset() {
328 assert_eq!(
329 date_time!(2025-06-22 T 22:13:42.000 01:00),
330 DateTime {
331 date_fullyear: 2025,
332 date_month: 6,
333 date_mday: 22,
334 time_hour: 22,
335 time_minute: 13,
336 time_second: 42.0,
337 timezone_offset: DateTimeTimezoneOffset {
338 time_hour: 1,
339 time_minute: 0
340 }
341 }
342 );
343 }
344
345 #[test]
346 fn date_time_macro_should_work_with_negative_offset() {
347 assert_eq!(
348 date_time!(2025-06-22 T 22:13:42.000 -01:30),
349 DateTime {
350 date_fullyear: 2025,
351 date_month: 6,
352 date_mday: 22,
353 time_hour: 22,
354 time_minute: 13,
355 time_second: 42.0,
356 timezone_offset: DateTimeTimezoneOffset {
357 time_hour: -1,
358 time_minute: 30
359 }
360 }
361 );
362 }
363}