use crate::{Color, Date, Error, GameResult, GameType, Win}; use chrono::Datelike; use nom::{ branch::alt, bytes::complete::{escaped_transform, tag, take_until, take_until1}, character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, combinator::{opt, value}, error::ParseError, multi::{many0, many1, separated_list1}, IResult, Parser, }; use std::{num::ParseIntError, time::Duration}; impl From for Error { fn from(_: ParseSizeError) -> Self { Self::InvalidBoardSize } } #[derive(Debug)] pub enum ParseSizeError { ParseIntError(ParseIntError), InsufficientArguments, } impl From for ParseSizeError { fn from(e: ParseIntError) -> Self { Self::ParseIntError(e) } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Annotation { BadMove, DoubtfulMove, InterestingMove, Tesuji, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Evaluation { Even, GoodForBlack, GoodForWhite, Unclear, } impl From<&str> for GameType { fn from(s: &str) -> Self { match s { "1" => Self::Go, "2" => Self::Othello, "3" => Self::Chess, "4" => Self::GomokuRenju, "5" => Self::NineMensMorris, "6" => Self::Backgammon, "7" => Self::ChineseChess, "8" => Self::Shogi, "9" => Self::LinesOfAction, "10" => Self::Ataxx, "11" => Self::Hex, "12" => Self::Jungle, "13" => Self::Neutron, "14" => Self::PhilosophersFootball, "15" => Self::Quadrature, "16" => Self::Trax, "17" => Self::Tantrix, "18" => Self::Amazons, "19" => Self::Octi, "20" => Self::Gess, "21" => Self::Twixt, "22" => Self::Zertz, "23" => Self::Plateau, "24" => Self::Yinsh, "25" => Self::Punct, "26" => Self::Gobblet, "27" => Self::Hive, "28" => Self::Exxit, "29" => Self::Hnefatal, "30" => Self::Kuba, "31" => Self::Tripples, "32" => Self::Chase, "33" => Self::TumblingDown, "34" => Self::Sahara, "35" => Self::Byte, "36" => Self::Focus, "37" => Self::Dvonn, "38" => Self::Tamsk, "39" => Self::Gipf, "40" => Self::Kropki, _ => Self::Other(s.to_owned()), } } } impl From<&GameType> for String { fn from(g: &GameType) -> String { match g { GameType::Go => "1".to_owned(), GameType::Othello => "2".to_owned(), GameType::Chess => "3".to_owned(), GameType::GomokuRenju => "4".to_owned(), GameType::NineMensMorris => "5".to_owned(), GameType::Backgammon => "6".to_owned(), GameType::ChineseChess => "7".to_owned(), GameType::Shogi => "8".to_owned(), GameType::LinesOfAction => "9".to_owned(), GameType::Ataxx => "10".to_owned(), GameType::Hex => "11".to_owned(), GameType::Jungle => "12".to_owned(), GameType::Neutron => "13".to_owned(), GameType::PhilosophersFootball => "14".to_owned(), GameType::Quadrature => "15".to_owned(), GameType::Trax => "16".to_owned(), GameType::Tantrix => "17".to_owned(), GameType::Amazons => "18".to_owned(), GameType::Octi => "19".to_owned(), GameType::Gess => "20".to_owned(), GameType::Twixt => "21".to_owned(), GameType::Zertz => "22".to_owned(), GameType::Plateau => "23".to_owned(), GameType::Yinsh => "24".to_owned(), GameType::Punct => "25".to_owned(), GameType::Gobblet => "26".to_owned(), GameType::Hive => "27".to_owned(), GameType::Exxit => "28".to_owned(), GameType::Hnefatal => "29".to_owned(), GameType::Kuba => "30".to_owned(), GameType::Tripples => "31".to_owned(), GameType::Chase => "32".to_owned(), GameType::TumblingDown => "33".to_owned(), GameType::Sahara => "34".to_owned(), GameType::Byte => "35".to_owned(), GameType::Focus => "36".to_owned(), GameType::Dvonn => "37".to_owned(), GameType::Tamsk => "38".to_owned(), GameType::Gipf => "39".to_owned(), GameType::Kropki => "40".to_owned(), GameType::Other(v) => v.clone(), } } } impl ToString for GameType { fn to_string(&self) -> String { String::from(self) } } #[derive(Clone, Debug, PartialEq)] pub struct Size { pub width: i32, pub height: i32, } impl TryFrom<&str> for Size { type Error = ParseSizeError; fn try_from(s: &str) -> Result { let parts = s .split(':') .map(|v| v.parse::()) .collect::, ParseIntError>>()?; match parts[..] { [width, height, ..] => Ok(Size { width, height }), [dim] => Ok(Size { width: dim, height: dim, }), [] => Err(ParseSizeError::InsufficientArguments), } } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Position(String); #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct PositionList(pub Vec); impl PositionList { pub fn compressed_list(&self) -> String { self.0.to_vec().join(":") } } #[derive(Clone, Debug, PartialEq)] pub struct Tree { pub root: Node, } impl ToString for Tree { fn to_string(&self) -> String { format!("({})", self.root.to_string()) } } #[derive(Clone, Debug, PartialEq)] pub struct Node { pub properties: Vec, pub next: Vec, } #[derive(Clone, Debug, PartialEq)] pub enum SetupInstr { Piece((Color, String)), Clear(String), } impl Node { pub fn mv(&self) -> Option<(Color, Move)> { self.find_by(|prop| match prop { Property::Move(val) => Some(val.clone()), _ => None, }) } pub fn setup(&self) -> Option> { let mut setup = Vec::new(); for prop in self.properties.iter() { match prop { Property::SetupBlackStones(positions) => { setup.append( &mut positions .0 .iter() .map(|pos| SetupInstr::Piece((Color::Black, pos.clone()))) .collect::>(), ); } Property::SetupWhiteStones(positions) => { setup.append( &mut positions .0 .iter() .map(|pos| SetupInstr::Piece((Color::White, pos.clone()))) .collect::>(), ); } Property::ClearStones(positions) => { setup.append( &mut positions .0 .iter() .map(|pos| SetupInstr::Clear(pos.clone())) .collect::>(), ); } _ => return None, } } if !setup.is_empty() { Some(setup) } else { None } } fn find_by(&self, f: F) -> Option where F: FnMut(&Property) -> Option, { self.properties.iter().filter_map(f).next() } } impl ToString for Node { fn to_string(&self) -> String { let props = self .properties .iter() .map(|prop| prop.to_string()) .collect::(); let next = if self.next.len() == 1 { self.next .iter() .map(|node| node.to_string()) .collect::>() .join("") } else { self.next .iter() .map(|node| format!("({})", node.to_string())) .collect::>() .join("") }; format!(";{}{}", props, next) } } #[derive(Clone, Debug, PartialEq)] pub enum Move { Move(String), Pass, } // KO // MN // N // AR // CR // DD // LB // LN // MA // SL // SQ // TR // OB // OW // FG // PM // VW #[derive(Clone, Debug, PartialEq)] pub enum Property { // B, W Move((Color, Move)), // C Comment(String), // BM, DO, IT, TE Annotation(Annotation), // AP Application(String), // CA Charset(String), // FF FileFormat(i32), // GM GameType(GameType), // ST VariationDisplay, // SZ BoardSize(Size), // AB SetupBlackStones(PositionList), // AE ClearStones(PositionList), // AW SetupWhiteStones(PositionList), // PL NextPlayer(Color), // DM, GB, GW, UC Evaluation(Evaluation), // HO Hotspot, // V Value(f32), // AN Annotator(String), // BR BlackRank(String), // BT BlackTeam(String), // CP Copyright(String), // DT EventDates(Vec), // EV EventName(String), // GN GameName(String), // GC ExtraGameInformation(String), // ON GameOpening(String), // OT Overtime(String), // PB BlackPlayer(String), // PC GameLocation(String), // PW WhitePlayer(String), // RE Result(GameResult), // RO Round(String), // RU Ruleset(String), // SO Source(String), // TM TimeLimit(Duration), // US User(String), // WR WhiteRank(String), // WT WhiteTeam(String), // BL, WL TimeLeft((Color, Duration)), // TW, TB Territory(Color, Vec), Unknown(UnknownProperty), } #[derive(Clone, Debug, PartialEq)] pub struct UnknownProperty { pub ident: String, pub value: String, } impl ToString for Property { fn to_string(&self) -> String { match self { Property::Move((color, Move::Move(mv))) => { format!("{}[{}]", color.abbreviation(), mv) } Property::Move((color, Move::Pass)) => { format!("{}[]", color.abbreviation()) } Property::TimeLeft((color, time)) => { format!("{}[{}]", color.abbreviation(), time.as_secs()) } Property::Comment(value) => format!("C[{}]", value), Property::Annotation(Annotation::BadMove) => "BM[]".to_owned(), Property::Annotation(Annotation::DoubtfulMove) => "DO[]".to_owned(), Property::Annotation(Annotation::InterestingMove) => "IT[]".to_owned(), Property::Annotation(Annotation::Tesuji) => "TE[]".to_owned(), Property::Application(app) => format!("AP[{}]", app), Property::Charset(set) => format!("CA[{}]", set), Property::FileFormat(ff) => format!("FF[{}]", ff), Property::GameType(gt) => format!("GM[{}]", gt.to_string()), Property::VariationDisplay => unimplemented!(), Property::BoardSize(Size { width, height }) => { if width == height { format!("SZ[{}]", width) } else { format!("SZ[{}:{}]", width, height) } } Property::SetupBlackStones(positions) => { format!("AB[{}]", positions.compressed_list(),) } Property::ClearStones(positions) => { format!("AE[{}]", positions.compressed_list(),) } Property::SetupWhiteStones(positions) => { format!("AW[{}]", positions.compressed_list(),) } Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()), Property::Evaluation(Evaluation::Even) => "DM[]".to_owned(), Property::Evaluation(Evaluation::GoodForBlack) => "GB[]".to_owned(), Property::Evaluation(Evaluation::GoodForWhite) => "GW[]".to_owned(), Property::Evaluation(Evaluation::Unclear) => "UC[]".to_owned(), Property::Hotspot => "HO[]".to_owned(), Property::Value(value) => format!("V[{}]", value), Property::Annotator(value) => format!("AN[{}]", value), Property::BlackRank(value) => format!("BR[{}]", value), Property::BlackTeam(value) => format!("BT[{}]", value), Property::Copyright(value) => format!("CP[{}]", value), Property::EventDates(_) => unimplemented!(), Property::EventName(value) => format!("EV[{}]", value), Property::GameName(value) => format!("GN[{}]", value), Property::ExtraGameInformation(value) => format!("GC[{}]", value), Property::GameOpening(value) => format!("ON[{}]", value), Property::Overtime(value) => format!("OT[{}]", value), Property::BlackPlayer(value) => format!("PB[{}]", value), Property::GameLocation(value) => format!("PC[{}]", value), Property::WhitePlayer(value) => format!("PW[{}]", value), Property::Result(_) => unimplemented!(), Property::Round(value) => format!("RO[{}]", value), Property::Ruleset(value) => format!("RU[{}]", value), Property::Source(value) => format!("SO[{}]", value), Property::TimeLimit(value) => format!("TM[{}]", value.as_secs()), Property::User(value) => format!("US[{}]", value), Property::WhiteRank(value) => format!("WR[{}]", value), Property::WhiteTeam(value) => format!("WT[{}]", value), Property::Territory(Color::White, positions) => { format!( "TW{}", positions .iter() .map(|Position(p)| format!("[{}]", p)) .collect::() ) } Property::Territory(Color::Black, positions) => { format!( "TB{}", positions .iter() .map(|Position(p)| format!("[{}]", p)) .collect::() ) } Property::Unknown(UnknownProperty { ident, value }) => { format!("{}[{}]", ident, value) } } } } pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Vec, E> { let (input, roots) = separated_list1(multispace1, parse_tree)(input)?; let trees = roots .into_iter() .map(|root| Tree { root }) .collect::>(); Ok((input, trees)) } // note: must preserve unknown properties // note: must fix or preserve illegally formatted game-info properties // note: must correct or delete illegally foramtted properties, but display a warning fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { let (input, _) = multispace0(input)?; let (input, _) = tag("(")(input)?; let (input, node) = parse_node(input)?; let (input, _) = multispace0(input)?; let (input, _) = tag(")")(input)?; Ok((input, node)) } fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { let (input, _) = multispace0(input)?; let (input, _) = opt(tag(";"))(input)?; let (input, properties) = many1(parse_property)(input)?; let (input, next) = opt(parse_node)(input)?; let (input, mut next_seq) = many0(parse_tree)(input)?; let mut next = next.map(|n| vec![n]).unwrap_or(vec![]); next.append(&mut next_seq); Ok((input, Node { properties, next })) } fn parse_property<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Property, E> { let (input, _) = multispace0(input)?; let (input, ident) = alpha1(input)?; let (input, prop) = match ident { "W" => parse_propval(parse_move(Color::White))(input)?, "B" => parse_propval(parse_move(Color::Black))(input)?, "C" => parse_propval(parse_comment())(input)?, "WL" => parse_propval(parse_time_left(Color::White))(input)?, "BL" => parse_propval(parse_time_left(Color::Black))(input)?, "BM" => discard_propval() .map(|_| Property::Annotation(Annotation::BadMove)) .parse(input)?, "DO" => discard_propval() .map(|_| Property::Annotation(Annotation::DoubtfulMove)) .parse(input)?, "IT" => discard_propval() .map(|_| Property::Annotation(Annotation::InterestingMove)) .parse(input)?, "TE" => discard_propval() .map(|_| Property::Annotation(Annotation::Tesuji)) .parse(input)?, "AP" => parse_propval(parse_simple_text().map(Property::Application))(input)?, "CA" => parse_propval(parse_simple_text().map(Property::Charset))(input)?, "FF" => parse_propval(parse_number().map(Property::FileFormat))(input)?, "GM" => parse_propval(parse_gametype().map(Property::GameType))(input)?, "ST" => discard_propval() .map(|_| Property::VariationDisplay) .parse(input)?, "SZ" => parse_propval(parse_size().map(Property::BoardSize))(input)?, "DM" => discard_propval() .map(|_| Property::Evaluation(Evaluation::Even)) .parse(input)?, "GB" => discard_propval() .map(|_| Property::Evaluation(Evaluation::GoodForBlack)) .parse(input)?, "GW" => discard_propval() .map(|_| Property::Evaluation(Evaluation::GoodForWhite)) .parse(input)?, "UC" => discard_propval() .map(|_| Property::Evaluation(Evaluation::Unclear)) .parse(input)?, "V" => unimplemented!(), "AN" => parse_propval(parse_simple_text().map(Property::Annotator))(input)?, "BR" => parse_propval(parse_simple_text().map(Property::BlackRank))(input)?, "BT" => parse_propval(parse_simple_text().map(Property::BlackTeam))(input)?, "CP" => parse_propval(parse_simple_text().map(Property::Copyright))(input)?, "DT" => parse_propval(parse_date_field().map(Property::EventDates))(input)?, "EV" => parse_propval(parse_simple_text().map(Property::EventName))(input)?, "GN" => parse_propval(parse_simple_text().map(Property::GameName))(input)?, "GC" => parse_propval(parse_simple_text().map(Property::ExtraGameInformation))(input)?, "ON" => parse_propval(parse_simple_text().map(Property::GameOpening))(input)?, "OT" => parse_propval(parse_simple_text().map(Property::Overtime))(input)?, "PB" => parse_propval(parse_simple_text().map(Property::BlackPlayer))(input)?, "PC" => parse_propval(parse_simple_text().map(Property::GameLocation))(input)?, "PW" => parse_propval(parse_simple_text().map(Property::WhitePlayer))(input)?, "RE" => parse_propval(parse_game_result().map(Property::Result))(input)?, "RO" => parse_propval(parse_simple_text().map(Property::Round))(input)?, "RU" => parse_propval(parse_simple_text().map(Property::Ruleset))(input)?, "SO" => parse_propval(parse_simple_text().map(Property::Source))(input)?, "TM" => parse_propval( parse_real().map(|seconds| Property::TimeLimit(Duration::from_secs(seconds as u64))), )(input)?, "US" => parse_propval(parse_simple_text().map(Property::User))(input)?, "WR" => parse_propval(parse_simple_text().map(Property::WhiteRank))(input)?, "WT" => parse_propval(parse_simple_text().map(Property::WhiteTeam))(input)?, "TW" => parse_territory() .map(|p| Property::Territory(Color::White, p)) .parse(input)?, "TB" => parse_territory() .map(|p| Property::Territory(Color::Black, p)) .parse(input)?, _ => parse_propval(parse_simple_text().map(|value| { Property::Unknown(UnknownProperty { ident: ident.to_owned(), value, }) }))(input)?, }; Ok((input, prop)) } fn parse_comment<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Property, E> { parse_text().map(Property::Comment) } fn parse_move<'a, E: nom::error::ParseError<&'a str>>( color: Color, ) -> impl Parser<&'a str, Property, E> { { let color = color.clone(); move |input: &'a str| { take_until("]") .map(|text: &'a str| { if text.is_empty() { Property::Move((color.clone(), Move::Pass)) } else { Property::Move((color.clone(), Move::Move(text.to_owned()))) } }) .parse(input) } } } fn parse_gametype<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameType, E> { |input: &'a str| take_until1("]").map(GameType::from).parse(input) } fn parse_time_left<'a, E: ParseError<&'a str>>( color: Color, ) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> { { let color = color.clone(); move |input: &'a str| { let (input, value) = parse_real().parse(input)?; Ok(( input, Property::TimeLeft((color.clone(), Duration::from_secs(value as u64))), )) } } } fn parse_size<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Size, E> { |input: &'a str| { let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?; let (width, height) = match dimensions.as_slice() { [width] => (width.parse::().unwrap(), width.parse::().unwrap()), [width, height] => ( width.parse::().unwrap(), height.parse::().unwrap(), ), _ => (19, 19), }; Ok((input, Size { width, height })) } } fn parse_game_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> { alt((parse_draw_result(), parse_void_result(), parse_win_result())) } fn parse_territory<'a, E: nom::error::ParseError<&'a str>>( ) -> impl Parser<&'a str, Vec, E> { many1(|input| { let (input, _) = tag("[")(input)?; let (input, position) = parse_simple_text().map(Position).parse(input)?; let (input, _) = tag("]")(input)?; Ok((input, position)) }) } fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( mut parser: impl Parser<&'a str, Property, E>, ) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> { move |input| { let (input, _) = multispace0(input)?; let (input, _) = tag("[")(input)?; let (input, value) = parser.parse(input)?; let (input, _) = tag("]")(input)?; Ok((input, value)) } } fn discard_propval<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, (), E> { |input| { let (input, _) = multispace0(input)?; let (input, _) = tag("[")(input)?; let (input, _) = parse_text().parse(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } } fn parse_number<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, i32, E> { |input| { let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?; let (input, value) = digit1(input)?; let mult = if sign == Some("-") { -1 } else { 1 }; Ok((input, value.parse::().unwrap() * mult)) } } fn parse_real<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, f32, E> { |input| { let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?; let (input, whole_value) = digit1(input)?; let (input, fractional_value) = opt(|input| { let (input, _) = tag(".")(input)?; let (input, fractional_value) = digit1(input)?; Ok((input, format!(".{}", fractional_value))) })(input)?; let value = format!( "{}{}{}", sign.unwrap_or("+"), whole_value, fractional_value.unwrap_or("".to_owned()) ) .parse::() .unwrap(); Ok((input, value)) } } fn parse_simple_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> { |input| { let (input, value) = opt(escaped_transform( none_of("\\]"), '\\', alt(( value("]", tag("]")), value("\\", tag("\\")), value("", tag("\n")), )), ))(input)?; Ok((input, value.unwrap_or("".to_owned()))) } } fn parse_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> { |input| { let (input, value) = opt(escaped_transform( none_of("\\]"), '\\', alt(( value("]", tag("]")), value("\\", tag("\\")), value("", tag("\n")), )), ))(input)?; Ok((input, value.unwrap_or("".to_owned()))) } } enum DateSegment { One(i32), Two(i32, i32), Three(i32, i32, i32), } fn parse_date_field<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, Vec, E> { |input| { let (input, first_date) = parse_date().parse(input)?; let (input, mut more_dates) = many0(parse_date_segment())(input)?; let mut date_segments = vec![first_date]; date_segments.append(&mut more_dates); let mut dates = vec![]; let mut most_recent = None; for date_segment in date_segments { let new_date = match date_segment { DateSegment::One(v) => match most_recent { Some(Date::Year(_)) => Date::Year(v), Some(Date::YearMonth(y, _)) => Date::YearMonth(y, v as u32), Some(Date::Date(d)) => Date::Date(d.clone().with_day(v as u32).unwrap()), None => Date::Year(v), }, DateSegment::Two(y, m) => Date::YearMonth(y, m as u32), DateSegment::Three(y, m, d) => { Date::Date(chrono::NaiveDate::from_ymd_opt(y, m as u32, d as u32).unwrap()) } }; dates.push(new_date.clone()); most_recent = Some(new_date); } Ok((input, dates)) } } fn parse_date_segment<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { |input| { let (input, _) = tag(",")(input)?; let (input, element) = parse_date().parse(input)?; Ok((input, element)) } } fn parse_date<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { alt((parse_three(), parse_two(), parse_one())) } fn parse_one<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { parse_number().map(DateSegment::One) } fn parse_two<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { |input| { let (input, year) = parse_number().parse(input)?; let (input, _) = tag("-")(input)?; let (input, month) = parse_number().parse(input)?; Ok((input, DateSegment::Two(year, month))) } } fn parse_three<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { |input| { let (input, year) = parse_number().parse(input)?; let (input, _) = tag("-")(input)?; let (input, month) = parse_number().parse(input)?; let (input, _) = tag("-")(input)?; let (input, day) = parse_number().parse(input)?; Ok((input, DateSegment::Three(year, month, day))) } } fn parse_draw_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> { tag("0").map(|_| GameResult::Draw) } fn parse_void_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> { tag("Void").map(|_| GameResult::Void) } fn parse_win_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> { |input: &'a str| { let (input, color) = alt(( tag("B").map(|_| Color::Black), tag("b").map(|_| Color::Black), tag("W").map(|_| Color::White), tag("w").map(|_| Color::White), ))(input)?; let (input, _) = tag("+")(input)?; let (input, score) = parse_win_score().parse(input)?; Ok(( input, match color { Color::Black => GameResult::Black(score), Color::White => GameResult::White(score), }, )) } } enum WinType { St(String), Num(f32), } fn parse_win_score<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Win, E> { |input: &'a str| { let (input, win) = alt(( parse_real().map(WinType::Num), parse_simple_text().map(WinType::St), ))(input)?; let w = match win { WinType::St(s) => match s.to_ascii_lowercase().as_str() { "r" | "resign" => Win::Resignation, "t" | "time" => Win::Time, "f" | "forfeit" => Win::Forfeit, _ => Win::Unknown, }, WinType::Num(n) => Win::Score(n), }; Ok((input, w)) } } #[cfg(test)] mod test { use super::*; const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) (;C[d];C[e])) (;C[f](;C[g];C[h];C[i]) (;C[j])))"; #[test] fn it_can_parse_properties() { let (_, prop) = parse_property::>("C[a]").unwrap(); assert_eq!(prop, Property::Comment("a".to_owned())); } #[test] fn it_can_parse_a_standalone_node() { let (_, node) = parse_node::>(";B[ab]").unwrap(); assert_eq!( node, Node { properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![] } ); let (_, node) = parse_node::>(";B[ab];W[dp];B[pq]C[some comments]") .unwrap(); assert_eq!( node, Node { properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![Node { properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], next: vec![Node { properties: vec![ Property::Move((Color::Black, Move::Move("pq".to_owned()))), Property::Comment("some comments".to_owned()) ], next: vec![], }] }] } ); } #[test] fn it_can_parse_a_simple_sequence() { let (_, sequence) = parse_tree::>("(;B[ab];W[dp];B[pq]C[some comments])") .unwrap(); assert_eq!( sequence, Node { properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![Node { properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], next: vec![Node { properties: vec![ Property::Move((Color::Black, Move::Move("pq".to_owned()))), Property::Comment("some comments".to_owned()) ], next: vec![], }] }], }, ); } #[test] fn it_can_parse_a_branching_sequence() { let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; let (_, tree) = parse_tree::>(text).unwrap(); let expected = Node { properties: vec![Property::Comment("a".to_owned())], next: vec![Node { properties: vec![Property::Comment("b".to_owned())], next: vec![ Node { properties: vec![Property::Comment("c".to_owned())], next: vec![], }, Node { properties: vec![Property::Comment("d".to_owned())], next: vec![Node { properties: vec![Property::Comment("e".to_owned())], next: vec![], }], }, ], }], }; assert_eq!(tree, expected); } #[test] fn it_can_parse_example_1() { let (_, tree) = parse_tree::>(EXAMPLE).unwrap(); let j = Node { properties: vec![Property::Comment("j".to_owned())], next: vec![], }; let i = Node { properties: vec![Property::Comment("i".to_owned())], next: vec![], }; let h = Node { properties: vec![Property::Comment("h".to_owned())], next: vec![i], }; let g = Node { properties: vec![Property::Comment("g".to_owned())], next: vec![h], }; let f = Node { properties: vec![Property::Comment("f".to_owned())], next: vec![g, j], }; let e = Node { properties: vec![Property::Comment("e".to_owned())], next: vec![], }; let d = Node { properties: vec![Property::Comment("d".to_owned())], next: vec![e], }; let c = Node { properties: vec![Property::Comment("c".to_owned())], next: vec![], }; let b = Node { properties: vec![Property::Comment("b".to_owned())], next: vec![c, d], }; let a = Node { properties: vec![Property::Comment("a".to_owned())], next: vec![b], }; let expected = Node { properties: vec![ Property::FileFormat(4), Property::Comment("root".to_owned()), ], next: vec![a, f], }; assert_eq!(tree, expected); } #[test] fn it_can_regenerate_the_tree() { let (_, tree1) = parse_tree::>(EXAMPLE).unwrap(); let tree1 = Tree { root: tree1 }; assert_eq!( tree1.to_string(), "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))" ); let (_, tree2) = parse_tree::>(&tree1.to_string()).unwrap(); assert_eq!(tree1, Tree { root: tree2 }); } #[test] fn it_parses_propvals() { let (_, propval) = parse_propval::>(parse_comment()) .parse("[]") .unwrap(); assert_eq!(propval, Property::Comment("".to_owned())); let (_, propval) = parse_propval::>(parse_comment()) .parse("[normal propval]") .unwrap(); assert_eq!(propval, Property::Comment("normal propval".to_owned())); let (_, propval) = parse_propval::>(parse_comment()) .parse(r"[need an [escape\] in the propval]") .unwrap(); assert_eq!( propval, Property::Comment("need an [escape] in the propval".to_owned()) ); } #[test] fn it_parses_propvals_with_hard_linebreaks() { let (_, propval) = parse_text::>() .parse( "There are hard linebreaks & soft linebreaks. Soft linebreaks...", ) .unwrap(); assert_eq!( propval, "There are hard linebreaks & soft linebreaks. Soft linebreaks..." ); } #[test] fn it_parses_propvals_with_escaped_closing_brackets() { let (_, propval) = parse_text::>() .parse(r"escaped closing \] bracket") .unwrap(); assert_eq!(propval, r"escaped closing ] bracket".to_owned()); } #[test] fn it_parses_propvals_with_soft_linebreaks() { let (_, propval) = parse_text::>() .parse( r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ k<. Hard line breaks are all other linebreaks.", ) .unwrap(); assert_eq!( propval, "Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks." ); } #[test] fn it_parses_sgf_with_newline_in_sequence() { let data = String::from( "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e] ))(;C[f](;C[g];C[h];C[i])(;C[j])))", ); parse_tree::>(&data).unwrap(); } #[test] fn it_parses_sgf_with_newline_between_two_sequence_closings() { let data = String::from( "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]) )(;C[f](;C[g];C[h];C[i])(;C[j])))", ); parse_tree::>(&data).unwrap(); } } #[cfg(test)] mod date_test { use super::*; use chrono::NaiveDate; use cool_asserts::assert_matches; #[test] fn it_parses_a_year() { assert_matches!(parse_date_field::>().parse("1996"), Ok((_, date)) => { assert_eq!(date, vec![Date::Year(1996)]); }); } #[test] fn it_parses_a_month() { assert_matches!( parse_date_field::>().parse("1996-12"), Ok((_, date)) => assert_eq!(date, vec![Date::YearMonth(1996, 12)]) ); } #[test] fn it_parses_a_date() { assert_matches!( parse_date_field::>().parse("1996-12-27"), Ok((_, date)) => assert_eq!(date, vec![Date::Date( NaiveDate::from_ymd_opt(1996, 12, 27).unwrap() )]) ); } #[test] fn it_parses_date_continuation() { assert_matches!( parse_date_field::>().parse("1996-12-27,28"), Ok((_, date)) => assert_eq!(date, 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_matches!( parse_date_field::>().parse("1996-12-27,28,1997-01-03,04"), Ok((_, date)) => assert_eq!(date, 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()), ]) ); } } #[cfg(test)] mod property_tests { use super::*; #[test] fn it_can_parse_time_left() { let (_, val) = parse_time_left::>(Color::Black)("170]").unwrap(); assert_eq!( val, Property::TimeLeft((Color::Black, Duration::from_secs(170))) ); let (_, val) = parse_time_left::>(Color::Black)("170.6]").unwrap(); assert_eq!( val, Property::TimeLeft((Color::Black, Duration::from_secs(170))) ); let (_, prop) = parse_property::>("BL[170]").unwrap(); assert_eq!( prop, Property::TimeLeft((Color::Black, Duration::from_secs(170))) ); let (_, prop) = parse_property::>("BL[170.5]").unwrap(); assert_eq!( prop, Property::TimeLeft((Color::Black, Duration::from_secs(170))) ); } }