1use winnow::ascii::dec_int;
11use winnow::combinator::{alt, opt};
12use winnow::error::{ContextError, ErrMode};
13use winnow::prelude::*;
14use winnow::token::take;
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, PartialEq, Eq, Clone)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22pub enum Edtf {
23 Date(Date),
25 Interval(Interval),
27 IntervalFrom(Date),
29 IntervalTo(Date),
31}
32
33impl Edtf {
34 pub fn year(&self) -> i64 {
36 match self {
37 Self::Date(date) => date.year.value,
38 Self::Interval(interval) => interval.start.year.value,
39 Self::IntervalFrom(date) => date.year.value,
40 Self::IntervalTo(date) => date.year.value,
41 }
42 }
43
44 pub fn month(&self) -> Option<u32> {
46 let m_opt = match self {
47 Self::Date(date) => date.month_or_season,
48 Self::Interval(interval) => interval.start.month_or_season,
49 Self::IntervalFrom(date) => date.month_or_season,
50 Self::IntervalTo(date) => date.month_or_season,
51 };
52 match m_opt {
53 Some(MonthOrSeason::Month(m)) => Some(m),
54 _ => None,
55 }
56 }
57
58 pub fn day(&self) -> Option<u32> {
60 let d_opt = match self {
61 Self::Date(date) => date.day,
62 Self::Interval(interval) => interval.start.day,
63 Self::IntervalFrom(date) => date.day,
64 Self::IntervalTo(date) => date.day,
65 };
66 match d_opt {
67 Some(Day::Day(d)) => Some(d),
68 _ => None,
69 }
70 }
71
72 pub fn is_range(&self) -> bool {
74 matches!(
75 self,
76 Self::Interval(_) | Self::IntervalFrom(_) | Self::IntervalTo(_)
77 )
78 }
79
80 pub fn is_open_range(&self) -> bool {
82 matches!(self, Self::IntervalFrom(_))
83 }
84
85 pub fn time(&self) -> Option<Time> {
87 match self {
88 Self::Date(date) => date.time,
89 _ => None,
90 }
91 }
92
93 pub fn has_time(&self) -> bool {
95 self.time().is_some()
96 }
97}
98
99#[derive(Debug, PartialEq, Eq, Clone)]
101#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
102pub struct Interval {
103 pub start: Date,
105 pub end: Date,
107}
108
109#[derive(Debug, PartialEq, Eq, Clone, Copy)]
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112pub enum MonthOrSeason {
113 Month(u32),
115 Unspecified,
117 Spring,
119 Summer,
121 Autumn,
123 Winter,
125}
126
127#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
129#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
130pub struct Quality {
131 pub uncertain: bool,
133 pub approximate: bool,
135}
136
137#[derive(Debug, PartialEq, Eq, Clone, Copy)]
139#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
140pub struct Year {
141 pub value: i64,
143 pub unspecified: UnspecifiedYear,
145}
146
147#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
149#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
150pub enum UnspecifiedYear {
151 #[default]
152 None,
154 One,
156 Two,
158 Three,
160 Four,
162}
163
164#[derive(Debug, PartialEq, Eq, Clone, Copy)]
166#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167pub enum Day {
168 Day(u32),
170 Unspecified,
172}
173
174#[derive(Debug, PartialEq, Eq, Clone)]
176#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
177pub struct Date {
178 pub year: Year,
180 pub year_quality: Quality,
182 pub month_or_season: Option<MonthOrSeason>,
184 pub month_quality: Quality,
186 pub day: Option<Day>,
188 pub day_quality: Quality,
190 pub time: Option<Time>,
192}
193
194#[derive(Debug, PartialEq, Eq, Clone, Copy)]
196#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
197pub enum Timezone {
198 Utc,
200 Offset(i16),
202}
203
204#[derive(Debug, PartialEq, Eq, Clone, Copy)]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207pub struct Time {
208 pub hour: u32,
210 pub minute: u32,
212 pub second: u32,
214 pub timezone: Option<Timezone>,
216}
217
218use std::fmt;
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct ParseError(String);
223
224impl fmt::Display for ParseError {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 write!(f, "invalid EDTF: {}", self.0)
227 }
228}
229
230impl std::error::Error for ParseError {}
231
232impl std::str::FromStr for Edtf {
233 type Err = ParseError;
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 let mut input = s;
237 let edtf = parse(&mut input).map_err(|e| ParseError(e.to_string()))?;
238 if !input.is_empty() {
239 return Err(ParseError(format!("unexpected trailing input: {input}")));
240 }
241 Ok(edtf)
242 }
243}
244
245impl std::str::FromStr for Date {
246 type Err = ParseError;
247
248 fn from_str(s: &str) -> Result<Self, Self::Err> {
249 let mut input = s;
250 let date = parse_date(&mut input).map_err(|e| ParseError(e.to_string()))?;
251 if !input.is_empty() {
252 return Err(ParseError(format!("unexpected trailing input: {input}")));
253 }
254 Ok(date)
255 }
256}
257
258impl fmt::Display for Edtf {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 match self {
261 Edtf::Date(d) => write!(f, "{d}"),
262 Edtf::Interval(i) => write!(f, "{}/{}", i.start, i.end),
263 Edtf::IntervalFrom(d) => write!(f, "{d}/.."),
264 Edtf::IntervalTo(d) => write!(f, "../{d}"),
265 }
266 }
267}
268
269impl fmt::Display for Date {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 write!(f, "{}{}", self.year, self.year_quality)?;
272 if let Some(m) = self.month_or_season {
273 write!(f, "-{}{}", m, self.month_quality)?;
274 if let Some(d) = self.day {
275 write!(f, "-{}{}", d, self.day_quality)?;
276 }
277 }
278 if let Some(t) = self.time {
279 write!(f, "T{t}")?;
280 }
281 Ok(())
282 }
283}
284
285impl fmt::Display for Year {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 if self.value > 9999 || self.value < -9999 {
288 write!(f, "Y{}", self.value)
289 } else if self.value < 0 {
290 let abs_val = self.value.abs();
291 let mut s = format!("{abs_val:04}");
292 match self.unspecified {
293 UnspecifiedYear::None => write!(f, "-{s}"),
294 UnspecifiedYear::One => {
295 s.replace_range(3..4, "u");
296 write!(f, "-{s}")
297 }
298 UnspecifiedYear::Two => {
299 s.replace_range(2..4, "uu");
300 write!(f, "-{s}")
301 }
302 UnspecifiedYear::Three => {
303 s.replace_range(1..4, "uuu");
304 write!(f, "-{s}")
305 }
306 UnspecifiedYear::Four => {
307 s.replace_range(0..4, "uuuu");
308 write!(f, "-{s}")
309 }
310 }
311 } else {
312 let mut s = format!("{:04}", self.value);
313 match self.unspecified {
314 UnspecifiedYear::None => write!(f, "{s}"),
315 UnspecifiedYear::One => {
316 s.replace_range(3..4, "u");
317 write!(f, "{s}")
318 }
319 UnspecifiedYear::Two => {
320 s.replace_range(2..4, "uu");
321 write!(f, "{s}")
322 }
323 UnspecifiedYear::Three => {
324 s.replace_range(1..4, "uuu");
325 write!(f, "{s}")
326 }
327 UnspecifiedYear::Four => {
328 s.replace_range(0..4, "uuuu");
329 write!(f, "{s}")
330 }
331 }
332 }
333 }
334}
335
336impl fmt::Display for MonthOrSeason {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 match self {
339 MonthOrSeason::Month(m) => write!(f, "{m:02}"),
340 MonthOrSeason::Unspecified => write!(f, "uu"),
341 MonthOrSeason::Spring => write!(f, "21"),
342 MonthOrSeason::Summer => write!(f, "22"),
343 MonthOrSeason::Autumn => write!(f, "23"),
344 MonthOrSeason::Winter => write!(f, "24"),
345 }
346 }
347}
348
349impl fmt::Display for Day {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 match self {
352 Day::Day(d) => write!(f, "{d:02}"),
353 Day::Unspecified => write!(f, "uu"),
354 }
355 }
356}
357
358impl fmt::Display for Time {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
361 match self.timezone {
362 Some(Timezone::Utc) => write!(f, "Z"),
363 Some(Timezone::Offset(mins)) => {
364 let sign = if mins >= 0 { '+' } else { '-' };
365 let abs = mins.unsigned_abs();
366 write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)
367 }
368 None => Ok(()),
369 }
370 }
371}
372
373impl fmt::Display for Quality {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 match (self.uncertain, self.approximate) {
376 (true, true) => write!(f, "%"),
377 (true, false) => write!(f, "?"),
378 (false, true) => write!(f, "~"),
379 (false, false) => Ok(()),
380 }
381 }
382}
383
384fn parse_quality(input: &mut &str) -> Result<Quality, ErrMode<ContextError>> {
385 let qualifier = opt(alt(('?', '~', '%'))).parse_next(input)?;
386 Ok(match qualifier {
387 Some('?') => Quality {
388 uncertain: true,
389 approximate: false,
390 },
391 Some('~') => Quality {
392 uncertain: false,
393 approximate: true,
394 },
395 Some('%') => Quality {
396 uncertain: true,
397 approximate: true,
398 },
399 _ => Quality::default(),
400 })
401}
402
403fn parse_year(input: &mut &str) -> Result<Year, ErrMode<ContextError>> {
404 if input.starts_with('Y') {
405 let _ = 'Y'.parse_next(input)?;
406 let value: i64 = dec_int.parse_next(input)?;
407 return Ok(Year {
408 value,
409 unspecified: UnspecifiedYear::None,
410 });
411 }
412
413 let sign = opt(alt(('-', '+'))).parse_next(input)?;
414 let s = take(4_usize).parse_next(input)?;
415
416 let mut value_str = String::with_capacity(4);
417 let mut unspecified_count = 0;
418
419 for c in s.chars() {
420 if c == 'u' || c == 'X' {
421 value_str.push('0');
422 unspecified_count += 1;
423 } else if c.is_ascii_digit() {
424 value_str.push(c);
425 } else {
426 return Err(ErrMode::Backtrack(ContextError::default()));
427 }
428 }
429
430 let mut value = value_str
431 .parse::<i64>()
432 .map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
433
434 if let Some('-') = sign {
435 value = -value;
436 }
437
438 let unspecified = match unspecified_count {
439 0 => UnspecifiedYear::None,
440 1 => UnspecifiedYear::One,
441 2 => UnspecifiedYear::Two,
442 3 => UnspecifiedYear::Three,
443 4 => UnspecifiedYear::Four,
444 _ => return Err(ErrMode::Backtrack(ContextError::default())),
445 };
446
447 Ok(Year { value, unspecified })
448}
449
450fn parse_month_or_season(input: &mut &str) -> Result<MonthOrSeason, ErrMode<ContextError>> {
451 let s = take(2_usize).parse_next(input)?;
452 if s == "uu" || s == "XX" {
453 return Ok(MonthOrSeason::Unspecified);
454 }
455
456 let val: u32 = s
457 .parse()
458 .map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
459
460 match val {
461 1..=12 => Ok(MonthOrSeason::Month(val)),
462 21 => Ok(MonthOrSeason::Spring),
463 22 => Ok(MonthOrSeason::Summer),
464 23 => Ok(MonthOrSeason::Autumn),
465 24 => Ok(MonthOrSeason::Winter),
466 _ => Err(ErrMode::Backtrack(ContextError::default())),
467 }
468}
469
470fn parse_day(input: &mut &str) -> Result<Day, ErrMode<ContextError>> {
471 let s = take(2_usize).parse_next(input)?;
472 if s == "uu" || s == "XX" {
473 return Ok(Day::Unspecified);
474 }
475
476 let val: u32 = s
477 .parse()
478 .map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
479 match val {
480 1..=31 => Ok(Day::Day(val)),
481 _ => Err(ErrMode::Backtrack(ContextError::default())),
482 }
483}
484
485fn parse_timezone(input: &mut &str) -> Result<Option<Timezone>, ErrMode<ContextError>> {
486 if input.starts_with('Z') {
487 let _ = 'Z'.parse_next(input)?;
488 return Ok(Some(Timezone::Utc));
489 }
490 if input.starts_with('+') || input.starts_with('-') {
491 let sign = opt(alt(('+', '-'))).parse_next(input)?.unwrap_or('+');
492 let h = take(2_usize)
493 .try_map(|s: &str| s.parse::<i16>())
494 .parse_next(input)?;
495 let _ = ':'.parse_next(input)?;
496 let m = take(2_usize)
497 .try_map(|s: &str| s.parse::<i16>())
498 .parse_next(input)?;
499 let total = h * 60 + m;
500 let offset = if sign == '-' { -total } else { total };
501 return Ok(Some(Timezone::Offset(offset)));
502 }
503 Ok(None)
504}
505
506fn parse_time(input: &mut &str) -> Result<Time, ErrMode<ContextError>> {
507 let hour = take(2_usize)
508 .try_map(|s: &str| s.parse::<u32>())
509 .parse_next(input)?;
510 let _ = ':'.parse_next(input)?;
511 let minute = take(2_usize)
512 .try_map(|s: &str| s.parse::<u32>())
513 .parse_next(input)?;
514 let _ = ':'.parse_next(input)?;
515 let second = take(2_usize)
516 .try_map(|s: &str| s.parse::<u32>())
517 .parse_next(input)?;
518 let timezone = parse_timezone(input)?;
519
520 if hour > 23 || minute > 59 || second > 59 {
521 return Err(ErrMode::Backtrack(ContextError::default()));
522 }
523
524 Ok(Time {
525 hour,
526 minute,
527 second,
528 timezone,
529 })
530}
531
532pub fn parse_date(input: &mut &str) -> Result<Date, ErrMode<ContextError>> {
542 let year = parse_year.parse_next(input)?;
543 let year_quality = parse_quality.parse_next(input)?;
544
545 let month_or_season = if input.starts_with('-') {
546 let _ = '-'.parse_next(input)?;
547 Some(parse_month_or_season.parse_next(input)?)
548 } else {
549 None
550 };
551 let month_quality = if month_or_season.is_some() {
552 parse_quality.parse_next(input)?
553 } else {
554 Quality::default()
555 };
556
557 let day = if let Some(MonthOrSeason::Month(_) | MonthOrSeason::Unspecified) = month_or_season {
558 if input.starts_with('-') {
559 let _ = '-'.parse_next(input)?;
560 Some(parse_day.parse_next(input)?)
561 } else {
562 None
563 }
564 } else {
565 None
566 };
567 let day_quality = if day.is_some() {
568 parse_quality.parse_next(input)?
569 } else {
570 Quality::default()
571 };
572
573 let time = if input.starts_with('T') {
574 let _ = 'T'.parse_next(input)?;
575 Some(parse_time.parse_next(input)?)
576 } else {
577 None
578 };
579
580 Ok(Date {
586 year,
587 year_quality,
588 month_or_season,
589 month_quality,
590 day,
591 day_quality,
592 time,
593 })
594}
595
596pub fn parse(input: &mut &str) -> Result<Edtf, ErrMode<ContextError>> {
606 if input.starts_with("../") {
607 let _ = "../".parse_next(input)?;
608 let date = parse_date.parse_next(input)?;
609 return Ok(Edtf::IntervalTo(date));
610 }
611
612 let start_date = parse_date.parse_next(input)?;
613
614 if input.starts_with('/') {
615 let _ = '/'.parse_next(input)?;
616 if input.is_empty() || *input == ".." {
617 if *input == ".." {
618 let _ = "..".parse_next(input)?;
619 }
620 Ok(Edtf::IntervalFrom(start_date))
621 } else {
622 let end_date = parse_date.parse_next(input)?;
623 Ok(Edtf::Interval(Interval {
624 start: start_date,
625 end: end_date,
626 }))
627 }
628 } else {
629 Ok(Edtf::Date(start_date))
630 }
631}
632
633#[cfg(test)]
634#[allow(
635 clippy::unwrap_used,
636 clippy::expect_used,
637 clippy::panic,
638 clippy::indexing_slicing,
639 clippy::todo,
640 clippy::unimplemented,
641 clippy::unreachable,
642 clippy::get_unwrap,
643 reason = "Panicking is acceptable and often desired in tests."
644)]
645mod tests {
646 use super::*;
647
648 #[test]
649 fn test_parse_date() {
650 let mut input = "2023-05-15";
651 let res = parse_date(&mut input).unwrap();
652 assert_eq!(res.year.value, 2023);
653 assert_eq!(res.month_or_season, Some(MonthOrSeason::Month(5)));
654 assert_eq!(res.day, Some(Day::Day(15)));
655 }
656
657 #[test]
658 fn test_unspecified_year() {
659 let mut input = "199u";
660 let res = parse_date(&mut input).unwrap();
661 assert_eq!(res.year.value, 1990);
662 assert_eq!(res.year.unspecified, UnspecifiedYear::One);
663 }
664
665 #[test]
666 fn test_extended_year() {
667 let mut input = "Y17000000002";
668 let res = parse_date(&mut input).unwrap();
669 assert_eq!(res.year.value, 17_000_000_002_i64);
670 }
671
672 #[test]
673 fn test_unspecified_month_day() {
674 let mut input = "2004-uu-uu";
675 let res = parse_date(&mut input).unwrap();
676 assert_eq!(res.month_or_season, Some(MonthOrSeason::Unspecified));
677 assert_eq!(res.day, Some(Day::Unspecified));
678 }
679
680 #[test]
681 fn test_component_quality() {
682 let mut input = "2004?-06-11";
683 let res = parse_date(&mut input).unwrap();
684 assert!(res.year_quality.uncertain);
685 assert!(!res.month_quality.uncertain);
686 assert!(!res.day_quality.uncertain);
687
688 let mut input2 = "2004-06-11?";
689 let res2 = parse_date(&mut input2).unwrap();
690 assert!(!res2.year_quality.uncertain);
691 assert!(!res2.month_quality.uncertain);
692 assert!(res2.day_quality.uncertain);
693 }
694
695 #[test]
696 fn test_parse_interval() {
697 let mut input = "2023-05/2024-06";
698 let res = parse(&mut input).unwrap();
699 if let Edtf::Interval(interval) = res {
700 assert_eq!(interval.start.year.value, 2023);
701 assert_eq!(interval.end.year.value, 2024);
702 } else {
703 panic!("Expected Interval");
704 }
705 }
706
707 #[test]
708 fn test_parse_interval_from() {
709 let mut input = "2023-05/..";
710 let res = parse(&mut input).unwrap();
711 if let Edtf::IntervalFrom(date) = res {
712 assert_eq!(date.year.value, 2023);
713 } else {
714 panic!("Expected IntervalFrom");
715 }
716 }
717
718 #[test]
719 fn test_parse_interval_to() {
720 let mut input = "../2023-05";
721 let res = parse(&mut input).unwrap();
722 if let Edtf::IntervalTo(date) = res {
723 assert_eq!(date.year.value, 2023);
724 assert_eq!(date.month_or_season, Some(MonthOrSeason::Month(5)));
725 } else {
726 panic!("Expected IntervalTo");
727 }
728 }
729
730 #[test]
731 fn test_parse_season() {
732 let mut input = "2023-21";
733 let res = parse_date(&mut input).unwrap();
734 assert_eq!(res.month_or_season, Some(MonthOrSeason::Spring));
735 assert_eq!(res.to_string(), "2023-21");
736 }
737
738 #[test]
739 fn test_round_trip() {
740 let cases = vec![
741 "2023-05-15",
742 "199u",
743 "2004-uu-uu",
744 "2004?-06-11",
745 "2004-06-11?",
746 "2023-05/2024-06",
747 "2023-05/..",
748 "../2023-05",
749 "Y17000000002",
750 "1985-04-12T23:20:30Z",
751 "2004-01-01T10:10:10+05:30",
752 ];
753 for case in cases {
754 let mut input = case;
755 let res = parse(&mut input).unwrap();
756 assert_eq!(res.to_string(), case);
757 }
758 }
759
760 #[test]
761 fn test_parse_datetime_utc() {
762 let mut input = "1985-04-12T23:20:30Z";
763 let res = parse_date(&mut input).unwrap();
764 let t = res.time.unwrap();
765 assert_eq!(t.hour, 23);
766 assert_eq!(t.minute, 20);
767 assert_eq!(t.second, 30);
768 assert_eq!(t.timezone, Some(Timezone::Utc));
769 }
770
771 #[test]
772 fn test_parse_datetime_offset() {
773 let mut input = "2004-01-01T10:10:10+05:30";
774 let res = parse_date(&mut input).unwrap();
775 let t = res.time.unwrap();
776 assert_eq!(t.timezone, Some(Timezone::Offset(330)));
777 }
778
779 #[test]
780 fn test_parse_datetime_no_tz() {
781 let mut input = "2004-01-01T10:10:10";
782 let res = parse_date(&mut input).unwrap();
783 let t = res.time.unwrap();
784 assert_eq!(t.timezone, None);
785 }
786
787 #[test]
788 fn test_parse_leaves_unconsumed_suffix() {
789 let mut input = "2023-05 trailing";
790 let res = parse(&mut input).unwrap();
791 assert_eq!(res.to_string(), "2023-05");
792 assert_eq!(input, " trailing");
793 }
794
795 #[test]
796 fn test_invalid_day_is_rejected() {
797 let mut input = "2023-05-32";
798 assert!(parse_date(&mut input).is_err());
799 }
800
801 #[test]
802 fn test_invalid_time_is_rejected() {
803 let mut invalid_hour = "2023-05-15T24:00:00";
804 assert!(parse_date(&mut invalid_hour).is_err());
805
806 let mut invalid_minute = "2023-05-15T23:60:00";
807 assert!(parse_date(&mut invalid_minute).is_err());
808
809 let mut invalid_second = "2023-05-15T23:59:60";
810 assert!(parse_date(&mut invalid_second).is_err());
811 }
812}