pgn_filter 1.1.0

For searching/filtering pgn files of chess games.
Documentation
//! Holds a collection of games, with methods to load, search and save 
//! the game collection.
//!
//! Collection can be built by reading games from one or more PGN files.
//! The collection supports 'iter', to work with the games within it.

use std::fs::File;
use std::io::{BufReader, Write};
use super::board::filter::BoardFilter;
use super::moves;
use super::Game;

/// This struct is used to hold a collection of games, providing methods 
/// to load, search and save the game collection.
///
/// Users can create a new collection in two ways.
///
/// 1. by reading directly from a pgn file:
///
/// ```no_run
/// let games = pgn_filter::Games::from_file("favourite-games.pgn").unwrap();
/// ```
///
/// 2. by creating an instance of Games and adding games from PGN files to it:
///
/// ```no_run
/// let mut games = pgn_filter::Games::new();
/// games.add_games("favourite-games.pgn").unwrap();
/// ```
///
/// The main purpose of this library is to filter games based on some piece 
/// combination: this is done using the [`self::search()`] method.
///
/// Once a selection has been made, the new collection can be saved back out 
/// as a PGN file using the [`self::to_file()`] method.
///
/// The collection provides a [`self::iter()`] method, for processing each 
/// [`self::Game`] in turn.
///
pub struct Games {
    games: Vec<Game>,
    // hold a copy of Matches here to avoid recreating for each new game
    matcher: moves::Matches 
}

impl Games {
    /// Creates an empty instance of games.
    pub fn new () -> Games {
        let games: Vec<Game> = vec![];
        Games { 
            games,
            matcher: moves::Matches::new()
        }
    }

    /// Creates a collection of games from a PGN file.
    ///
    /// # Example
    /// 
    /// Read a file containing your favourite games:
    /// ```no_run
    /// let favs = pgn_filter::Games::from_file("favourite-games.pgn").unwrap();
    /// ```
    /// 
    /// # Error
    ///
    /// An error is returned if there is a problem in reading the PGN file.
    ///
    pub fn from_file (filename: &str) -> std::io::Result<Games> {
        let mut games = Games::new();
        games.add_games(filename)?;
        Ok(games)
    }

    /// Adds games to the current collection from a PGN file.
    ///
    /// # Example
    /// 
    /// Read and combine two files containing your favourite games:
    /// ```no_run
    /// let mut favs = pgn_filter::Games::from_file("favourite-games.pgn").unwrap();
    /// favs.add_games("more-favourites.pgn").unwrap();
    /// ```
    /// 
    /// # Error
    ///
    /// An error is returned if there is a problem in reading the PGN file.
    ///
    pub fn add_games (&mut self, filename: &str) -> std::io::Result<()> {
        let file = File::open(filename).expect("Error in opening file");
        let mut reader = BufReader::new(file);

        loop {
            let game = Game::from_pgn(&mut reader, &self.matcher)?;
            match game {
                Some(game) => self.games.push(game),
                None => break,
            };
        }

        Ok(())
    }

    /// Provides access to an iterator over the stored games.
    ///
    /// # Example
    ///
    /// Counting the games in a collection:
    /// ```no_run
    /// let favs = pgn_filter::Games::from_file("favourite-games.pgn").unwrap();
    /// println!("Read {} games", favs.iter().count());
    /// ```
    ///
    pub fn iter(&self) -> std::slice::Iter<'_, Game> {
        self.games.iter()
    }

    /// Returns a new collection of games for those from the current 
    /// collection which satisfy the given filter.
    ///
    /// # Example
    ///
    /// The following code constructs a filter for 5-4 rook endings, loads in 
    /// a (user-supplied!) pgn file, and extracts the rook endings as a new 
    /// Games collection.
    ///
    /// ```no_run
    /// let filter = pgn_filter::Board::must_have()
    ///                             .exactly(1, "R")
    ///                             .exactly(1, "r")
    ///                             .exactly(5, "P")
    ///                             .exactly(4, "p");
    /// let fischer = pgn_filter::Games::from_file("fischer.pgn").unwrap();
    /// let rook_endings = fischer.search(&filter);
    /// ```
    ///
    /// # Panics
    ///
    /// Panics if an invalid move occurs in any of the games.
    ///
    pub fn search(&self, filter: &BoardFilter) -> Games {
        let mut selected_games = vec![];

        for game in &self.games {
            if game.meets_filter(&filter) {
                selected_games.push(game.clone());
            }
        }

        Games { 
            games: selected_games, 
            matcher: moves::Matches::new() 
        }
    }

    /// Saves the game collection in PGN format to the given filename.
    ///
    /// (Note that the order of the header is not preserved, apart from 
    /// Event/Site/Date/Round/White/Black/Result, which always appear first.)
    ///
    /// # Error
    ///
    /// An error is returned if there was some problem in writing to the file.
    ///
    pub fn to_file(&self, filename: &str) -> std::io::Result<()> {
        let mut out_file = File::create(&filename)?;

        for game in &self.games {
            game.to_file(&mut out_file)?;
            out_file.write_all(b"\n")?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_file () {
        match Games::from_file("src/samples/games1.pgn") {
            Ok(db) => {
                assert_eq!(2, db.games.len());
                let game = &db.games[0];
                assert_eq!(Some(&"F/S Return Match".to_string()), game.header.get("Event"));
                assert_eq!(Some(&"Belgrade, Serbia JUG".to_string()), game.header.get("Site"));
                assert_eq!(Some(&"1992.11.04".to_string()), game.header.get("Date"));
                assert_eq!(Some(&"29".to_string()), game.header.get("Round"));
                assert_eq!(Some(&"Fischer, Robert J.".to_string()), game.header.get("White"));
                assert_eq!(Some(&"Spassky, Boris V.".to_string()), game.header.get("Black"));
                assert_eq!(85, game.total_half_moves());

                let game = &db.games[1];
                assert_eq!(Some(&"8th RUS-CHN Summit Men Classical".to_string()), game.header.get("Event"));
                assert_eq!(Some(&"2012.07.02".to_string()), game.header.get("Date"));
                assert_eq!(103, game.total_half_moves());
            },
            Err(_) => {
                assert!(false);
            },
        };
    }

    // Test file with acute accent (for forward tick) in header
    #[test]
    fn test_read_file_with_accent () {
        match Games::from_file("src/samples/games3.pgn") {
            Ok(db) => {
                assert_eq!(2, db.games.len());
            },
            Err(_) => {
                assert!(false);
            },
        };
    }
}