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}