1use std::fmt::{self, Write};
2
3use crate::types::{PieceKind, Square};
4
5use byte_parser::{ParseIterator, StrParser};
6
7#[derive(Debug, Clone)]
8pub enum Error {
9 NumberExpected,
10 MismatchNumber,
11 IncorrectMove,
12 ExpectedSquare,
13 UnknownPiece(u8),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PgnMove {
18 pub piece: PgnPieceMove,
19 pub duck: Option<Square>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum PgnPieceMove {
24 Piece {
25 piece: PieceKind,
26 from: Option<Square>,
27 to: Square,
28 capture: bool,
29 },
30 Castle {
31 long: bool,
32 },
33}
34
35pub fn parse_moves(s: &str) -> Result<Vec<PgnMove>, Error> {
36 let mut parser = StrParser::new(s.trim());
37 let mut moves = vec![];
38
39 'main_loop: loop {
40 parser.consume_while_byte_fn(u8::is_ascii_whitespace);
41
42 if parser.peek().is_none() {
43 break;
44 }
45
46 let _ = parse_number(&mut parser)?;
47
48 for _ in 0..2 {
49 parser.consume_while_byte_fn(u8::is_ascii_whitespace);
50
51 if let Some(mov) = parse_move(&mut parser)? {
52 moves.push(mov);
53 } else {
54 break 'main_loop;
55 }
56 }
57 }
58
59 Ok(moves)
60}
61
62fn parse_number<'a, I>(iter: &mut I) -> Result<usize, Error>
63where
64 I: ParseIterator<'a>,
65{
66 let mut iter = iter.record();
67
68 iter.while_byte_fn(u8::is_ascii_digit)
70 .consume_at_least(1)
71 .map_err(|_| Error::NumberExpected)?;
72
73 let digits = iter.to_str();
74
75 iter.expect_byte(b'.').map_err(|_| Error::NumberExpected)?;
76
77 digits.parse().map_err(|_| Error::NumberExpected)
78}
79
80fn parse_move<'a, I>(iter: &mut I) -> Result<Option<PgnMove>, Error>
83where
84 I: ParseIterator<'a>,
85{
86 let move_str = iter
87 .record()
88 .consume_while_byte_fn(|b| b.is_ascii_alphanumeric() || *b == b'-')
89 .to_str();
90
91 if matches!(move_str, "0-0" | "1-0" | "0-1") {
92 return Ok(None);
93 }
94
95 let bytes = move_str.as_bytes();
96 if bytes.len() < 4 {
97 return Err(Error::IncorrectMove);
98 }
99 let (mut bytes, duck_bytes) = bytes.split_at(bytes.len() - 2);
100 let duck = parse_square(duck_bytes[0], duck_bytes[1])?;
101
102 if bytes == b"O-O" {
103 return Ok(Some(PgnMove {
104 piece: PgnPieceMove::Castle { long: false },
105 duck: Some(duck),
106 }));
107 } else if bytes == b"O-O-O" {
108 return Ok(Some(PgnMove {
109 piece: PgnPieceMove::Castle { long: true },
110 duck: Some(duck),
111 }));
112 }
113
114 let mut piece = PieceKind::Pawn;
115 if bytes[0].is_ascii_uppercase() {
116 piece = match bytes[0] {
117 b'R' => PieceKind::Rook,
118 b'N' => PieceKind::Knight,
119 b'B' => PieceKind::Bishop,
120 b'K' => PieceKind::King,
121 b'Q' => PieceKind::Queen,
122 l => return Err(Error::UnknownPiece(l)),
123 };
124
125 bytes = &bytes[1..];
126 }
127
128 let (mut bytes, square_bytes) = bytes.split_at(bytes.len() - 2);
129 let to = parse_square(square_bytes[0], square_bytes[1])?;
130
131 let capture = bytes.ends_with(&[b'x']);
132 if capture {
133 bytes = &bytes[..bytes.len() - 1];
134 }
135
136 let mut from = None;
137 if bytes.len() == 1 {
138 let byte = bytes[0];
139 if byte.is_ascii_digit() {
140 from = Some(parse_square(square_bytes[0], byte)?);
141 } else if byte.is_ascii_alphabetic() && byte.is_ascii_lowercase() {
142 from = Some(parse_square(byte, square_bytes[1])?);
143 } else {
144 return Err(Error::IncorrectMove);
145 }
146 } else if !bytes.is_empty() {
147 return Err(Error::IncorrectMove);
148 }
149
150 Ok(Some(PgnMove {
151 piece: PgnPieceMove::Piece {
152 piece,
153 from,
154 to,
155 capture,
156 },
157 duck: Some(duck),
158 }))
159}
160
161fn parse_square(letter: u8, number: u8) -> Result<Square, Error> {
162 assert_ne!(number, b'0');
163
164 let number = number - b'1';
165
166 let letter_number = letter - b'a';
167 if letter_number >= 8 || number >= 8 {
168 return Err(Error::ExpectedSquare);
169 }
170
171 let number = 7 - number;
173
174 Ok(Square::from_xy(letter_number, number))
175}
176
177pub struct MovesFormatter<'a>(pub &'a [PgnMove]);
178
179impl fmt::Display for MovesFormatter<'_> {
180 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181 for (i, moves) in self.0.chunks(2).enumerate() {
182 if i > 0 {
183 write!(f, " ")?;
184 }
185
186 write!(f, "{}.", i + 1)?;
187
188 for mov in moves {
189 write!(f, " {}", mov)?;
190 }
191 }
192
193 Ok(())
194 }
195}
196
197impl fmt::Display for PgnPieceMove {
198 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
199 match self {
200 Self::Piece {
201 piece,
202 from,
203 to,
204 capture,
205 } => {
206 match piece {
208 PieceKind::Rook => f.write_char('R')?,
209 PieceKind::Knight => f.write_char('N')?,
210 PieceKind::Bishop => f.write_char('B')?,
211 PieceKind::Queen => f.write_char('Q')?,
212 PieceKind::King => f.write_char('K')?,
213 _ => {}
214 }
215
216 if let Some(from) = from {
217 if from.x() != to.x() {
218 f.write_char(from.x_letter() as char)?;
219 }
220
221 if from.y() != to.y() {
222 write!(f, "{}", from.y() + 1)?;
223 }
224 }
225
226 if *capture {
227 f.write_char('x')?;
228 }
229
230 write!(f, "{}", to)
231 }
232 Self::Castle { long } => {
233 if *long {
234 f.write_str("O-O-O")
235 } else {
236 f.write_str("O-O")
237 }
238 }
239 }
240 }
241}
242
243impl fmt::Display for PgnMove {
244 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245 self.piece.fmt(f)?;
246 if let Some(duck) = self.duck {
247 duck.fmt(f)?;
248 }
249
250 Ok(())
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
260 fn test_parsing() {
261 let moves = parse_moves(
262 "
263 1. e2e4 d7d6
264 2. Nb1c3 g7g6
265 3. Ng1f3 Bf8g7
266 4. Bf1e2 c7c6
267 ",
268 )
269 .unwrap();
270
271 let correct_moves = vec![
272 PgnMove {
273 piece: PgnPieceMove::Piece {
274 piece: PieceKind::Pawn,
275 from: None,
276 to: Square::E2,
277 capture: false,
278 },
279 duck: Some(Square::E4),
280 },
281 PgnMove {
282 piece: PgnPieceMove::Piece {
283 piece: PieceKind::Pawn,
284 from: None,
285 to: Square::D7,
286 capture: false,
287 },
288 duck: Some(Square::D6),
289 },
290 PgnMove {
291 piece: PgnPieceMove::Piece {
292 piece: PieceKind::Knight,
293 from: None,
294 to: Square::B1,
295 capture: false,
296 },
297 duck: Some(Square::C3),
298 },
299 ];
300
301 assert_eq!(&moves[..correct_moves.len()], &correct_moves);
302 }
303}