1use crate::error::DateTimeSyntaxError;
10#[cfg(not(feature = "chrono"))]
11use crate::utils::parse_date_time_bytes;
12#[cfg(not(feature = "chrono"))]
13use std::fmt::Display;
14
15#[cfg(feature = "chrono")]
16#[macro_export]
82macro_rules! date_time {
83 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal) => {{
84 const D: chrono::NaiveDate = date_time!(@INTERNAL @DATE $Y-$M-$D);
85 const T: chrono::NaiveTime = date_time!(@INTERNAL @TIME $h:$m:$s);
86 D.and_time(T).and_utc().fixed_offset()
87 }};
88 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal $x:literal:$y:literal) => {{
89 const D: chrono::NaiveDate = date_time!(@INTERNAL @DATE $Y-$M-$D);
90 const T: chrono::NaiveTime = date_time!(@INTERNAL @TIME $h:$m:$s);
91 const TZ: chrono::FixedOffset = date_time!(@INTERNAL @TIMEZONE $x:$y);
92 D.and_time(T).and_local_timezone(TZ).earliest().unwrap()
94 }};
95 (@INTERNAL @DATE $Y:literal-$M:literal-$D:literal) => {{
96 const D: Option<chrono::NaiveDate> = chrono::NaiveDate::from_ymd_opt($Y, $M, $D);
97 const _: () = assert!(D.is_some(), "Invalid date");
98 D.unwrap()
99 }};
100 (@INTERNAL @TIME $h:literal:$m:literal:$s:literal) => {{
101 const _: () = assert!($s >= 0.0, "Seconds must be positive");
102 const S: u32 = $s as u32;
103 const MS: u32 = (($s * 1000.0 as f64).round() % 1000.0) as u32;
104 const T: Option<chrono::NaiveTime> = chrono::NaiveTime::from_hms_milli_opt($h, $m, S, MS);
105 const _: () = assert!(T.is_some(), "Invalid time");
106 T.unwrap()
107 }};
108 (@INTERNAL @TIMEZONE $x:literal:$y:literal) => {{
109 const _: () = assert!($y >= 0, "Minutes must be positive");
110 const TZ_H: i32 = ($x as i32).abs() as i32;
111 const TZ_M: i32 = $y as i32;
112 const MULTIPLIER: i32 = if $x == TZ_H { 1 } else { -1 };
113 const TZ: Option<chrono::FixedOffset> =
114 chrono::FixedOffset::east_opt(MULTIPLIER * ((TZ_H * 3600) + (TZ_M * 60)));
115 const _: () = assert!(TZ.is_some(), "Invalid timezone offset");
116 TZ.unwrap()
117 }};
118}
119#[cfg(not(feature = "chrono"))]
120#[macro_export]
202macro_rules! date_time {
203 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal) => {
204 date_time!($Y-$M-$D T $h:$m:$s 0:0)
205 };
206 ($Y:literal-$M:literal-$D:literal T $h:literal:$m:literal:$s:literal $x:literal:$y:literal) => {{
207 const _: () = assert!($Y <= 9999, "Year must be at most 4 digits");
208 const _: () = assert!($M > 0, "Month must be greater than 0");
209 const _: () = assert!($M <= 12, "Month must be less than or equal to 12");
210 const _: () = assert!($D > 0, "Day must be greater than 0");
211 const _: () = assert!($D <= 31, "Day must be less than or equal to 31");
212 const _: () = assert!($h < 24, "Hour must be less than 24");
213 const _: () = assert!($m < 60, "Minute must be less than 60");
214 const _: () = assert!($s >= 0.0, "Seconds must be positive");
215 const _: () = assert!($s < 60.0, "Seconds must be less than 60.0");
216 const _: () = assert!($x > -24, "Hour offset must be greater than -24");
217 const _: () = assert!($x < 24, "Hour offset must be less than 24");
218 const _: () = assert!($y < 60, "Minute offset must be less than 60");
219 $crate::date::DateTime {
220 date_fullyear: $Y,
221 date_month: $M,
222 date_mday: $D,
223 time_hour: $h,
224 time_minute: $m,
225 time_second: $s,
226 timezone_offset: $crate::date::DateTimeTimezoneOffset {
227 time_hour: $x,
228 time_minute: $y,
229 },
230 }
231 }};
232}
233
234#[cfg(not(feature = "chrono"))]
235#[derive(Debug, PartialEq, Clone, Copy)]
239pub struct DateTime {
240 pub date_fullyear: u32,
242 pub date_month: u8,
244 pub date_mday: u8,
246 pub time_hour: u8,
248 pub time_minute: u8,
250 pub time_second: f64,
256 pub timezone_offset: DateTimeTimezoneOffset,
258}
259
260#[cfg(not(feature = "chrono"))]
261impl Display for DateTime {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(
264 f,
265 "{:04}-{:02}-{:02}T{:02}:{:02}:{:06.3}{}",
266 self.date_fullyear,
267 self.date_month,
268 self.date_mday,
269 self.time_hour,
270 self.time_minute,
271 self.time_second,
272 self.timezone_offset
273 )
274 }
275}
276
277#[cfg(not(feature = "chrono"))]
278impl From<DateTime> for String {
279 fn from(value: DateTime) -> Self {
280 format!("{value}")
281 }
282}
283
284#[cfg(not(feature = "chrono"))]
285impl Default for DateTime {
286 fn default() -> Self {
287 Self {
288 date_fullyear: 1970,
289 date_month: 1,
290 date_mday: 1,
291 time_hour: 0,
292 time_minute: 0,
293 time_second: 0.0,
294 timezone_offset: Default::default(),
295 }
296 }
297}
298
299#[cfg(not(feature = "chrono"))]
300#[derive(Debug, PartialEq, Clone, Copy, Default)]
302pub struct DateTimeTimezoneOffset {
303 pub time_hour: i8,
305 pub time_minute: u8,
307}
308
309#[cfg(not(feature = "chrono"))]
310impl Display for DateTimeTimezoneOffset {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 if self.time_hour == 0 && self.time_minute == 0 {
313 write!(f, "Z")
314 } else {
315 write!(f, "{:+03}:{:02}", self.time_hour, self.time_minute)
316 }
317 }
318}
319
320#[cfg(not(feature = "chrono"))]
321impl From<DateTimeTimezoneOffset> for String {
322 fn from(value: DateTimeTimezoneOffset) -> Self {
323 format!("{value}")
324 }
325}
326
327#[cfg(feature = "chrono")]
328pub fn parse(input: &str) -> Result<chrono::DateTime<chrono::FixedOffset>, DateTimeSyntaxError> {
330 chrono::DateTime::parse_from_rfc3339(input).map_err(DateTimeSyntaxError::from)
331}
332#[cfg(not(feature = "chrono"))]
333pub fn parse(input: &str) -> Result<DateTime, DateTimeSyntaxError> {
335 parse_bytes(input.as_bytes())
336}
337
338#[cfg(feature = "chrono")]
339pub fn parse_bytes(
341 input: &[u8],
342) -> Result<chrono::DateTime<chrono::FixedOffset>, DateTimeSyntaxError> {
343 let input_str = str::from_utf8(input)?;
344 parse(input_str)
345}
346#[cfg(not(feature = "chrono"))]
347pub fn parse_bytes(input: &[u8]) -> Result<DateTime, DateTimeSyntaxError> {
349 Ok(parse_date_time_bytes(input)?.parsed)
350}
351
352#[cfg(feature = "chrono")]
353pub fn string_from(date_time: &chrono::DateTime<chrono::FixedOffset>) -> String {
355 let dt = date_time.naive_local();
356 let date = dt.date();
357 let time = dt.time();
358 let offset = date_time.offset();
359 if offset.local_minus_utc() == 0 {
360 format!("{date}T{time}Z")
361 } else {
362 format!("{date}T{time}{offset}")
363 }
364}
365#[cfg(not(feature = "chrono"))]
366pub fn string_from(date_time: &DateTime) -> String {
368 format!("{date_time}")
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use pretty_assertions::assert_eq;
375
376 #[test]
377 fn no_timezone() {
378 assert_eq!(
379 date_time!(2025-06-04 T 13:50:42.148),
380 parse("2025-06-04T13:50:42.148Z").unwrap()
381 );
382 }
383
384 #[test]
385 fn plus_timezone() {
386 assert_eq!(
387 date_time!(2025-06-04 T 13:50:42.148 03:00),
388 parse("2025-06-04T13:50:42.148+03:00").unwrap()
389 );
390 }
391
392 #[test]
393 fn negative_timezone() {
394 assert_eq!(
395 date_time!(2025-06-04 T 13:50:42.148 -01:30),
396 parse("2025-06-04T13:50:42.148-01:30").unwrap()
397 );
398 }
399
400 #[test]
401 fn no_fractional_seconds() {
402 assert_eq!(
403 date_time!(2025-06-04 T 13:50:42.0),
404 parse("2025-06-04T13:50:42Z").unwrap()
405 );
406 }
407
408 #[test]
409 fn string_from_single_digit_dates_should_be_valid() {
410 assert_eq!(
411 String::from("2025-06-04T13:50:42.123Z"),
412 string_from(&date_time!(2025-06-04 T 13:50:42.123))
413 )
414 }
415
416 #[ignore = "change to chrono breaks test but maybe the expectation is wrong anyway"]
417 #[test]
418 fn string_from_no_fractional_seconds_should_still_be_3_decimals_precise() {
419 assert_eq!(
420 String::from("2025-06-04T13:50:42.000Z"),
421 string_from(&date_time!(2025-06-04 T 13:50:42.0))
422 )
423 }
424
425 #[test]
426 fn string_from_single_digit_times_should_be_valid() {
427 assert_eq!(
428 String::from("2025-12-25T04:00:02.001Z"),
429 string_from(&date_time!(2025-12-25 T 04:00:02.001))
430 )
431 }
432
433 #[test]
434 fn string_from_negative_time_offset_should_be_valid() {
435 assert_eq!(
436 String::from("2025-06-04T13:50:42.123-05:00"),
437 string_from(&date_time!(2025-06-04 T 13:50:42.123 -05:00))
438 )
439 }
440
441 #[test]
442 fn string_from_positive_offset_should_be_valid() {
443 assert_eq!(
444 String::from("2025-06-04T13:50:42.100+01:00"),
445 string_from(&date_time!(2025-06-04 T 13:50:42.100 01:00))
446 )
447 }
448
449 #[test]
450 fn string_from_positive_offset_non_zero_minutes_should_be_valid() {
451 assert_eq!(
452 String::from("2025-06-04T13:50:42.010+06:30"),
453 string_from(&date_time!(2025-06-04 T 13:50:42.010 06:30))
454 )
455 }
456
457 #[cfg(not(feature = "chrono"))]
458 #[test]
459 fn date_time_macro_should_work_with_no_offset() {
460 assert_eq!(
461 date_time!(2025-06-22 T 22:13:42.000),
462 DateTime {
463 date_fullyear: 2025,
464 date_month: 6,
465 date_mday: 22,
466 time_hour: 22,
467 time_minute: 13,
468 time_second: 42.0,
469 timezone_offset: DateTimeTimezoneOffset {
470 time_hour: 0,
471 time_minute: 0
472 }
473 }
474 );
475 }
476
477 #[cfg(not(feature = "chrono"))]
478 #[test]
479 fn date_time_macro_should_work_with_positive_offset() {
480 assert_eq!(
481 date_time!(2025-06-22 T 22:13:42.000 01:00),
482 DateTime {
483 date_fullyear: 2025,
484 date_month: 6,
485 date_mday: 22,
486 time_hour: 22,
487 time_minute: 13,
488 time_second: 42.0,
489 timezone_offset: DateTimeTimezoneOffset {
490 time_hour: 1,
491 time_minute: 0
492 }
493 }
494 );
495 }
496
497 #[cfg(not(feature = "chrono"))]
498 #[test]
499 fn date_time_macro_should_work_with_negative_offset() {
500 assert_eq!(
501 date_time!(2025-06-22 T 22:13:42.000 -01:30),
502 DateTime {
503 date_fullyear: 2025,
504 date_month: 6,
505 date_mday: 22,
506 time_hour: 22,
507 time_minute: 13,
508 time_second: 42.0,
509 timezone_offset: DateTimeTimezoneOffset {
510 time_hour: -1,
511 time_minute: 30
512 }
513 }
514 );
515 }
516}