Compare commits

..

No commits in common. "f28a64d9a6ece966317e5246a790bb75f5208153" and "b2ba257cac1d2ae5287823efc4ee890d65a6a768" have entirely different histories.

4 changed files with 163 additions and 349 deletions

View File

@ -11,6 +11,3 @@ nom = { version = "7" }
serde = { version = "1", features = [ "derive" ] } serde = { version = "1", features = [ "derive" ] }
thiserror = { version = "1"} thiserror = { version = "1"}
typeshare = { version = "1" } typeshare = { version = "1" }
[dev-dependencies]
cool_asserts = { version = "2" }

View File

@ -74,7 +74,6 @@ use crate::{
Error, Error,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::Deref;
use typeshare::typeshare; use typeshare::typeshare;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -85,18 +84,11 @@ pub struct Game {
pub tree: Tree, pub tree: Tree,
} }
impl Deref for Game {
type Target = Tree;
fn deref(&self) -> &Self::Target {
&self.tree
}
}
impl TryFrom<Tree> for Game { impl TryFrom<Tree> for Game {
type Error = Error; type Error = Error;
fn try_from(tree: Tree) -> Result<Self, Self::Error> { fn try_from(tree: Tree) -> Result<Self, Self::Error> {
let board_size = match tree.root.find_prop("SZ") { let board_size = match tree.sequence[0].find_prop("SZ") {
Some(prop) => Size::try_from(prop.values[0].as_str())?, Some(prop) => Size::try_from(prop.values[0].as_str())?,
None => Size { None => Size {
width: 19, width: 19,
@ -104,34 +96,35 @@ impl TryFrom<Tree> for Game {
}, },
}; };
let mut info = GameInfo::default(); let mut info = GameInfo::default();
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone()); info.app_name = tree.sequence[0]
info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", ")); .find_prop("AP")
.map(|prop| prop.values[0].clone());
info.black_player = tree.sequence[0]
.find_prop("PB")
.map(|prop| prop.values.join(", "));
info.black_rank = tree info.black_rank = tree.sequence[0]
.root
.find_prop("BR") .find_prop("BR")
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
info.white_player = tree.root.find_prop("PW").map(|prop| prop.values.join(", ")); info.white_player = tree.sequence[0]
.find_prop("PW")
.map(|prop| prop.values.join(", "));
info.white_rank = tree info.white_rank = tree.sequence[0]
.root
.find_prop("WR") .find_prop("WR")
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
info.result = tree info.result = tree.sequence[0]
.root
.find_prop("RE") .find_prop("RE")
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok()); .and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
info.time_limits = tree info.time_limits = tree.sequence[0]
.root
.find_prop("TM") .find_prop("TM")
.and_then(|prop| prop.values[0].parse::<u64>().ok()) .and_then(|prop| prop.values[0].parse::<u64>().ok())
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds))); .and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
info.date = tree info.date = tree.sequence[0]
.root
.find_prop("DT") .find_prop("DT")
.and_then(|prop| { .and_then(|prop| {
let v = prop let v = prop
@ -151,13 +144,21 @@ impl TryFrom<Tree> for Game {
}) })
.unwrap_or(vec![]); .unwrap_or(vec![]);
info.event = tree.root.find_prop("EV").map(|prop| prop.values.join(", ")); info.event = tree.sequence[0]
.find_prop("EV")
.map(|prop| prop.values.join(", "));
info.round = tree.root.find_prop("RO").map(|prop| prop.values.join(", ")); info.round = tree.sequence[0]
.find_prop("RO")
.map(|prop| prop.values.join(", "));
info.source = tree.root.find_prop("SO").map(|prop| prop.values.join(", ")); info.source = tree.sequence[0]
.find_prop("SO")
.map(|prop| prop.values.join(", "));
info.game_keeper = tree.root.find_prop("US").map(|prop| prop.values.join(", ")); info.game_keeper = tree.sequence[0]
.find_prop("US")
.map(|prop| prop.values.join(", "));
Ok(Game { Ok(Game {
board_size, board_size,
@ -300,7 +301,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
date::Date, date::Date,
tree::{parse_collection, Property, Size}, tree::{parse_collection, Size},
}; };
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@ -383,123 +384,4 @@ mod tests {
assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned())); assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned()));
}); });
} }
#[test]
fn it_presents_the_mainline_of_game_without_branches() {
with_file(
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|trees| {
assert_eq!(trees.len(), 1);
let tree = &trees[0];
let node = &tree.root;
assert_eq!(node.properties.len(), 16);
let expected_properties = vec![
Property {
ident: "GM".to_owned(),
values: vec!["1".to_owned()],
},
Property {
ident: "FF".to_owned(),
values: vec!["4".to_owned()],
},
Property {
ident: "CA".to_owned(),
values: vec!["UTF-8".to_owned()],
},
Property {
ident: "AP".to_owned(),
values: vec!["CGoban:3".to_owned()],
},
Property {
ident: "ST".to_owned(),
values: vec!["2".to_owned()],
},
Property {
ident: "RU".to_owned(),
values: vec!["AGA".to_owned()],
},
Property {
ident: "SZ".to_owned(),
values: vec!["19".to_owned()],
},
Property {
ident: "KM".to_owned(),
values: vec!["7.50".to_owned()],
},
Property {
ident: "TM".to_owned(),
values: vec!["1800".to_owned()],
},
Property {
ident: "OT".to_owned(),
values: vec!["5x30 byo-yomi".to_owned()],
},
Property {
ident: "PW".to_owned(),
values: vec!["Geckoz".to_owned()],
},
Property {
ident: "PB".to_owned(),
values: vec!["savanni".to_owned()],
},
Property {
ident: "BR".to_owned(),
values: vec!["23k".to_owned()],
},
Property {
ident: "DT".to_owned(),
values: vec!["2020-08-05".to_owned()],
},
Property {
ident: "PC".to_owned(),
values: vec!["The KGS Go Server at http://www.gokgs.com/".to_owned()],
},
Property {
ident: "RE".to_owned(),
values: vec!["W+17.50".to_owned()],
},
];
for i in 0..16 {
assert_eq!(node.properties[i], expected_properties[i]);
}
let node = node.next().unwrap();
let expected_properties = vec![
Property {
ident: "B".to_owned(),
values: vec!["pp".to_owned()],
},
Property {
ident: "BL".to_owned(),
values: vec!["1795.449".to_owned()],
},
Property {
ident: "C".to_owned(),
values: vec!["Geckoz [?]: Good game\nsavanni [23k?]: There we go! This UI is... tough.\nsavanni [23k?]: Have fun! Talk to you at the end.\nGeckoz [?]: Yeah, OGS is much better; I'm a UX professional\n".to_owned()],
}
];
for i in 0..3 {
assert_eq!(node.properties[i], expected_properties[i]);
}
let node = node.next().unwrap();
let expected_properties = vec![
Property {
ident: "W".to_owned(),
values: vec!["dp".to_owned()],
},
Property {
ident: "WL".to_owned(),
values: vec!["1765.099".to_owned()],
},
];
for i in 0..2 {
assert_eq!(node.properties[i], expected_properties[i]);
}
},
);
}
} }

View File

@ -65,7 +65,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?; let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
Ok(trees Ok(trees
.into_iter() .into_iter()
.map(|t| match t.root.find_prop("GM") { .map(|t| match t.sequence[0].find_prop("GM") {
Some(prop) if prop.values == vec!["1".to_owned()] => { Some(prop) if prop.values == vec!["1".to_owned()] => {
Game::Go(go::Game::try_from(t).expect("properly structured game tree")) Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
} }

View File

@ -5,6 +5,7 @@ use nom::{
character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
combinator::{opt, value}, combinator::{opt, value},
multi::{many0, many1, separated_list1}, multi::{many0, many1, separated_list1},
sequence::delimited,
IResult, IResult,
}; };
use std::num::ParseIntError; use std::num::ParseIntError;
@ -53,19 +54,29 @@ impl TryFrom<&str> for Size {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Tree { pub struct Tree {
pub root: Node, pub sequence: Vec<Node>,
pub sub_sequences: Vec<Tree>,
} }
impl ToString for Tree { impl ToString for Tree {
fn to_string(&self) -> String { fn to_string(&self) -> String {
format!("({})", self.root.to_string()) let sequence = self
.sequence
.iter()
.map(|node| node.to_string())
.collect::<String>();
let subsequences = self
.sub_sequences
.iter()
.map(|seq| seq.to_string())
.collect::<String>();
format!("({}{})", sequence, subsequences)
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Node { pub struct Node {
pub properties: Vec<Property>, pub properties: Vec<Property>,
pub next: Vec<Node>,
} }
impl ToString for Node { impl ToString for Node {
@ -75,21 +86,7 @@ impl ToString for Node {
.iter() .iter()
.map(|prop| prop.to_string()) .map(|prop| prop.to_string())
.collect::<String>(); .collect::<String>();
format!(";{}", props)
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)
} }
} }
@ -100,10 +97,6 @@ impl Node {
.find(|prop| prop.ident == ident) .find(|prop| prop.ident == ident)
.cloned() .cloned()
} }
pub fn next<'a>(&'a self) -> Option<&'a Node> {
self.next.get(0)
}
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -126,40 +119,40 @@ impl ToString for Property {
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
input: &'a str, input: &'a str,
) -> IResult<&'a str, Vec<Tree>, E> { ) -> IResult<&'a str, Vec<Tree>, E> {
let (input, roots) = separated_list1(multispace1, parse_tree)(input)?; 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 preserve unknown properties
// note: must fix or preserve illegally formatted game-info properties // note: must fix or preserve illegally formatted game-info properties
// note: must correct or delete illegally foramtted properties, but display a warning // 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> { fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> {
let (input, _) = multispace0(input)?; let (input, _) = multispace0(input)?;
let (input, _) = tag("(")(input)?; delimited(tag("("), parse_sequence, tag(")"))(input)
let (input, node) = parse_node(input)?; }
let (input, _) = multispace0(input)?;
let (input, _) = tag(")")(input)?;
Ok((input, node)) fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Tree, E> {
let (input, _) = multispace0(input)?;
let (input, nodes) = many1(parse_node)(input)?;
let (input, _) = multispace0(input)?;
let (input, sub_sequences) = many0(parse_tree)(input)?;
let (input, _) = multispace0(input)?;
Ok((
input,
Tree {
sequence: nodes,
sub_sequences,
},
))
} }
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
let (input, _) = multispace0(input)?; let (input, _) = multispace0(input)?;
let (input, _) = opt(tag(";"))(input)?; let (input, _) = tag(";")(input)?;
let (input, properties) = many1(parse_property)(input)?; let (input, properties) = many1(parse_property)(input)?;
Ok((input, Node { properties }))
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>>( fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
@ -226,6 +219,8 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{fs::File, io::Read};
use super::*; use super::*;
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
@ -264,8 +259,7 @@ mod test {
properties: vec![Property { properties: vec![Property {
ident: "B".to_owned(), ident: "B".to_owned(),
values: vec!["ab".to_owned()] values: vec!["ab".to_owned()]
}], }]
next: vec![]
} }
); );
@ -279,25 +273,6 @@ mod test {
properties: vec![Property { properties: vec![Property {
ident: "B".to_owned(), ident: "B".to_owned(),
values: vec!["ab".to_owned()] values: vec!["ab".to_owned()]
}],
next: vec![Node {
properties: vec![Property {
ident: "W".to_owned(),
values: vec!["dp".to_owned()]
}],
next: vec![Node {
properties: vec![
Property {
ident: "B".to_owned(),
values: vec!["pq".to_owned()]
},
Property {
ident: "C".to_owned(),
values: vec!["some comments".to_owned()]
}
],
next: vec![],
}]
}] }]
} }
); );
@ -311,17 +286,21 @@ mod test {
assert_eq!( assert_eq!(
sequence, sequence,
Tree {
sequence: vec![
Node { Node {
properties: vec![Property { properties: vec![Property {
ident: "B".to_owned(), ident: "B".to_owned(),
values: vec!["ab".to_owned()] values: vec!["ab".to_owned()]
}], }]
next: vec![Node { },
Node {
properties: vec![Property { properties: vec![Property {
ident: "W".to_owned(), ident: "W".to_owned(),
values: vec!["dp".to_owned()] values: vec!["dp".to_owned()]
}], }]
next: vec![Node { },
Node {
properties: vec![ properties: vec![
Property { Property {
ident: "B".to_owned(), ident: "B".to_owned(),
@ -331,158 +310,114 @@ mod test {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["some comments".to_owned()] values: vec!["some comments".to_owned()]
} }
]
}
], ],
next: vec![], sub_sequences: vec![],
}] }
}],
},
); );
} }
#[test] #[test]
fn it_can_parse_a_branching_sequence() { fn it_can_parse_a_sequence_with_subsequences() {
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap(); let (_, sequence) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
let expected = Node { let main_sequence = vec![
Node {
properties: vec![Property { properties: vec![Property {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["a".to_owned()], values: vec!["a".to_owned()],
}], }],
next: vec![Node { },
Node {
properties: vec![Property { properties: vec![Property {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["b".to_owned()], values: vec!["b".to_owned()],
}], }],
next: vec![ },
Node { ];
let subsequence_1 = Tree {
sequence: vec![Node {
properties: vec![Property { properties: vec![Property {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["c".to_owned()], values: vec!["c".to_owned()],
}], }],
next: vec![], }],
}, sub_sequences: vec![],
};
let subsequence_2 = Tree {
sequence: vec![
Node { Node {
properties: vec![Property { properties: vec![Property {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["d".to_owned()], values: vec!["d".to_owned()],
}], }],
next: vec![Node { },
Node {
properties: vec![Property { properties: vec![Property {
ident: "C".to_owned(), ident: "C".to_owned(),
values: vec!["e".to_owned()], values: vec!["e".to_owned()],
}], }],
next: vec![],
}],
}, },
], ],
}], sub_sequences: vec![],
}; };
assert_eq!(tree, expected); assert_eq!(
sequence,
Tree {
sequence: main_sequence,
sub_sequences: vec![subsequence_1, subsequence_2],
}
);
} }
#[test] #[test]
fn it_can_parse_example_1() { fn it_can_parse_example_1() {
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap(); let (_, ex_tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
assert_eq!(ex_tree.sequence.len(), 1);
let j = Node { assert_eq!(ex_tree.sequence[0].properties.len(), 2);
properties: vec![Property { assert_eq!(
ident: "C".to_owned(), ex_tree.sequence[0].properties[0],
values: vec!["j".to_owned()],
}],
next: vec![],
};
let i = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["i".to_owned()],
}],
next: vec![],
};
let h = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["h".to_owned()],
}],
next: vec![i],
};
let g = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["g".to_owned()],
}],
next: vec![h],
};
let f = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["f".to_owned()],
}],
next: vec![g, j],
};
let e = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["e".to_owned()],
}],
next: vec![],
};
let d = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["d".to_owned()],
}],
next: vec![e],
};
let c = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["c".to_owned()],
}],
next: vec![],
};
let b = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["b".to_owned()],
}],
next: vec![c, d],
};
let a = Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["a".to_owned()],
}],
next: vec![b],
};
let expected = Node {
properties: vec![
Property { Property {
ident: "FF".to_owned(), ident: "FF".to_owned(),
values: vec!["4".to_owned()], values: vec!["4".to_owned()]
}, }
Property { );
ident: "C".to_owned(), assert_eq!(ex_tree.sub_sequences.len(), 2);
values: vec!["root".to_owned()],
},
],
next: vec![a, f],
};
assert_eq!(tree, expected); assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2);
assert_eq!(
ex_tree.sub_sequences[0].sequence,
vec![
Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["a".to_owned()]
}]
},
Node {
properties: vec![Property {
ident: "C".to_owned(),
values: vec!["b".to_owned()]
}]
},
]
);
assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2);
} }
#[test] #[test]
fn it_can_regenerate_the_tree() { fn it_can_regenerate_the_tree() {
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap(); let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
let tree1 = Tree { root: tree1 };
assert_eq!( assert_eq!(
tree1.to_string(), 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])))" "(;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(); let (_, tree2) = parse_tree::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
assert_eq!(tree1, Tree { root: tree2 }); assert_eq!(tree1, tree2);
} }
#[test] #[test]