pgn_parser/
lib.rs

1use pest::error::ErrorVariant;
2use pest::iterators::Pair;
3use pest::{Parser, Span};
4use pest_derive::Parser;
5
6#[derive(Parser)]
7#[grammar = "pgn.pest"]
8pub struct PGNParser;
9
10pub struct ParsedGame {
11    pub metadata: Vec<(String, String)>,
12    pub moves: Vec<(Move, Option<Move>)>,
13    pub game_result: String,
14}
15
16impl ToString for ParsedGame {
17    fn to_string(&self) -> String {
18        let metadata_str: String = self
19            .metadata
20            .iter()
21            .map(|(key, value)| format!("{}: {}", key, value))
22            .collect::<Vec<String>>()
23            .join("\n");
24
25        let moves_str: String = self
26            .moves
27            .iter()
28            .enumerate()
29            .map(|(index, (first_move, second_move))| {
30                let first_move_str = first_move.to_string();
31                let second_move_str = second_move
32                    .as_ref()
33                    .map(|mv| mv.to_string())
34                    .unwrap_or_default();
35
36                format!(
37                    "Move #{}\nWhite: {} Black: {}",
38                    (index + 1),
39                    first_move_str,
40                    second_move_str
41                )
42            })
43            .collect::<Vec<String>>()
44            .join("\n");
45
46        format!(
47            "Game Metadata\n{}\n\nList of moves\n{}\n\nGame Result\n{}\n",
48            metadata_str, moves_str, self.game_result
49        )
50    }
51}
52
53pub struct Move {
54    piece_move: String,
55    comment: Option<String>,
56}
57
58impl ToString for Move {
59    fn to_string(&self) -> String {
60        let piece_move_str = &self.piece_move;
61        let comment_str = self.comment.as_ref().map_or("", |comment| comment);
62        format!("{} {}", piece_move_str, comment_str)
63    }
64}
65
66pub fn extract_pairs(pair: Pair<Rule>, rule: Rule) -> Option<Pair<Rule>> {
67    pair.into_inner().find(|pair| pair.as_rule() == rule)
68}
69
70pub fn parse_pgn(to_parse: &str) -> Result<ParsedGame, pest::error::Error<Rule>> {
71    let mut parsed_game = ParsedGame {
72        metadata: Vec::new(),
73        moves: Vec::new(),
74        game_result: String::new(),
75    };
76    let parse_result = PGNParser::parse(Rule::game, to_parse);
77
78    return match parse_result {
79        Ok(pairs) => {
80            let pgn_game = pairs
81                .into_iter()
82                .find(|pair| pair.as_rule() == Rule::game)
83                .ok_or_else(|| {
84                    pest::error::Error::<Rule>::new_from_span(
85                        ErrorVariant::CustomError {
86                            message: String::from("Game wasn't found"),
87                        },
88                        Span::new(to_parse, 0, to_parse.len()).unwrap(),
89                    )
90                })?;
91            let move_list = extract_pairs(pgn_game.clone(), Rule::move_list).ok_or_else(|| {
92                pest::error::Error::<Rule>::new_from_span(
93                    ErrorVariant::CustomError {
94                        message: String::from("Move list wasn't found"),
95                    },
96                    Span::new(to_parse, 0, to_parse.len()).unwrap(),
97                )
98            })?;
99            let metadata_block: Vec<(String, String)> = pgn_game
100                .clone()
101                .into_inner()
102                .filter_map(|pair| match pair.as_rule() {
103                    Rule::metadata_block => {
104                        let mut inner_pairs = pair.into_inner();
105                        let key = inner_pairs.next().unwrap().as_str().to_string();
106                        let value = inner_pairs.next().unwrap().as_str().to_string();
107                        Some((key, value))
108                    }
109                    _ => None,
110                })
111                .collect();
112            let game_result =
113                extract_pairs(pgn_game.clone(), Rule::game_result).ok_or_else(|| {
114                    pest::error::Error::<Rule>::new_from_span(
115                        ErrorVariant::CustomError {
116                            message: String::from("Game result wasn't found"),
117                        },
118                        Span::new(to_parse, 0, to_parse.len()).unwrap(),
119                    )
120                })?;
121
122            let moves_list: Vec<(Move, Option<Move>)> = move_list
123                .clone()
124                .into_inner()
125                .filter_map(|pair| match pair.as_rule() {
126                    Rule::move_pair => {
127                        let mut first_comment: Option<String> = None;
128                        let mut second_comment: Option<String> = None;
129                        let mut second_piece_move: Option<String> = None;
130                        let mut inner_pairs = pair.into_inner();
131                        let first_piece_move = inner_pairs.next().unwrap().as_str().to_string();
132                        let second_lex = inner_pairs.next();
133                        let third_lex = inner_pairs.next();
134                        let fourth_lex = inner_pairs.next();
135                        if let Some(pair) = second_lex {
136                            if pair.as_rule() == Rule::move_comment {
137                                first_comment = Some(pair.as_str().to_string());
138                            } else {
139                                second_piece_move = Some(pair.as_str().to_string());
140                            }
141                        }
142                        if let Some(pair) = third_lex {
143                            if pair.as_rule() == Rule::move_comment {
144                                second_comment = Some(pair.as_str().to_string());
145                            } else {
146                                second_piece_move = Some(pair.as_str().to_string());
147                            }
148                        }
149
150                        if let Some(pair) = fourth_lex {
151                            second_comment = Some(pair.as_str().to_string());
152                        }
153
154                        let first_move = Move {
155                            piece_move: first_piece_move,
156                            comment: first_comment,
157                        };
158                        if let Some(piece_move) = second_piece_move {
159                            let second_move = Move {
160                                piece_move,
161                                comment: second_comment,
162                            };
163                            Some((first_move, Some(second_move)))
164                        } else {
165                            Some((first_move, None))
166                        }
167                    }
168                    _ => None,
169                })
170                .collect();
171            parsed_game.game_result = game_result.as_str().to_string();
172            parsed_game.moves = moves_list;
173            parsed_game.metadata = metadata_block;
174            return Ok(parsed_game);
175        }
176        Err(e) => Err(e),
177    };
178}