diff --git a/go-sgf/src/date.rs b/go-sgf/src/date.rs new file mode 100644 index 0000000..5dc06f2 --- /dev/null +++ b/go-sgf/src/date.rs @@ -0,0 +1,147 @@ +use chrono::{Datelike, NaiveDate}; +use std::num::ParseIntError; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum Error { + #[error("Failed to parse integer {0}")] + ParseNumberError(ParseIntError), + + #[error("Invalid date")] + InvalidDate, + + #[error("unsupported date format")] + Unsupported, +} + +#[derive(Clone, Debug, PartialEq, PartialOrd)] +pub enum Date { + Year(i32), + YearMonth(i32, u32), + Date(chrono::NaiveDate), +} + +/* +impl TryFrom<&str> for Date { + type Error = String; + fn try_from(s: &str) -> Result { + let date_parts = s.split("-").collect::>(); + + if date_parts.len() >= 1 { + let year = date_parts[0] + .parse::() + .map_err(|e| format!("{:?}", e))?; + + if date_parts.len() >= 2 { + let month = date_parts[1] + .parse::() + .map_err(|e| format!("{:?}", e))?; + + if date_parts.len() >= 3 { + let day = date_parts[2] + .parse::() + .map_err(|e| format!("{:?}", e))?; + let date = + chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or("invalid date")?; + Ok(Date::Date(date)) + } else { + Ok(Date::YearMonth(year, month)) + } + } else { + Ok(Date::Year(year)) + } + } else { + return Err("no elements".to_owned()); + } + } +} +*/ + +fn parse_numbers(s: &str) -> Result, Error> { + s.split("-") + .map(|s| s.parse::().map_err(|err| Error::ParseNumberError(err))) + .collect::, Error>>() +} + +fn parse_date_field(s: &str) -> Result, Error> { + let date_elements = s.split(","); + let mut dates = Vec::new(); + + let mut most_recent: Option = None; + for element in date_elements { + let fields = parse_numbers(element)?; + + let new_date = match fields.as_slice() { + [] => panic!("all segments must have a field"), + [v1] => match most_recent { + Some(Date::Year(_)) => Date::Year(*v1), + Some(Date::YearMonth(y, _)) => Date::YearMonth(y, *v1 as u32), + Some(Date::Date(d)) => { + Date::Date(d.clone().with_day(*v1 as u32).ok_or(Error::InvalidDate)?) + } + + None => Date::Year(*v1), + }, + [v1, v2] => Date::YearMonth(*v1, *v2 as u32), + [v1, v2, v3, ..] => Date::Date( + NaiveDate::from_ymd_opt(v1.clone(), v2.clone() as u32, v3.clone() as u32).unwrap(), + ), + }; + dates.push(new_date.clone()); + most_recent = Some(new_date); + } + + Ok(dates) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn it_parses_a_year() { + assert_eq!(parse_date_field("1996"), Ok(vec![Date::Year(1996)])); + } + + #[test] + fn it_parses_a_month() { + assert_eq!( + parse_date_field("1996-12"), + Ok(vec![Date::YearMonth(1996, 12)]) + ); + } + + #[test] + fn it_parses_a_date() { + assert_eq!( + parse_date_field("1996-12-27"), + Ok(vec![Date::Date( + NaiveDate::from_ymd_opt(1996, 12, 27).unwrap() + )]) + ); + } + + #[test] + fn it_parses_date_continuation() { + assert_eq!( + parse_date_field("1996-12-27,28"), + Ok(vec![ + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap()) + ]) + ); + } + + #[test] + fn it_parses_date_crossing_year_boundary() { + assert_eq!( + parse_date_field("1996-12-27,28,1997-01-03,04"), + Ok(vec![ + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1997, 1, 3).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1997, 1, 4).unwrap()), + ]) + ); + } +} diff --git a/go-sgf/src/go.rs b/go-sgf/src/go.rs index f900167..15e570f 100644 --- a/go-sgf/src/go.rs +++ b/go-sgf/src/go.rs @@ -68,8 +68,10 @@ // PM // VW -use crate::tree::{parse_collection, parse_size, ParseSizeError, Size}; -use nom::IResult; +use crate::{ + date::Date, + tree::{parse_collection, ParseSizeError, Size}, +}; #[derive(Debug)] pub enum Error<'a> { @@ -133,7 +135,7 @@ pub struct GameInfo { pub event: Option, // Games can be played across multiple days, even multiple years. The format specifies // shortcuts. - pub date: Vec, + pub date: Vec, pub location: Option, // special rules for the round-number and type pub round: Option, @@ -209,6 +211,7 @@ pub enum GameType { Unsupported, } +/* enum PropType { Move, Setup, @@ -228,9 +231,10 @@ enum PropValue { Move, Stone, } +*/ pub fn parse_sgf<'a>(input: &'a str) -> Result, Error<'a>> { - let (input, trees) = parse_collection::>(input)?; + let (_, trees) = parse_collection::>(input)?; let games = trees .into_iter() @@ -290,7 +294,7 @@ pub fn parse_sgf<'a>(input: &'a str) -> Result, Error<'a>> { #[cfg(test)] mod tests { use super::*; - use crate::tree::Size; + use crate::{date::Date, tree::Size}; use std::fs::File; use std::io::Read; @@ -362,8 +366,8 @@ mod tests { assert_eq!( tree.info.date, vec![ - chrono::NaiveDate::from_ymd_opt(1996, 10, 18).unwrap(), - chrono::NaiveDate::from_ymd_opt(1996, 10, 19).unwrap(), + Date::Date(chrono::NaiveDate::from_ymd_opt(1996, 10, 18).unwrap()), + Date::Date(chrono::NaiveDate::from_ymd_opt(1996, 10, 19).unwrap()), ] ); assert_eq!(tree.info.event, Some("21st Meijin".to_owned())); diff --git a/go-sgf/src/lib.rs b/go-sgf/src/lib.rs index fd695ee..10decd2 100644 --- a/go-sgf/src/lib.rs +++ b/go-sgf/src/lib.rs @@ -1,3 +1,4 @@ +pub mod date; pub mod go; pub mod tree;