pgn_filter 1.1.0

For searching/filtering pgn files of chess games.
Documentation
//! Used to define and apply a board definition (filter) to a board position.
//!

use crate::piece::*;
use crate::Board;
use std::collections::HashMap;

enum Filter {
    Exactly(usize),
    AtLeast(usize),
    AtMost(usize),
}

/// A BoardFilter definition specifies the permitted number of pieces of each type which can appear
/// in a position.
///
/// For example, the following filter matches 5-4 rook-and-pawn endings, i.e. those with 5 white
/// pawns, 4 black pawns and a rook on either side:
///
/// ```no_run 
/// let filter = pgn_filter::Board::must_have() 
///     .exactly(1, "R") 
///     .exactly(1, "r")
///     .exactly(5, "P") 
///     .exactly(4, "p"); 
/// ```
///
/// Notice that the filter is constructed from a base filter obtained by calling
/// [`Board::must_have()`]. This base filter recognises positions with exactly one king of each
/// colour, and no pieces or pawns. You build up the filter by adding a relation for each of the
/// pieces you require. If you add a second filter for a given piece, the second filter will
/// replace the first one.
///
/// The available filters are:
///
/// * [`exactly`](self::exactly())(n, piece) 
/// * [`at_least`](self::at_least())(n, piece) 
/// * [`at_most`](self::at_most())(n, piece)
///
/// A piece is a one-letter string, upper-case for white, lower for black: "K", "k", "Q", "q", "R",
/// "r", "B", "b", "N", "n", "P", "p"
///
pub struct BoardFilter { filters: HashMap<Piece, Filter>, }

impl BoardFilter {
    /// Creates a new filter, seeded with filters for lone kings.
    ///
    pub(super) fn new() -> BoardFilter {
        let mut filters: HashMap<Piece, Filter> = HashMap::new();

        filters.insert(Piece::WK, Filter::Exactly(1));
        filters.insert(Piece::WQ, Filter::Exactly(0));
        filters.insert(Piece::WR, Filter::Exactly(0));
        filters.insert(Piece::WB, Filter::Exactly(0));
        filters.insert(Piece::WN, Filter::Exactly(0));
        filters.insert(Piece::WP, Filter::Exactly(0));
        filters.insert(Piece::BK, Filter::Exactly(1));
        filters.insert(Piece::BQ, Filter::Exactly(0));
        filters.insert(Piece::BR, Filter::Exactly(0));
        filters.insert(Piece::BB, Filter::Exactly(0));
        filters.insert(Piece::BN, Filter::Exactly(0));
        filters.insert(Piece::BP, Filter::Exactly(0));

        BoardFilter { filters }
    }

    /// Adds a filter which returns true if the number of occurrences of
    /// the given piece is at least n.
    ///
    /// # Panics
    ///
    /// The function panics if an unknown piece name is given.
    ///
    pub fn at_least(mut self, n: usize, piece: &str) -> BoardFilter {
        let piece = piece_from(&piece);
        if piece == Piece::NO {
            panic!("Unknown piece name passed to 'at_least'");
        }
        let filter = self.filters.entry(piece).or_insert(Filter::Exactly(0));
        *filter = Filter::AtLeast(n);

        self
    }

    /// Adds a filter which returns true if the number of occurrences of
    /// the given piece is at most n.
    ///
    /// # Panics
    ///
    /// The function panics if an unknown piece name is given.
    ///
    pub fn at_most(mut self, n: usize, piece: &str) -> BoardFilter {
        let piece = piece_from(&piece);
        if piece == Piece::NO {
            panic!("Unknown piece name passed to 'at_most'");
        }
        let filter = self.filters.entry(piece).or_insert(Filter::Exactly(0));
        *filter = Filter::AtMost(n);

        self
    }

    /// Adds a filter which returns true if the number of occurrences of
    /// the given piece is exactly n.
    ///
    /// # Panics
    ///
    /// The function panics if an unknown piece name is given.
    ///
    pub fn exactly(mut self, n: usize, piece: &str) -> BoardFilter {
        let piece = piece_from(&piece);
        if piece == Piece::NO {
            panic!("Unknown piece name passed to 'exactly'");
        }
        let filter = self.filters.entry(piece).or_insert(Filter::Exactly(0));
        *filter = Filter::Exactly(n);

        self
    }

    /// Applies current definition to the given board.
    /// Returns true if all the filters pass.
    ///
    pub(crate) fn is_match(&self, board: &Board) -> bool {
        let counts = board.piece_counts();

        for (piece, count) in counts.iter() {
            match self.filters[piece] {
                Filter::AtLeast(n) => {
                    if *count < n {
                        return false;
                    }
                }
                Filter::AtMost(n) => {
                    if *count > n {
                        return false;
                    }
                }
                Filter::Exactly(n) => {
                    if *count != n {
                        return false;
                    }
                }
            }
        }
        true
    }
}