monorepo/sgf/src/parser.rs

1304 lines
41 KiB
Rust
Raw Normal View History

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,
};
2024-03-22 03:23:47 +00:00
use serde::{Deserialize, Serialize};
2024-03-26 12:46:54 +00:00
use std::{fmt::Write, num::ParseIntError, time::Duration};
impl From<ParseSizeError> for Error {
fn from(_: ParseSizeError) -> Self {
Self::InvalidBoardSize
}
}
#[derive(Debug)]
pub enum ParseSizeError {
ParseIntError(ParseIntError),
InsufficientArguments,
}
impl From<ParseIntError> for ParseSizeError {
fn from(e: ParseIntError) -> Self {
Self::ParseIntError(e)
}
}
2024-03-22 03:23:47 +00:00
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum Annotation {
BadMove,
DoubtfulMove,
InterestingMove,
Tesuji,
}
2024-03-22 03:23:47 +00:00
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
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)
}
}
2024-03-22 03:23:47 +00:00
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Size {
pub width: i32,
pub height: i32,
}
impl TryFrom<&str> for Size {
type Error = ParseSizeError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let parts = s
.split(':')
.map(|v| v.parse::<i32>())
.collect::<Result<Vec<i32>, 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<String>);
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<Property>,
pub next: Vec<Node>,
}
2024-03-22 03:23:47 +00:00
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
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<Vec<SetupInstr>> {
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::<Vec<SetupInstr>>(),
);
}
Property::SetupWhiteStones(positions) => {
setup.append(
&mut positions
.0
.iter()
.map(|pos| SetupInstr::Piece((Color::White, pos.clone())))
.collect::<Vec<SetupInstr>>(),
);
}
Property::ClearStones(positions) => {
setup.append(
&mut positions
.0
.iter()
.map(|pos| SetupInstr::Clear(pos.clone()))
.collect::<Vec<SetupInstr>>(),
);
}
_ => return None,
}
}
if !setup.is_empty() {
Some(setup)
} else {
None
}
}
fn find_by<F, R>(&self, f: F) -> Option<R>
where
F: FnMut(&Property) -> Option<R>,
{
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::<String>();
let next = if self.next.len() == 1 {
self.next
.iter()
.map(|node| node.to_string())
.collect::<Vec<String>>()
.join("")
} else {
self.next
.iter()
.map(|node| format!("({})", node.to_string()))
.collect::<Vec<String>>()
.join("")
};
format!(";{}{}", props, next)
}
}
2024-03-22 03:23:47 +00:00
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum Move {
Move(String),
Pass,
}
impl Move {
pub fn coordinate(&self) -> Option<(u8, u8)> {
match self {
Move::Pass => None,
Move::Move(s) => {
if s.len() == 2 {
let mut parts = s.chars();
let column_char = parts.next().unwrap();
2024-03-26 12:46:54 +00:00
let column = column_char as u8 - b'a';
let row_char = parts.next().unwrap();
let row = row_char as u8 - b'a';
Some((row, column))
} else {
unimplemented!("moves must contain exactly two characters");
}
}
}
}
}
// 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<Date>),
// 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<Position>),
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) => {
2024-03-26 12:46:54 +00:00
positions
.iter()
.fold("TW".to_owned(), |mut output, Position(p)| {
let _ = write!(output, "{}", p);
output
})
}
Property::Territory(Color::Black, positions) => {
2024-03-26 12:46:54 +00:00
positions
.iter()
.fold("TB".to_owned(), |mut output, Position(p)| {
let _ = write!(output, "{}", p);
output
})
}
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<Tree>, E> {
let (input, roots) = separated_list1(multispace1, parse_tree)(input)?;
let trees = roots
.into_iter()
.map(|root| Tree { root })
.collect::<Vec<Tree>>();
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::<i32>().unwrap(), width.parse::<i32>().unwrap()),
[width, height] => (
width.parse::<i32>().unwrap(),
height.parse::<i32>().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<Position>, 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::<i32>().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::<f32>()
.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<Date>, 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::*;
2024-03-26 12:46:54 +00:00
const EXAMPLE: &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::<nom::error::VerboseError<&str>>("C[a]").unwrap();
assert_eq!(prop, Property::Comment("a".to_owned()));
}
#[test]
fn it_can_parse_a_standalone_node() {
let (_, node) = parse_node::<nom::error::VerboseError<&str>>(";B[ab]").unwrap();
assert_eq!(
node,
Node {
properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))],
next: vec![]
}
);
let (_, node) =
parse_node::<nom::error::VerboseError<&str>>(";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::<nom::error::VerboseError<&str>>("(;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::<nom::error::VerboseError<&str>>(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::<nom::error::VerboseError<&str>>(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::<nom::error::VerboseError<&str>>(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::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
assert_eq!(tree1, Tree { root: tree2 });
}
#[test]
fn it_parses_propvals() {
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
.parse("[]")
.unwrap();
assert_eq!(propval, Property::Comment("".to_owned()));
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
.parse("[normal propval]")
.unwrap();
assert_eq!(propval, Property::Comment("normal propval".to_owned()));
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(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::<nom::error::VerboseError<&str>>()
.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::<nom::error::VerboseError<&str>>()
.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::<nom::error::VerboseError<&str>>()
.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::<nom::error::VerboseError<&str>>(&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::<nom::error::VerboseError<&str>>(&data).unwrap();
}
#[test]
fn it_can_convert_moves_to_coordinates() {
assert_eq!(Move::Pass.coordinate(), None);
assert_eq!(Move::Move("dd".to_owned()).coordinate(), Some((3, 3)));
assert_eq!(Move::Move("jj".to_owned()).coordinate(), Some((9, 9)));
assert_eq!(Move::Move("pp".to_owned()).coordinate(), Some((15, 15)));
}
}
#[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::<nom::error::VerboseError<&str>>().parse("1996"), Ok((_, date)) => {
assert_eq!(date, vec![Date::Year(1996)]);
});
}
#[test]
fn it_parses_a_month() {
assert_matches!(
parse_date_field::<nom::error::VerboseError<&str>>().parse("1996-12"),
Ok((_, date)) => assert_eq!(date, vec![Date::YearMonth(1996, 12)])
);
}
#[test]
fn it_parses_a_date() {
assert_matches!(
parse_date_field::<nom::error::VerboseError<&str>>().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::<nom::error::VerboseError<&str>>().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::<nom::error::VerboseError<&str>>().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::<nom::error::VerboseError<&str>>(Color::Black)("170]").unwrap();
assert_eq!(
val,
Property::TimeLeft((Color::Black, Duration::from_secs(170)))
);
let (_, val) =
parse_time_left::<nom::error::VerboseError<&str>>(Color::Black)("170.6]").unwrap();
assert_eq!(
val,
Property::TimeLeft((Color::Black, Duration::from_secs(170)))
);
let (_, prop) = parse_property::<nom::error::VerboseError<&str>>("BL[170]").unwrap();
assert_eq!(
prop,
Property::TimeLeft((Color::Black, Duration::from_secs(170)))
);
let (_, prop) = parse_property::<nom::error::VerboseError<&str>>("BL[170.5]").unwrap();
assert_eq!(
prop,
Property::TimeLeft((Color::Black, Duration::from_secs(170)))
);
}
}