1277 lines
40 KiB
Rust
1277 lines
40 KiB
Rust
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<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)
|
|
}
|
|
}
|
|
|
|
#[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<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>,
|
|
}
|
|
|
|
#[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<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)
|
|
}
|
|
}
|
|
|
|
#[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<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) => {
|
|
format!(
|
|
"TW{}",
|
|
positions
|
|
.iter()
|
|
.map(|Position(p)| format!("[{}]", p))
|
|
.collect::<String>()
|
|
)
|
|
}
|
|
Property::Territory(Color::Black, positions) => {
|
|
format!(
|
|
"TB{}",
|
|
positions
|
|
.iter()
|
|
.map(|Position(p)| format!("[{}]", p))
|
|
.collect::<String>()
|
|
)
|
|
}
|
|
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::*;
|
|
|
|
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::<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();
|
|
}
|
|
}
|
|
|
|
#[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)))
|
|
);
|
|
}
|
|
}
|