diff --git a/Cargo.lock b/Cargo.lock index bb74c16..7137afc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2304,6 +2304,7 @@ name = "sgf" version = "0.1.0" dependencies = [ "chrono", + "cool_asserts", "nom", "serde", "thiserror", diff --git a/sgf/Cargo.toml b/sgf/Cargo.toml index ab75ce2..2dbd6bf 100644 --- a/sgf/Cargo.toml +++ b/sgf/Cargo.toml @@ -11,3 +11,6 @@ nom = { version = "7" } serde = { version = "1", features = [ "derive" ] } thiserror = { version = "1"} typeshare = { version = "1" } + +[dev-dependencies] +cool_asserts = { version = "2" } diff --git a/sgf/src/go.rs b/sgf/src/go.rs index 0fc3db0..d0fa213 100644 --- a/sgf/src/go.rs +++ b/sgf/src/go.rs @@ -74,6 +74,7 @@ use crate::{ Error, }; use serde::{Deserialize, Serialize}; +use std::ops::Deref; use typeshare::typeshare; #[derive(Clone, Debug)] @@ -84,11 +85,18 @@ pub struct Game { pub tree: Tree, } +impl Deref for Game { + type Target = Tree; + fn deref(&self) -> &Self::Target { + &self.tree + } +} + impl TryFrom for Game { type Error = Error; fn try_from(tree: Tree) -> Result { - let board_size = match tree.sequence[0].find_prop("SZ") { + let board_size = match tree.root.find_prop("SZ") { Some(prop) => Size::try_from(prop.values[0].as_str())?, None => Size { width: 19, @@ -96,40 +104,37 @@ impl TryFrom for Game { }, }; let mut info = GameInfo::default(); - info.app_name = tree.sequence[0] - .find_prop("AP") - .map(|prop| prop.values[0].clone()); + info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone()); - info.game_name = tree.sequence[0] - .find_prop("GN") - .map(|prop| prop.values[0].clone()); + info.game_name = tree.root.find_prop("GN").map(|prop| prop.values[0].clone()); - info.black_player = tree.sequence[0] - .find_prop("PB") - .map(|prop| prop.values.join(", ")); + info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", ")); - info.black_rank = tree.sequence[0] + info.black_rank = tree + .root .find_prop("BR") .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); - info.white_player = tree.sequence[0] - .find_prop("PW") - .map(|prop| prop.values.join(", ")); + info.white_player = tree.root.find_prop("PW").map(|prop| prop.values.join(", ")); - info.white_rank = tree.sequence[0] + info.white_rank = tree + .root .find_prop("WR") .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); - info.result = tree.sequence[0] + info.result = tree + .root .find_prop("RE") .and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok()); - info.time_limits = tree.sequence[0] + info.time_limits = tree + .root .find_prop("TM") .and_then(|prop| prop.values[0].parse::().ok()) .and_then(|seconds| Some(std::time::Duration::from_secs(seconds))); - info.date = tree.sequence[0] + info.date = tree + .root .find_prop("DT") .and_then(|prop| { let v = prop @@ -149,21 +154,13 @@ impl TryFrom for Game { }) .unwrap_or(vec![]); - info.event = tree.sequence[0] - .find_prop("EV") - .map(|prop| prop.values.join(", ")); + info.event = tree.root.find_prop("EV").map(|prop| prop.values.join(", ")); - info.round = tree.sequence[0] - .find_prop("RO") - .map(|prop| prop.values.join(", ")); + info.round = tree.root.find_prop("RO").map(|prop| prop.values.join(", ")); - info.source = tree.sequence[0] - .find_prop("SO") - .map(|prop| prop.values.join(", ")); + info.source = tree.root.find_prop("SO").map(|prop| prop.values.join(", ")); - info.game_keeper = tree.sequence[0] - .find_prop("US") - .map(|prop| prop.values.join(", ")); + info.game_keeper = tree.root.find_prop("US").map(|prop| prop.values.join(", ")); Ok(Game { board_size, @@ -307,7 +304,7 @@ mod tests { use super::*; use crate::{ date::Date, - tree::{parse_collection, Size}, + tree::{parse_collection, Property, Size}, }; use std::fs::File; use std::io::Read; @@ -390,4 +387,123 @@ mod tests { 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]); + } + }, + ); + } } diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index 4b4f8fa..bd2937c 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -65,7 +65,7 @@ pub fn parse_sgf(input: &str) -> Result, Error> { let (_, trees) = parse_collection::>(input)?; Ok(trees .into_iter() - .map(|t| match t.sequence[0].find_prop("GM") { + .map(|t| match t.root.find_prop("GM") { Some(prop) if prop.values == vec!["1".to_owned()] => { Game::Go(go::Game::try_from(t).expect("properly structured game tree")) } diff --git a/sgf/src/tree.rs b/sgf/src/tree.rs index 59d24c0..e1340a2 100644 --- a/sgf/src/tree.rs +++ b/sgf/src/tree.rs @@ -5,7 +5,6 @@ use nom::{ character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, combinator::{opt, value}, multi::{many0, many1, separated_list1}, - sequence::delimited, IResult, }; use std::num::ParseIntError; @@ -54,29 +53,19 @@ impl TryFrom<&str> for Size { #[derive(Clone, Debug, PartialEq)] pub struct Tree { - pub sequence: Vec, - pub sub_sequences: Vec, + pub root: Node, } impl ToString for Tree { fn to_string(&self) -> String { - let sequence = self - .sequence - .iter() - .map(|node| node.to_string()) - .collect::(); - let subsequences = self - .sub_sequences - .iter() - .map(|seq| seq.to_string()) - .collect::(); - format!("({}{})", sequence, subsequences) + format!("({})", self.root.to_string()) } } #[derive(Clone, Debug, PartialEq)] pub struct Node { pub properties: Vec, + pub next: Vec, } impl ToString for Node { @@ -86,7 +75,21 @@ impl ToString for Node { .iter() .map(|prop| prop.to_string()) .collect::(); - format!(";{}", props) + + 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) } } @@ -97,6 +100,10 @@ impl Node { .find(|prop| prop.ident == ident) .cloned() } + + pub fn next<'a>(&'a self) -> Option<&'a Node> { + self.next.get(0) + } } #[derive(Clone, Debug, PartialEq)] @@ -119,40 +126,40 @@ impl ToString for Property { pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Vec, E> { - separated_list1(multispace1, parse_tree)(input) + 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, Tree, E> { +fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { let (input, _) = multispace0(input)?; - delimited(tag("("), parse_sequence, tag(")"))(input) -} + let (input, _) = tag("(")(input)?; + let (input, node) = parse_node(input)?; + let (input, _) = multispace0(input)?; + let (input, _) = tag(")")(input)?; -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, - }, - )) + 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, _) = tag(";")(input)?; + let (input, _) = opt(tag(";"))(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>>( @@ -219,8 +226,6 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>( #[cfg(test)] mod test { - use std::{fs::File, io::Read}; - use super::*; const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) @@ -259,7 +264,8 @@ mod test { properties: vec![Property { ident: "B".to_owned(), values: vec!["ab".to_owned()] - }] + }], + next: vec![] } ); @@ -273,6 +279,25 @@ mod test { properties: vec![Property { ident: "B".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![], + }] }] } ); @@ -286,21 +311,17 @@ mod test { assert_eq!( sequence, - Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }] - }, - Node { - properties: vec![Property { - ident: "W".to_owned(), - values: vec!["dp".to_owned()] - }] - }, - Node { + Node { + properties: vec![Property { + ident: "B".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(), @@ -310,114 +331,158 @@ mod test { ident: "C".to_owned(), values: vec!["some comments".to_owned()] } - ] - } - ], - sub_sequences: vec![], - } + ], + next: vec![], + }] + }], + }, ); } #[test] - fn it_can_parse_a_sequence_with_subsequences() { + fn it_can_parse_a_branching_sequence() { let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; - let (_, sequence) = parse_tree::>(text).unwrap(); + let (_, tree) = parse_tree::>(text).unwrap(); - let main_sequence = vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()], - }], - }, - Node { + let expected = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()], + }], + next: vec![Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["b".to_owned()], }], - }, - ]; - let subsequence_1 = Tree { - sequence: vec![Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["c".to_owned()], - }], + next: vec![ + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["c".to_owned()], + }], + next: vec![], + }, + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["d".to_owned()], + }], + next: vec![Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["e".to_owned()], + }], + next: vec![], + }], + }, + ], }], - sub_sequences: vec![], - }; - let subsequence_2 = Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["d".to_owned()], - }], - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["e".to_owned()], - }], - }, - ], - sub_sequences: vec![], }; - assert_eq!( - sequence, - Tree { - sequence: main_sequence, - sub_sequences: vec![subsequence_1, subsequence_2], - } - ); + assert_eq!(tree, expected); } #[test] fn it_can_parse_example_1() { - let (_, ex_tree) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!(ex_tree.sequence.len(), 1); + let (_, tree) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!(ex_tree.sequence[0].properties.len(), 2); - assert_eq!( - ex_tree.sequence[0].properties[0], - Property { - ident: "FF".to_owned(), - values: vec!["4".to_owned()] - } - ); - assert_eq!(ex_tree.sub_sequences.len(), 2); + let j = Node { + properties: vec![Property { + ident: "C".to_owned(), + 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 { + ident: "FF".to_owned(), + values: vec!["4".to_owned()], + }, + Property { + ident: "C".to_owned(), + values: vec!["root".to_owned()], + }, + ], + next: vec![a, f], + }; - 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); + 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, tree2); + assert_eq!(tree1, Tree { root: tree2 }); } #[test]