1use std::error::Error;
12use std::fmt::{self, Display};
13
14use jiff::Zoned;
15
16mod items;
17
18#[derive(Debug, PartialEq)]
19pub enum ParseDateTimeError {
20 InvalidInput,
21}
22
23impl Display for ParseDateTimeError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 ParseDateTimeError::InvalidInput => {
27 write!(
28 f,
29 "Invalid input string: cannot be parsed as a relative time"
30 )
31 }
32 }
33 }
34}
35
36impl Error for ParseDateTimeError {}
37
38impl From<items::error::Error> for ParseDateTimeError {
39 fn from(_: items::error::Error) -> Self {
40 ParseDateTimeError::InvalidInput
41 }
42}
43
44pub fn parse_datetime<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, ParseDateTimeError> {
73 items::parse_at_local(input).map_err(|e| e.into())
74}
75
76pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
110 date: Zoned,
111 input: S,
112) -> Result<Zoned, ParseDateTimeError> {
113 items::parse_at_date(date, input).map_err(|e| e.into())
114}
115
116#[cfg(test)]
117mod tests {
118 use jiff::{
119 civil::{date, time, Time, Weekday},
120 ToSpan, Zoned,
121 };
122
123 use crate::parse_datetime;
124
125 #[cfg(test)]
126 mod iso_8601 {
127 use crate::parse_datetime;
128
129 static TEST_TIME: i64 = 1613371067;
130
131 #[test]
132 fn test_t_sep() {
133 let dt = "2021-02-15T06:37:47 +0000";
134 let actual = parse_datetime(dt).unwrap();
135 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
136 }
137
138 #[test]
139 fn test_space_sep() {
140 let dt = "2021-02-15 06:37:47 +0000";
141 let actual = parse_datetime(dt).unwrap();
142 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
143 }
144
145 #[test]
146 fn test_space_sep_offset() {
147 let dt = "2021-02-14 22:37:47 -0800";
148 let actual = parse_datetime(dt).unwrap();
149 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
150 }
151
152 #[test]
153 fn test_t_sep_offset() {
154 let dt = "2021-02-14T22:37:47 -0800";
155 let actual = parse_datetime(dt).unwrap();
156 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
157 }
158
159 #[test]
160 fn test_t_sep_single_digit_offset_no_space() {
161 let dt = "2021-02-14T22:37:47-8";
162 let actual = parse_datetime(dt).unwrap();
163 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
164 }
165
166 #[test]
167 fn invalid_formats() {
168 let invalid_dts = vec![
169 "NotADate",
170 "202104",
171 "202104-12T22:37:47",
172 "a774e26sec", "12.", ];
175 for dt in invalid_dts {
176 assert!(
177 parse_datetime(dt).is_err(),
178 "Expected error for input: {}",
179 dt
180 );
181 }
182 }
183
184 #[test]
185 fn test_epoch_seconds() {
186 let dt = "@1613371067";
187 let actual = parse_datetime(dt).unwrap();
188 assert_eq!(actual.timestamp().as_second(), TEST_TIME);
189 }
190
191 }
199
200 #[cfg(test)]
201 mod calendar_date_items {
202 use jiff::{
203 civil::{date, time},
204 Zoned,
205 };
206
207 use crate::parse_datetime;
208
209 #[test]
210 fn single_digit_month_day() {
211 let expected = Zoned::now()
212 .with()
213 .date(date(1987, 5, 7))
214 .time(time(0, 0, 0, 0))
215 .build()
216 .unwrap();
217
218 assert_eq!(expected, parse_datetime("1987-05-07").unwrap());
219 assert_eq!(expected, parse_datetime("1987-5-07").unwrap());
220 assert_eq!(expected, parse_datetime("1987-05-7").unwrap());
221 assert_eq!(expected, parse_datetime("1987-5-7").unwrap());
222 assert_eq!(expected, parse_datetime("5/7/1987").unwrap());
223 assert_eq!(expected, parse_datetime("5/07/1987").unwrap());
224 assert_eq!(expected, parse_datetime("05/7/1987").unwrap());
225 assert_eq!(expected, parse_datetime("05/07/1987").unwrap());
226 }
227 }
228
229 #[cfg(test)]
230 mod offsets {
231 use jiff::{civil::DateTime, tz, Zoned};
232
233 use crate::parse_datetime;
234
235 #[test]
236 fn test_positive_offsets() {
237 let offsets = vec![
238 "UTC+07:00",
239 "UTC+0700",
240 "UTC+07",
241 "Z+07:00",
242 "Z+0700",
243 "Z+07",
244 ];
245
246 let expected = format!("{}{}", Zoned::now().strftime("%Y%m%d"), "0000+0700");
247 for offset in offsets {
248 let actual = parse_datetime(offset).unwrap();
249 assert_eq!(expected, actual.strftime("%Y%m%d%H%M%z").to_string());
250 }
251 }
252
253 #[test]
254 fn test_partial_offset() {
255 let offsets = vec!["UTC+00:15", "UTC+0015", "Z+00:15", "Z+0015"];
256 let expected = format!("{}{}", Zoned::now().strftime("%Y%m%d"), "0000+0015");
257 for offset in offsets {
258 let actual = parse_datetime(offset).unwrap();
259 assert_eq!(expected, actual.strftime("%Y%m%d%H%M%z").to_string());
260 }
261 }
262
263 #[test]
264 fn test_datetime_with_offset() {
265 let actual = parse_datetime("1997-01-19 08:17:48 +2").unwrap();
266 let expected = "1997-01-19 08:17:48"
267 .parse::<DateTime>()
268 .unwrap()
269 .to_zoned(tz::TimeZone::fixed(tz::offset(2)))
270 .unwrap();
271 assert_eq!(actual, expected);
272 }
273
274 #[test]
275 fn test_datetime_with_timezone() {
276 let actual = parse_datetime("1997-01-19 08:17:48 BRT").unwrap();
277 let expected = "1997-01-19 08:17:48"
278 .parse::<DateTime>()
279 .unwrap()
280 .to_zoned(tz::TimeZone::fixed(tz::offset(-3)))
281 .unwrap();
282 assert_eq!(actual, expected);
283 }
284
285 #[test]
286 fn offset_overflow() {
287 assert!(parse_datetime("m+25").is_err());
288 assert!(parse_datetime("24:00").is_err());
289 }
290 }
291
292 #[cfg(test)]
293 mod relative_time {
294 use crate::parse_datetime;
295
296 #[test]
297 fn test_positive_offsets() {
298 let relative_times = vec![
299 "today",
300 "yesterday",
301 "1 minute",
302 "3 hours",
303 "1 year 3 months",
304 ];
305
306 for relative_time in relative_times {
307 assert!(parse_datetime(relative_time).is_ok());
308 }
309 }
310 }
311
312 #[cfg(test)]
313 mod weekday {
314 use jiff::{civil::DateTime, tz::TimeZone, Zoned};
315
316 use crate::parse_datetime_at_date;
317
318 fn get_formatted_date(date: &Zoned, weekday: &str) -> String {
319 let result = parse_datetime_at_date(date.clone(), weekday).unwrap();
320
321 result.strftime("%F %T %9f").to_string()
322 }
323
324 #[test]
325 fn test_weekday() {
326 let date = "2023-02-28 10:12:03"
328 .parse::<DateTime>()
329 .unwrap()
330 .to_zoned(TimeZone::system())
331 .unwrap();
332
333 assert_eq!(
335 get_formatted_date(&date, "tuesday"),
336 "2023-02-28 00:00:00 000000000"
337 );
338
339 assert_eq!(
341 get_formatted_date(&date, "wed"),
342 "2023-03-01 00:00:00 000000000"
343 );
344
345 assert_eq!(
346 get_formatted_date(&date, "thu"),
347 "2023-03-02 00:00:00 000000000"
348 );
349
350 assert_eq!(
351 get_formatted_date(&date, "fri"),
352 "2023-03-03 00:00:00 000000000"
353 );
354
355 assert_eq!(
356 get_formatted_date(&date, "sat"),
357 "2023-03-04 00:00:00 000000000"
358 );
359
360 assert_eq!(
361 get_formatted_date(&date, "sun"),
362 "2023-03-05 00:00:00 000000000"
363 );
364 }
365 }
366
367 #[cfg(test)]
368 mod timestamp {
369 use jiff::Timestamp;
370
371 use crate::parse_datetime;
372
373 #[test]
374 fn test_positive_and_negative_offsets() {
375 let offsets: Vec<i64> = vec![
376 0, 1, 2, 10, 100, 150, 2000, 1234400000, 1334400000, 1692582913, 2092582910,
377 ];
378
379 for offset in offsets {
380 let time = Timestamp::from_second(offset).unwrap();
382 let dt = parse_datetime(format!("@{offset}")).unwrap();
383 assert_eq!(dt.timestamp(), time);
384
385 let time = Timestamp::from_second(-offset).unwrap();
387 let dt = parse_datetime(format!("@-{offset}")).unwrap();
388 assert_eq!(dt.timestamp(), time);
389 }
390 }
391 }
392
393 mod readme_test {
395 use jiff::{civil::DateTime, tz::TimeZone};
396
397 use crate::parse_datetime;
398
399 #[test]
400 fn test_readme_code() {
401 let dt = parse_datetime("2021-02-14 06:37:47").unwrap();
402 let expected = "2021-02-14 06:37:47"
403 .parse::<DateTime>()
404 .unwrap()
405 .to_zoned(TimeZone::system())
406 .unwrap();
407
408 assert_eq!(dt, expected);
409 }
410 }
411
412 mod invalid_test {
413 use crate::parse_datetime;
414 use crate::ParseDateTimeError;
415
416 #[test]
417 fn test_invalid_input() {
418 let result = parse_datetime("foobar");
419 assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
420
421 let result = parse_datetime("invalid 1");
422 assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
423 }
424 }
425
426 #[test]
427 fn test_datetime_ending_in_z() {
428 let actual = parse_datetime("2023-06-03 12:00:01Z").unwrap();
429 let expected = "2023-06-03 12:00:01[UTC]".parse::<Zoned>().unwrap();
430 assert_eq!(actual, expected);
431 }
432
433 #[test]
434 fn test_parse_invalid_datetime() {
435 assert!(crate::parse_datetime("bogus +1 day").is_err());
436 }
437
438 #[test]
439 fn test_parse_invalid_delta() {
440 assert!(crate::parse_datetime("1997-01-01 bogus").is_err());
441 }
442
443 #[test]
444 fn test_parse_datetime_tz_nodelta() {
445 let expected = "1997-01-01 00:00:00[UTC]".parse::<Zoned>().unwrap();
447
448 for s in [
449 "1997-01-01 00:00:00 +0000",
450 "1997-01-01 00:00:00 +00",
451 "1997-01-01 00:00 +0000",
452 "1997-01-01 00:00:00 +0000",
453 "1997-01-01T00:00:00+0000",
454 "1997-01-01T00:00:00+00",
455 "1997-01-01T00:00:00Z",
456 "@852076800",
457 ] {
458 let actual = crate::parse_datetime(s).unwrap();
459 assert_eq!(actual, expected);
460 }
461 }
462
463 #[test]
464 fn test_parse_datetime_notz_nodelta() {
465 let expected = Zoned::now()
466 .with()
467 .date(date(1997, 1, 1))
468 .time(time(0, 0, 0, 0))
469 .build()
470 .unwrap();
471
472 for s in [
473 "1997-01-01 00:00:00.000000000",
474 "Wed Jan 1 00:00:00 1997",
475 "1997-01-01T00:00:00",
476 "1997-01-01 00:00:00",
477 "1997-01-01 00:00",
478 ] {
479 let actual = crate::parse_datetime(s).unwrap();
480 assert_eq!(actual, expected);
481 }
482 }
483
484 #[test]
485 fn test_parse_date_notz_nodelta() {
486 let expected = Zoned::now()
487 .with()
488 .date(date(1997, 1, 1))
489 .time(time(0, 0, 0, 0))
490 .build()
491 .unwrap();
492
493 for s in ["1997-01-01", "19970101", "01/01/1997", "01/01/97"] {
494 let actual = crate::parse_datetime(s).unwrap();
495 assert_eq!(actual, expected);
496 }
497 }
498
499 #[test]
500 fn test_parse_datetime_tz_delta() {
501 let expected = "1998-01-01 00:00:00[UTC]".parse::<Zoned>().unwrap();
503
504 for s in [
505 "1997-01-01 00:00:00 +0000 +1 year",
506 "1997-01-01 00:00:00 +00 +1 year",
507 "1997-01-01T00:00:00Z +1 year",
508 "1997-01-01 00:00 +0000 +1 year",
509 "1997-01-01 00:00:00 +0000 +1 year",
510 "1997-01-01T00:00:00+0000 +1 year",
511 "1997-01-01T00:00:00+00 +1 year",
512 ] {
513 let actual = crate::parse_datetime(s).unwrap();
514 assert_eq!(actual, expected);
515 }
516 }
517
518 #[test]
519 fn test_parse_datetime_notz_delta() {
520 let expected = Zoned::now()
521 .with()
522 .date(date(1998, 1, 1))
523 .time(time(0, 0, 0, 0))
524 .build()
525 .unwrap();
526
527 for s in [
528 "1997-01-01 00:00:00.000000000 1 year",
529 "Wed Jan 1 00:00:00 1997 1 year",
530 "1997-01-01T00:00:00 1 year",
531 "1997-01-01 00:00:00 1 year",
532 "1997-01-01 00:00 1 year",
533 ] {
534 let actual = crate::parse_datetime(s).unwrap();
535 assert_eq!(actual, expected);
536 }
537 }
538
539 #[test]
540 fn test_parse_invalid_datetime_notz_delta() {
541 for s in ["199701010000.00 +1 year", "199701010000 +1 year"] {
543 assert!(crate::parse_datetime(s).is_err());
544 }
545 }
546
547 #[test]
548 fn test_parse_date_notz_delta() {
549 let expected = Zoned::now()
550 .with()
551 .date(date(1998, 1, 1))
552 .time(time(0, 0, 0, 0))
553 .build()
554 .unwrap();
555
556 for s in [
557 "1997-01-01 +1 year",
558 "19970101 +1 year",
559 "01/01/1997 +1 year",
560 "01/01/97 +1 year",
561 ] {
562 let actual = crate::parse_datetime(s).unwrap();
563 assert_eq!(actual, expected);
564 }
565 }
566
567 #[test]
568 fn test_weekday_only() {
569 let now = Zoned::now();
570 let midnight = Time::new(0, 0, 0, 0).unwrap();
571 let today = now.weekday();
572 let midnight_today = now.with().time(midnight).build().unwrap();
573
574 for (s, day) in [
575 ("sunday", Weekday::Sunday),
576 ("monday", Weekday::Monday),
577 ("tuesday", Weekday::Tuesday),
578 ("wednesday", Weekday::Wednesday),
579 ("thursday", Weekday::Thursday),
580 ("friday", Weekday::Friday),
581 ("saturday", Weekday::Saturday),
582 ] {
583 let actual = parse_datetime(s).unwrap();
584 let delta = day.since(today);
585 let expected = midnight_today.checked_add(delta.days()).unwrap();
586 assert_eq!(actual, expected);
587 }
588 }
589
590 mod test_relative {
591 use crate::parse_datetime;
592
593 #[test]
594 fn test_month() {
595 assert_eq!(
596 parse_datetime("28 feb + 1 month")
597 .expect("parse_datetime")
598 .strftime("%m%d")
599 .to_string(),
600 "0328"
601 );
602
603 assert!(parse_datetime("29 feb + 1 year").is_err());
605
606 assert!(parse_datetime("29 feb 2025").is_err());
608
609 assert!(parse_datetime("29 feb 2025 + 1 day").is_err());
612
613 assert_eq!(
615 parse_datetime("28 feb 2023 + 1 day")
616 .unwrap()
617 .strftime("%m%d")
618 .to_string(),
619 "0301"
620 );
621 }
622
623 #[test]
624 fn month_overflow() {
625 assert_eq!(
626 parse_datetime("2024-01-31 + 1 month")
627 .unwrap()
628 .strftime("%Y-%m-%dT%H:%M:%S")
629 .to_string(),
630 "2024-03-02T00:00:00",
631 );
632
633 assert_eq!(
634 parse_datetime("2024-02-29 + 1 month")
635 .unwrap()
636 .strftime("%Y-%m-%dT%H:%M:%S")
637 .to_string(),
638 "2024-03-29T00:00:00",
639 );
640 }
641 }
642
643 mod test_gnu {
644 use crate::parse_datetime;
645
646 #[test]
647 fn gnu_compat() {
648 const FMT: &str = "%Y-%m-%d %H:%M:%S";
649 let input = "0000-03-02 00:00:00";
650 assert_eq!(
651 input,
652 parse_datetime(input).unwrap().strftime(FMT).to_string()
653 );
654
655 let input = "2621-03-10 00:00:00";
656 assert_eq!(
657 input,
658 parse_datetime(input).unwrap().strftime(FMT).to_string()
659 );
660
661 let input = "1038-03-10 00:00:00";
662 assert_eq!(
663 input,
664 parse_datetime(input).unwrap().strftime(FMT).to_string()
665 );
666 }
667 }
668}