sgf-parse 4.2.8

A parser for the SGF file format for Go games
Documentation
use std::collections::HashSet;
use std::hash::Hash;
use std::str::FromStr;

use super::SgfPropError;

pub trait FromCompressedList: Sized {
    fn from_compressed_list(
        upper_left: &Self,
        lower_right: &Self,
    ) -> Result<HashSet<Self>, SgfPropError>;
}

pub fn parse_single_value<T: FromStr>(values: &[String]) -> Result<T, SgfPropError> {
    if values.len() != 1 {
        return Err(SgfPropError {});
    }
    values[0].parse().map_err(|_| SgfPropError {})
}

pub fn parse_tuple<T1: FromStr, T2: FromStr>(value: &str) -> Result<(T1, T2), SgfPropError> {
    let (s1, s2) = split_compose(value)?;
    Ok((
        s1.parse().map_err(|_| SgfPropError {})?,
        s2.parse().map_err(|_| SgfPropError {})?,
    ))
}

pub fn parse_elist<T: FromStr + FromCompressedList + Eq + Hash>(
    values: &[String],
) -> Result<HashSet<T>, SgfPropError> {
    let mut elements = HashSet::new();
    for value in values {
        if value.contains(':') {
            let (upper_left, lower_right): (T, T) = parse_tuple(value)?;
            elements.extend(T::from_compressed_list(&upper_left, &lower_right)?);
        } else {
            let item = value.parse().map_err(|_| SgfPropError {})?;
            elements.insert(item);
        }
    }
    Ok(elements)
}

pub fn parse_list<T: FromStr + FromCompressedList + Eq + std::hash::Hash>(
    values: &[String],
) -> Result<HashSet<T>, SgfPropError> {
    let points = parse_elist::<T>(values)?;
    if points.is_empty() {
        return Err(SgfPropError {});
    }

    Ok(points)
}

pub fn parse_list_composed<T: FromStr + Eq + Hash>(
    values: &[String],
) -> Result<HashSet<(T, T)>, SgfPropError> {
    let mut pairs = HashSet::new();
    for value in values.iter() {
        let pair = parse_tuple(value)?;
        if pair.0 == pair.1 || pairs.contains(&pair) {
            return Err(SgfPropError {});
        }
        pairs.insert(pair);
    }

    Ok(pairs)
}

pub fn split_compose(value: &str) -> Result<(&str, &str), SgfPropError> {
    let parts: Vec<&str> = value.split(':').collect();
    if parts.len() != 2 {
        return Err(SgfPropError {});
    }

    Ok((parts[0], parts[1]))
}

pub fn verify_empty(values: &[String]) -> Result<(), SgfPropError> {
    if !(values.is_empty() || (values.len() == 1 && values[0].is_empty())) {
        return Err(SgfPropError {});
    }
    Ok(())
}

#[cfg(test)]
mod test {
    use super::parse_list;
    use crate::go::Point;
    use std::collections::HashSet;

    #[test]
    pub fn parse_list_point() {
        let values = vec!["pq:ss".to_string(), "so".to_string(), "lr:ns".to_string()];
        let expected: HashSet<_> = vec![
            (15, 16),
            (16, 16),
            (17, 16),
            (18, 16),
            (15, 17),
            (16, 17),
            (17, 17),
            (18, 17),
            (15, 18),
            (16, 18),
            (17, 18),
            (18, 18),
            (18, 14),
            (11, 17),
            (12, 17),
            (13, 17),
            (11, 18),
            (12, 18),
            (13, 18),
        ]
        .into_iter()
        .map(|(x, y)| Point { x, y })
        .collect();

        let result: HashSet<_> = parse_list::<Point>(&values).unwrap();

        assert_eq!(result, expected);
    }
}