chess_notation_parser/lib.rs
1//! # Chess notation parser
2//! Crate transforms algebraic chess notation into software readable structs and
3//! vice versa. Parsed chess notation for each turn is stored within `Turn`
4//! struct.
5//!
6//! To parse a certain chess turn, such as `d2xe3`, store it in form of `&str`
7//! and pass it as an argument into `Turn::try_from()` function.
8//!
9//! `Turn` is an enum with two elements:
10//! - `Castling` - a struct which describes *castling* turn
11//! - `Move` - a struct which describes every other possible turn
12//!
13//! ## Example for `Castling` turn
14//! #### `0-0` will be translated to:
15//! ```
16//! # use chess_notation_parser::{Turn, Castling, CastlingType, Flag};
17//! # use chess_notation_parser::{turn_castling};
18//! # let turn =
19//! Turn::Castling(Castling {
20//! r#type: CastlingType::Short,
21//! flags: Flag::NONE,
22//! });
23//! # assert_eq!(turn, Turn::try_from("O-O").unwrap());
24//! ```
25//!
26//! ## Examples for `Move` turns
27//! #### `d6` will be translated to:
28//! ```
29//! # use chess_notation_parser::{Turn, Move, Square, Piece, Flag};
30//! # let turn =
31//! Turn::Move (Move {
32//! who: Piece::Pawn,
33//! dst: Square::D6,
34//! flags: Flag::NONE,
35//! src: None,
36//! promotion: None,
37//! });
38//! # assert_eq!(turn, Turn::try_from("d6").unwrap())
39//! ```
40//!
41//! #### `d7xe8=B+?` will be translated to:
42//! ```
43//! # use chess_notation_parser::{Turn, Move, Square, Piece, Flag};
44//! # let turn =
45//! Turn::Move (Move {
46//! who: Piece::Pawn,
47//! dst: Square::E8,
48//! flags: Flag::CHECK | Flag::CAPTURE,
49//! src: Some(vec![Square::D7]),
50//! promotion: Some(Piece::Bishop),
51//! });
52//! # assert_eq!(turn, Turn::try_from("d7xe8=B+?").unwrap())
53//! ```
54//!
55//! #### `Nab3#` will be translated to:
56//! ```
57//! # use chess_notation_parser::{Turn, Move, Square, Piece, Flag};
58//! # let turn =
59//! Turn::Move (Move {
60//! who: Piece::Knight,
61//! dst: Square::B3,
62//! flags: Flag::CHECKMATE,
63//! src: Some(Square::get_file('a').unwrap()), // Vector of 'Ax' squares
64//! promotion: None,
65//! });
66//! # assert_eq!(turn, Turn::try_from("Nab3#").unwrap())
67//! ```
68//!
69//! # Chess notation parser rules
70//! - **Square notation** should use lowercase alphabetic characters
71//! - Valid: `a1`, `a2` ... `h7`, `h8`.
72//!
73//! - **Castling notation** can be written with both `0` and `O`
74//! - Valid example: `0-0-0` or `O-O`
75//! - When `Castling` turn is printed out, it will be printed with `0`
76//! notation
77//!
78//! - Notation for **pieces**:
79//! - `K`: King
80//! - `Q`: Queen
81//! - `R`: Rook
82//! - `B`: Bishop
83//! - `N`: Knight
84//! - Pawns are indicated by the absence of the letter
85//!
86//! - **Capture** is annotated with a lowercase `x` character
87//! - Valid example: `Qxd3`
88//!
89//! - **Check** is annotated with a `+` character
90//! - Valid example: `Qd3+`
91//!
92//! - **Checkmate** is annotated with a `#` character
93//! - Valid example: `Qd3#`
94//!
95//! - **Pawn promotion** is annoted with `=` symbol followed by a piece to which
96//! pawn is promoted to
97//! - Pawn promotion is valid only for ranks `8` and `1`
98//! - Valid example: `g8=Q`
99//!
100//! - Comments `??`, `!!`, `?`, `!`, `!?`, `?!` are allowed only at the end of
101//! the turn
102//! - Valid example: `a1=B??`
103//! - Invalid example: `??a1=B`
104
105use std::fmt;
106
107mod square;
108pub use crate::square::Square;
109
110mod piece;
111pub use crate::piece::Piece;
112
113mod flag;
114pub use crate::flag::{Flag, FlagCheck};
115
116mod r#move;
117pub use crate::r#move::Move;
118
119mod castling;
120pub use crate::castling::{Castling, CastlingType};
121
122mod parser;
123
124/// # Struct representation of a string formatted chess turn
125///
126/// Turn can be either `Move` or `Castling` turn.
127///
128/// ## Example for `Move`
129/// ```
130/// use chess_notation_parser::{Turn, Move, Square, Piece, Flag};
131///
132/// let turn = Turn::Move(
133/// Move {
134/// who: Piece::Queen,
135/// dst: Square::D7,
136/// flags: Flag::NONE,
137/// src: None,
138/// promotion: None,
139/// }
140/// );
141/// assert_eq!(turn, Turn::try_from("Qd7").unwrap());
142///
143/// // Turn::Move can be created with macro
144/// use chess_notation_parser::turn_move;
145/// let turn = turn_move!(Piece::Bishop, Square::C4);
146/// assert_eq!(turn, Turn::try_from("Bc4").unwrap());
147///
148/// let turn = turn_move!(
149/// Piece::King,
150/// Square::D5,
151/// Flag::CAPTURE,
152/// Some(vec![Square::E3])
153/// );
154/// assert_eq!(turn, Turn::try_from("Ke3xd5").unwrap());
155/// ```
156/// ## Example for `Castling`
157/// ```
158/// use chess_notation_parser::{Turn, Castling, CastlingType, Flag};
159/// use chess_notation_parser::turn_castling;
160///
161/// let turn = turn_castling!(
162/// CastlingType::Long,
163/// Flag::NONE
164/// );
165///
166/// assert_eq!(turn, Turn::try_from("0-0-0").unwrap());
167/// ```
168#[derive(PartialEq, Debug, Clone)]
169pub enum Turn {
170 Castling(Castling),
171 Move(Move),
172}
173
174impl Turn {
175 /// Check if `Turn` results in check
176 pub fn is_check(&self) -> bool {
177 self.check_flag(Flag::CHECK)
178 }
179
180 /// Check if `Turn` results in checkmate
181 pub fn is_checkmate(&self) -> bool {
182 self.check_flag(Flag::CHECKMATE)
183 }
184
185 /// Check if `Turn` results in capture of opponent's piece
186 pub fn is_capture(&self) -> bool {
187 self.check_flag(Flag::CAPTURE)
188 }
189
190 fn check_flag(&self, flag: u8) -> bool {
191 match self {
192 Self::Move(turn_move) => turn_move.check_flag(flag),
193 Self::Castling(turn_move) => turn_move.check_flag(flag),
194 }
195 }
196
197 #[inline(always)]
198 fn strip_turn_comments(turn: &str) -> Result<&str, &'static str> {
199 let comment_start_idx = turn.find(&['?', '!']);
200
201 match comment_start_idx {
202 Some(i) => {
203 match &turn[i..] {
204 "?" | "??" | "?!" | "!?" | "!!" | "!" => (),
205 _ => return Err("Invalid comment syntax"),
206 }
207
208 Ok(&turn[..i])
209 }
210 None => Ok(turn),
211 }
212 }
213}
214
215impl fmt::Display for Turn {
216 #[rustfmt::skip]
217 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218 write!(f, "{}", match self {
219 Self::Castling(ref castling) => castling.to_string(),
220 Self::Move(ref turn_move) => turn_move.to_string(),
221 }
222 )
223 }
224}
225
226impl TryFrom<&str> for Turn {
227 type Error = &'static str;
228
229 fn try_from(turn: &str) -> Result<Self, Self::Error> {
230 let turn = turn.trim_end_matches('\0');
231
232 if turn.is_empty() {
233 return Err("Empty string received");
234 }
235
236 if turn.len() == 1 {
237 return Err("Insufficient length");
238 }
239
240 // '-' is present only in castling turns
241 let result = match turn.contains('-') {
242 true => parser::parse_castling(turn),
243 false => parser::parse_move(turn),
244 }?;
245
246 let result_str = result.to_string();
247 let result_str = result_str.as_str();
248 let turn_without_comments = Self::strip_turn_comments(turn)?;
249
250 if result_str != turn_without_comments
251 && result_str.replace("0", "O").as_str() != turn_without_comments
252 {
253 return Err("Turn verification failed: invalid format");
254 }
255
256 Ok(result)
257 }
258}
259
260/// Creates a `Turn::Move` from given number of arguments
261///
262/// # Example
263///
264/// ```rust
265/// use chess_notation_parser::{turn_move, Turn, Move, Square, Piece, Flag};
266///
267/// let turn = Turn::Move(
268/// Move {
269/// who: Piece::Queen,
270/// dst: Square::D7,
271/// flags: Flag::NONE,
272/// src: None,
273/// promotion: None,
274/// }
275/// );
276///
277/// assert_eq!(turn, turn_move!(Piece::Queen, Square::D7));
278/// assert_eq!(turn, turn_move!(Piece::Queen, Square::D7, Flag::NONE));
279/// assert_eq!(turn, turn_move!(Piece::Queen, Square::D7, Flag::NONE, None));
280/// assert_eq!(turn, turn_move!(
281/// Piece::Queen,
282/// Square::D7,
283/// Flag::NONE,
284/// None,
285/// None)
286/// );
287/// ```
288#[macro_export]
289macro_rules! turn_move {
290 ($who:expr, $dst:expr) => {
291 turn_move!($who, $dst, Flag::NONE, None, None)
292 };
293
294 ($who:expr, $dst:expr, $flags:expr) => {
295 turn_move!($who, $dst, $flags, None, None)
296 };
297
298 ($who:expr, $dst:expr, $flags:expr, $src:expr) => {
299 turn_move!($who, $dst, $flags, $src, None)
300 };
301
302 ($who:expr, $dst:expr, $flags:expr, $src:expr, $promotion:expr) => {
303 Turn::Move(Move {
304 who: $who,
305 dst: $dst,
306 flags: $flags,
307 src: $src,
308 promotion: $promotion,
309 })
310 };
311}
312
313/// Creates a `Turn::Castling` from given number of arguments
314///
315/// # Example
316///
317/// ```rust
318/// use chess_notation_parser::{Flag, Turn, Castling, CastlingType};
319/// use chess_notation_parser::turn_castling;
320///
321/// let turn = Turn::Castling(
322/// Castling {
323/// r#type: CastlingType::Short,
324/// flags: Flag::NONE,
325/// }
326/// );
327///
328/// assert_eq!(turn, turn_castling!(CastlingType::Short));
329/// assert_eq!(turn, turn_castling!(CastlingType::Short, Flag::NONE));
330/// ```
331#[macro_export]
332macro_rules! turn_castling {
333 ($castling_type:expr) => {
334 turn_castling!($castling_type, Flag::NONE)
335 };
336
337 ($castling_type:expr, $flags:expr) => {
338 Turn::Castling(Castling {
339 r#type: $castling_type,
340 flags: $flags,
341 })
342 };
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 /// This test was kept here since `parser` mod is private
350 #[test]
351 fn pawns_are_promoted() {
352 let c_check_promotion_for_piece = |square: Square, piece| {
353 let mut square_str = square.to_string();
354 square_str.push('=');
355 square_str.push(parser::get_piece_char(piece));
356
357 assert_eq!(
358 Turn::try_from(square_str.as_str()).unwrap(),
359 turn_move!(Piece::Pawn, square, Flag::NONE, None, Some(piece))
360 );
361 };
362
363 // Create all possible turns from below chars and append promotion
364 // piece to it
365 "18".chars()
366 .map(|rank| Square::get_rank(rank).unwrap())
367 .collect::<Vec<Vec<Square>>>()
368 .into_iter()
369 .flatten()
370 .collect::<Vec<Square>>()
371 .into_iter()
372 .for_each(|square| {
373 // This will cover all possibilities since squares are
374 // adjacent to each other
375 match square as u8 % 4 {
376 0 => c_check_promotion_for_piece(square, Piece::Queen),
377 1 => c_check_promotion_for_piece(square, Piece::Bishop),
378 2 => c_check_promotion_for_piece(square, Piece::Rook),
379 _ => c_check_promotion_for_piece(square, Piece::Knight),
380 }
381 });
382 }
383}