use winnow::ascii::dec_int;
use winnow::combinator::{alt, opt};
use winnow::error::{ContextError, ErrMode};
use winnow::prelude::*;
use winnow::token::take;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Edtf {
Date(Date),
Interval(Interval),
IntervalFrom(Date),
IntervalTo(Date),
}
impl Edtf {
pub fn year(&self) -> i64 {
match self {
Self::Date(date) => date.year.value,
Self::Interval(interval) => interval.start.year.value,
Self::IntervalFrom(date) => date.year.value,
Self::IntervalTo(date) => date.year.value,
}
}
pub fn month(&self) -> Option<u32> {
let m_opt = match self {
Self::Date(date) => date.month_or_season,
Self::Interval(interval) => interval.start.month_or_season,
Self::IntervalFrom(date) => date.month_or_season,
Self::IntervalTo(date) => date.month_or_season,
};
match m_opt {
Some(MonthOrSeason::Month(m)) => Some(m),
_ => None,
}
}
pub fn day(&self) -> Option<u32> {
let d_opt = match self {
Self::Date(date) => date.day,
Self::Interval(interval) => interval.start.day,
Self::IntervalFrom(date) => date.day,
Self::IntervalTo(date) => date.day,
};
match d_opt {
Some(Day::Day(d)) => Some(d),
_ => None,
}
}
pub fn is_range(&self) -> bool {
matches!(
self,
Self::Interval(_) | Self::IntervalFrom(_) | Self::IntervalTo(_)
)
}
pub fn is_open_range(&self) -> bool {
matches!(self, Self::IntervalFrom(_))
}
pub fn time(&self) -> Option<Time> {
match self {
Self::Date(date) => date.time,
_ => None,
}
}
pub fn has_time(&self) -> bool {
self.time().is_some()
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Interval {
pub start: Date,
pub end: Date,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MonthOrSeason {
Month(u32),
Unspecified,
Spring,
Summer,
Autumn,
Winter,
}
#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Quality {
pub uncertain: bool,
pub approximate: bool,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Year {
pub value: i64,
pub unspecified: UnspecifiedYear,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum UnspecifiedYear {
#[default]
None,
One,
Two,
Three,
Four,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Day {
Day(u32),
Unspecified,
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Date {
pub year: Year,
pub year_quality: Quality,
pub month_or_season: Option<MonthOrSeason>,
pub month_quality: Quality,
pub day: Option<Day>,
pub day_quality: Quality,
pub time: Option<Time>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Timezone {
Utc,
Offset(i16),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Time {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub timezone: Option<Timezone>,
}
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError(String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid EDTF: {}", self.0)
}
}
impl std::error::Error for ParseError {}
impl std::str::FromStr for Edtf {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut input = s;
let edtf = parse(&mut input).map_err(|e| ParseError(e.to_string()))?;
if !input.is_empty() {
return Err(ParseError(format!("unexpected trailing input: {input}")));
}
Ok(edtf)
}
}
impl std::str::FromStr for Date {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut input = s;
let date = parse_date(&mut input).map_err(|e| ParseError(e.to_string()))?;
if !input.is_empty() {
return Err(ParseError(format!("unexpected trailing input: {input}")));
}
Ok(date)
}
}
impl fmt::Display for Edtf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Edtf::Date(d) => write!(f, "{d}"),
Edtf::Interval(i) => write!(f, "{}/{}", i.start, i.end),
Edtf::IntervalFrom(d) => write!(f, "{d}/.."),
Edtf::IntervalTo(d) => write!(f, "../{d}"),
}
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.year, self.year_quality)?;
if let Some(m) = self.month_or_season {
write!(f, "-{}{}", m, self.month_quality)?;
if let Some(d) = self.day {
write!(f, "-{}{}", d, self.day_quality)?;
}
}
if let Some(t) = self.time {
write!(f, "T{t}")?;
}
Ok(())
}
}
impl fmt::Display for Year {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.value > 9999 || self.value < -9999 {
write!(f, "Y{}", self.value)
} else if self.value < 0 {
let abs_val = self.value.abs();
let mut s = format!("{abs_val:04}");
match self.unspecified {
UnspecifiedYear::None => write!(f, "-{s}"),
UnspecifiedYear::One => {
s.replace_range(3..4, "u");
write!(f, "-{s}")
}
UnspecifiedYear::Two => {
s.replace_range(2..4, "uu");
write!(f, "-{s}")
}
UnspecifiedYear::Three => {
s.replace_range(1..4, "uuu");
write!(f, "-{s}")
}
UnspecifiedYear::Four => {
s.replace_range(0..4, "uuuu");
write!(f, "-{s}")
}
}
} else {
let mut s = format!("{:04}", self.value);
match self.unspecified {
UnspecifiedYear::None => write!(f, "{s}"),
UnspecifiedYear::One => {
s.replace_range(3..4, "u");
write!(f, "{s}")
}
UnspecifiedYear::Two => {
s.replace_range(2..4, "uu");
write!(f, "{s}")
}
UnspecifiedYear::Three => {
s.replace_range(1..4, "uuu");
write!(f, "{s}")
}
UnspecifiedYear::Four => {
s.replace_range(0..4, "uuuu");
write!(f, "{s}")
}
}
}
}
}
impl fmt::Display for MonthOrSeason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MonthOrSeason::Month(m) => write!(f, "{m:02}"),
MonthOrSeason::Unspecified => write!(f, "uu"),
MonthOrSeason::Spring => write!(f, "21"),
MonthOrSeason::Summer => write!(f, "22"),
MonthOrSeason::Autumn => write!(f, "23"),
MonthOrSeason::Winter => write!(f, "24"),
}
}
}
impl fmt::Display for Day {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Day::Day(d) => write!(f, "{d:02}"),
Day::Unspecified => write!(f, "uu"),
}
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
match self.timezone {
Some(Timezone::Utc) => write!(f, "Z"),
Some(Timezone::Offset(mins)) => {
let sign = if mins >= 0 { '+' } else { '-' };
let abs = mins.unsigned_abs();
write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)
}
None => Ok(()),
}
}
}
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.uncertain, self.approximate) {
(true, true) => write!(f, "%"),
(true, false) => write!(f, "?"),
(false, true) => write!(f, "~"),
(false, false) => Ok(()),
}
}
}
fn parse_quality(input: &mut &str) -> Result<Quality, ErrMode<ContextError>> {
let qualifier = opt(alt(('?', '~', '%'))).parse_next(input)?;
Ok(match qualifier {
Some('?') => Quality {
uncertain: true,
approximate: false,
},
Some('~') => Quality {
uncertain: false,
approximate: true,
},
Some('%') => Quality {
uncertain: true,
approximate: true,
},
_ => Quality::default(),
})
}
fn parse_year(input: &mut &str) -> Result<Year, ErrMode<ContextError>> {
if input.starts_with('Y') {
let _ = 'Y'.parse_next(input)?;
let value: i64 = dec_int.parse_next(input)?;
return Ok(Year {
value,
unspecified: UnspecifiedYear::None,
});
}
let sign = opt(alt(('-', '+'))).parse_next(input)?;
let s = take(4_usize).parse_next(input)?;
let mut value_str = String::with_capacity(4);
let mut unspecified_count = 0;
for c in s.chars() {
if c == 'u' || c == 'X' {
value_str.push('0');
unspecified_count += 1;
} else if c.is_ascii_digit() {
value_str.push(c);
} else {
return Err(ErrMode::Backtrack(ContextError::default()));
}
}
let mut value = value_str
.parse::<i64>()
.map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
if let Some('-') = sign {
value = -value;
}
let unspecified = match unspecified_count {
0 => UnspecifiedYear::None,
1 => UnspecifiedYear::One,
2 => UnspecifiedYear::Two,
3 => UnspecifiedYear::Three,
4 => UnspecifiedYear::Four,
_ => return Err(ErrMode::Backtrack(ContextError::default())),
};
Ok(Year { value, unspecified })
}
fn parse_month_or_season(input: &mut &str) -> Result<MonthOrSeason, ErrMode<ContextError>> {
let s = take(2_usize).parse_next(input)?;
if s == "uu" || s == "XX" {
return Ok(MonthOrSeason::Unspecified);
}
let val: u32 = s
.parse()
.map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
match val {
1..=12 => Ok(MonthOrSeason::Month(val)),
21 => Ok(MonthOrSeason::Spring),
22 => Ok(MonthOrSeason::Summer),
23 => Ok(MonthOrSeason::Autumn),
24 => Ok(MonthOrSeason::Winter),
_ => Err(ErrMode::Backtrack(ContextError::default())),
}
}
fn parse_day(input: &mut &str) -> Result<Day, ErrMode<ContextError>> {
let s = take(2_usize).parse_next(input)?;
if s == "uu" || s == "XX" {
return Ok(Day::Unspecified);
}
let val: u32 = s
.parse()
.map_err(|_| ErrMode::Backtrack(ContextError::default()))?;
match val {
1..=31 => Ok(Day::Day(val)),
_ => Err(ErrMode::Backtrack(ContextError::default())),
}
}
fn parse_timezone(input: &mut &str) -> Result<Option<Timezone>, ErrMode<ContextError>> {
if input.starts_with('Z') {
let _ = 'Z'.parse_next(input)?;
return Ok(Some(Timezone::Utc));
}
if input.starts_with('+') || input.starts_with('-') {
let sign = opt(alt(('+', '-'))).parse_next(input)?.unwrap_or('+');
let h = take(2_usize)
.try_map(|s: &str| s.parse::<i16>())
.parse_next(input)?;
let _ = ':'.parse_next(input)?;
let m = take(2_usize)
.try_map(|s: &str| s.parse::<i16>())
.parse_next(input)?;
let total = h * 60 + m;
let offset = if sign == '-' { -total } else { total };
return Ok(Some(Timezone::Offset(offset)));
}
Ok(None)
}
fn parse_time(input: &mut &str) -> Result<Time, ErrMode<ContextError>> {
let hour = take(2_usize)
.try_map(|s: &str| s.parse::<u32>())
.parse_next(input)?;
let _ = ':'.parse_next(input)?;
let minute = take(2_usize)
.try_map(|s: &str| s.parse::<u32>())
.parse_next(input)?;
let _ = ':'.parse_next(input)?;
let second = take(2_usize)
.try_map(|s: &str| s.parse::<u32>())
.parse_next(input)?;
let timezone = parse_timezone(input)?;
if hour > 23 || minute > 59 || second > 59 {
return Err(ErrMode::Backtrack(ContextError::default()));
}
Ok(Time {
hour,
minute,
second,
timezone,
})
}
pub fn parse_date(input: &mut &str) -> Result<Date, ErrMode<ContextError>> {
let year = parse_year.parse_next(input)?;
let year_quality = parse_quality.parse_next(input)?;
let month_or_season = if input.starts_with('-') {
let _ = '-'.parse_next(input)?;
Some(parse_month_or_season.parse_next(input)?)
} else {
None
};
let month_quality = if month_or_season.is_some() {
parse_quality.parse_next(input)?
} else {
Quality::default()
};
let day = if let Some(MonthOrSeason::Month(_) | MonthOrSeason::Unspecified) = month_or_season {
if input.starts_with('-') {
let _ = '-'.parse_next(input)?;
Some(parse_day.parse_next(input)?)
} else {
None
}
} else {
None
};
let day_quality = if day.is_some() {
parse_quality.parse_next(input)?
} else {
Quality::default()
};
let time = if input.starts_with('T') {
let _ = 'T'.parse_next(input)?;
Some(parse_time.parse_next(input)?)
} else {
None
};
Ok(Date {
year,
year_quality,
month_or_season,
month_quality,
day,
day_quality,
time,
})
}
pub fn parse(input: &mut &str) -> Result<Edtf, ErrMode<ContextError>> {
if input.starts_with("../") {
let _ = "../".parse_next(input)?;
let date = parse_date.parse_next(input)?;
return Ok(Edtf::IntervalTo(date));
}
let start_date = parse_date.parse_next(input)?;
if input.starts_with('/') {
let _ = '/'.parse_next(input)?;
if input.is_empty() || *input == ".." {
if *input == ".." {
let _ = "..".parse_next(input)?;
}
Ok(Edtf::IntervalFrom(start_date))
} else {
let end_date = parse_date.parse_next(input)?;
Ok(Edtf::Interval(Interval {
start: start_date,
end: end_date,
}))
}
} else {
Ok(Edtf::Date(start_date))
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
#[test]
fn test_parse_date() {
let mut input = "2023-05-15";
let res = parse_date(&mut input).unwrap();
assert_eq!(res.year.value, 2023);
assert_eq!(res.month_or_season, Some(MonthOrSeason::Month(5)));
assert_eq!(res.day, Some(Day::Day(15)));
}
#[test]
fn test_unspecified_year() {
let mut input = "199u";
let res = parse_date(&mut input).unwrap();
assert_eq!(res.year.value, 1990);
assert_eq!(res.year.unspecified, UnspecifiedYear::One);
}
#[test]
fn test_extended_year() {
let mut input = "Y17000000002";
let res = parse_date(&mut input).unwrap();
assert_eq!(res.year.value, 17_000_000_002_i64);
}
#[test]
fn test_unspecified_month_day() {
let mut input = "2004-uu-uu";
let res = parse_date(&mut input).unwrap();
assert_eq!(res.month_or_season, Some(MonthOrSeason::Unspecified));
assert_eq!(res.day, Some(Day::Unspecified));
}
#[test]
fn test_component_quality() {
let mut input = "2004?-06-11";
let res = parse_date(&mut input).unwrap();
assert!(res.year_quality.uncertain);
assert!(!res.month_quality.uncertain);
assert!(!res.day_quality.uncertain);
let mut input2 = "2004-06-11?";
let res2 = parse_date(&mut input2).unwrap();
assert!(!res2.year_quality.uncertain);
assert!(!res2.month_quality.uncertain);
assert!(res2.day_quality.uncertain);
}
#[test]
fn test_parse_interval() {
let mut input = "2023-05/2024-06";
let res = parse(&mut input).unwrap();
if let Edtf::Interval(interval) = res {
assert_eq!(interval.start.year.value, 2023);
assert_eq!(interval.end.year.value, 2024);
} else {
panic!("Expected Interval");
}
}
#[test]
fn test_parse_interval_from() {
let mut input = "2023-05/..";
let res = parse(&mut input).unwrap();
if let Edtf::IntervalFrom(date) = res {
assert_eq!(date.year.value, 2023);
} else {
panic!("Expected IntervalFrom");
}
}
#[test]
fn test_parse_interval_to() {
let mut input = "../2023-05";
let res = parse(&mut input).unwrap();
if let Edtf::IntervalTo(date) = res {
assert_eq!(date.year.value, 2023);
assert_eq!(date.month_or_season, Some(MonthOrSeason::Month(5)));
} else {
panic!("Expected IntervalTo");
}
}
#[test]
fn test_parse_season() {
let mut input = "2023-21";
let res = parse_date(&mut input).unwrap();
assert_eq!(res.month_or_season, Some(MonthOrSeason::Spring));
assert_eq!(res.to_string(), "2023-21");
}
#[test]
fn test_round_trip() {
let cases = vec![
"2023-05-15",
"199u",
"2004-uu-uu",
"2004?-06-11",
"2004-06-11?",
"2023-05/2024-06",
"2023-05/..",
"../2023-05",
"Y17000000002",
"1985-04-12T23:20:30Z",
"2004-01-01T10:10:10+05:30",
];
for case in cases {
let mut input = case;
let res = parse(&mut input).unwrap();
assert_eq!(res.to_string(), case);
}
}
#[test]
fn test_parse_datetime_utc() {
let mut input = "1985-04-12T23:20:30Z";
let res = parse_date(&mut input).unwrap();
let t = res.time.unwrap();
assert_eq!(t.hour, 23);
assert_eq!(t.minute, 20);
assert_eq!(t.second, 30);
assert_eq!(t.timezone, Some(Timezone::Utc));
}
#[test]
fn test_parse_datetime_offset() {
let mut input = "2004-01-01T10:10:10+05:30";
let res = parse_date(&mut input).unwrap();
let t = res.time.unwrap();
assert_eq!(t.timezone, Some(Timezone::Offset(330)));
}
#[test]
fn test_parse_datetime_no_tz() {
let mut input = "2004-01-01T10:10:10";
let res = parse_date(&mut input).unwrap();
let t = res.time.unwrap();
assert_eq!(t.timezone, None);
}
#[test]
fn test_parse_leaves_unconsumed_suffix() {
let mut input = "2023-05 trailing";
let res = parse(&mut input).unwrap();
assert_eq!(res.to_string(), "2023-05");
assert_eq!(input, " trailing");
}
#[test]
fn test_invalid_day_is_rejected() {
let mut input = "2023-05-32";
assert!(parse_date(&mut input).is_err());
}
#[test]
fn test_invalid_time_is_rejected() {
let mut invalid_hour = "2023-05-15T24:00:00";
assert!(parse_date(&mut invalid_hour).is_err());
let mut invalid_minute = "2023-05-15T23:60:00";
assert!(parse_date(&mut invalid_minute).is_err());
let mut invalid_second = "2023-05-15T23:59:60";
assert!(parse_date(&mut invalid_second).is_err());
}
}