chess/pgn/
notation.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::errors::PGNParseError;
5use crate::{board, movegen::*};
6use crate::{hash_to_string, log_and_return_error};
7
8#[derive(Debug, PartialEq, Eq, Clone)]
9pub struct Notation {
10    piece: Option<char>,
11    dis_file: Option<char>, // for disambiguating moves if required
12    dis_rank: Option<char>, // for disambiguating moves if required
13    capture: bool,
14    to_file: char,
15    to_rank: char,
16    promotion: Option<char>,
17    check: bool,
18    checkmate: bool,
19    castle_str: Option<String>,
20}
21
22impl fmt::Display for Notation {
23    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24        let mut notation = String::new();
25
26        // return castling string if it exists
27        if let Some(cs) = &self.castle_str {
28            let mut castle_str = cs.clone();
29            if self.checkmate {
30                castle_str.push('#');
31            } else if self.check {
32                castle_str.push('+');
33            }
34            return write!(f, "{}", castle_str);
35        }
36
37        if let Some(piece) = self.piece {
38            notation.push(piece);
39        }
40        if let Some(dis_file) = self.dis_file {
41            notation.push(dis_file);
42        }
43        if let Some(dis_rank) = self.dis_rank {
44            notation.push(dis_rank);
45        }
46        if self.capture {
47            notation.push('x');
48        }
49        notation.push(self.to_file);
50        notation.push(self.to_rank);
51        if let Some(promotion) = self.promotion {
52            notation.push('=');
53            notation.push(promotion);
54        }
55        if self.checkmate {
56            notation.push('#');
57        } else if self.check {
58            notation.push('+');
59        }
60        write!(f, "{}", notation)
61    }
62}
63
64impl FromStr for Notation {
65    type Err = PGNParseError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        // check that str is valid ascii
69        Self::validate_ascii(s)?;
70
71        // min length is 2 (e.g. 'e4'), max length is 8 if all disambiguating notation is used and position is a check (e.g. 'Qd5xRd1+')
72        Self::validate_length(s)?;
73
74        // create new uninitialised Notation struct
75        let mut notation = Self::new();
76
77        // parse castling strings and return as it doesn't require further parsing
78        if notation.parse_castling_string(s) {
79            return Ok(notation);
80        }
81
82        // parse the notation string
83        notation.parse_notation_string(s)?;
84
85        Ok(notation)
86    }
87}
88
89// CONSTRUCTORS AND RELATED FUNCTIONS
90impl Notation {
91    // (private) new uninitialised Notation struct
92    fn new() -> Notation {
93        Notation {
94            piece: None,
95            dis_file: None,
96            dis_rank: None,
97            capture: false,
98            to_file: ' ',
99            to_rank: ' ',
100            promotion: None,
101            check: false,
102            checkmate: false,
103            castle_str: None,
104        }
105    }
106
107    // from move with boardstate context, disambiguaating notation will only be used if required
108    pub fn from_mv_with_context(
109        bs_context: &board::BoardState,
110        mv: &Move,
111    ) -> Result<Notation, PGNParseError> {
112        let legal_moves = extract_legal_moves(bs_context)?;
113
114        // create new uninitialised Notation struct
115        let mut notation = Self::new();
116
117        // check if move is legal and if it results in check or checkmate by generating a new boardstate
118        // set check and checkmate flags based off the new boardstate's gamestate
119        if legal_moves.contains(mv) {
120            let test_bs = bs_context.next_state(mv).unwrap(); // unwrap is safe as move is legal
121            match test_bs.get_gamestate() {
122                board::GameState::Check => notation.check = true, // SET CHECK FLAG
123                board::GameState::Checkmate => notation.checkmate = true, // SET CHECKMATE FLAG
124                _ => {}
125            }
126        } else {
127            let err = PGNParseError::NotationParseError(format!("Move not legal: {:?}", mv));
128            log_and_return_error!(err);
129        }
130
131        // set castling string if it is a castling move and return
132        if let MoveType::Castle(cm) = mv.move_type {
133            notation.castle_str = Some(match cm.get_castle_side() {
134                // SET CASTLE STRING
135                CastleSide::Short => "O-O".to_string(),
136                CastleSide::Long => "O-O-O".to_string(),
137            });
138            return Ok(notation); // RETURN ON CASTLE MOVE
139        }
140
141        // SET PIECE CHAR
142        notation.piece = ptype_to_piece_char(&mv.piece.ptype);
143
144        // SET TO FILE AND TO RANK
145        notation.to_file = index_to_file_notation(mv.to);
146        notation.to_rank = index_to_rank_notation_unchecked(mv.to);
147
148        // SET CAPTURE FLAG (Normal capture, en passant capture, or promotion capture)
149        notation.capture = mv.move_type.is_capture();
150
151        // SET PROMOTION CHAR
152        notation.promotion = mv_type_to_promotion_char(&mv.move_type);
153
154        // DISAMBIGUATING MOVES
155        // pawn moves that are captures or en passants only need dis_file, otherwise only to_file and to_rank are needed
156        if matches!(mv.piece.ptype, PieceType::Pawn) && notation.capture {
157            // notation.capture is set above in function
158            notation.dis_file = Some(index_to_file_notation(mv.from));
159        } else {
160            // check if there are any other pieces besides pawns that can move to the same square as the mv.piece
161            let same_piece_moves: Vec<&Move> = legal_moves
162                .iter()
163                .filter(|m| m.piece == mv.piece && m.to == mv.to && m.from != mv.from)
164                .collect();
165            // if there are other pieces that can move to same square
166            if !same_piece_moves.is_empty() {
167                // store the current mv.from square file and rank
168                let mv_from_file = index_to_file_notation(mv.from);
169                let mv_from_rank = index_to_rank_notation_unchecked(mv.from);
170                // keep track of whether any of the other moves have the same file or rank as the current mv.from square
171                let mut same_file = false;
172                let mut same_rank = false;
173                // check if any of the other moves have the same file or rank as the current mv.from square
174                for other_mv in same_piece_moves {
175                    let other_mv_from_file = index_to_file_notation(other_mv.from);
176                    let other_mv_from_rank = index_to_rank_notation_unchecked(other_mv.from);
177                    if other_mv_from_file == mv_from_file {
178                        same_file = true;
179                    }
180                    if other_mv_from_rank == mv_from_rank {
181                        same_rank = true;
182                    }
183                }
184                // disambiguate the move by setting the file, or setting the rank, or setting both if needed in that order
185                if !same_file {
186                    notation.dis_file = Some(mv_from_file);
187                } else if !same_rank {
188                    notation.dis_rank = Some(mv_from_rank);
189                } else {
190                    notation.dis_file = Some(mv_from_file);
191                    notation.dis_rank = Some(mv_from_rank);
192                }
193            }
194        }
195
196        Ok(notation)
197    }
198
199    fn parse_castling_string(&mut self, notation_str: &str) -> bool {
200        let possible_castle_str = notation_str.trim_end_matches(['+', '#']);
201        if possible_castle_str == "O-O" || possible_castle_str == "O-O-O" {
202            self.castle_str = Some(possible_castle_str.to_string());
203            self.check = notation_str.ends_with('+');
204            self.checkmate = notation_str.ends_with('#');
205            return true;
206        }
207        false
208    }
209
210    fn parse_notation_string(&mut self, notation_str: &str) -> Result<(), PGNParseError> {
211        let mut chars = notation_str.char_indices();
212        let mut rank_file_chars: Vec<char> = Vec::new();
213        let mut piece_char: Option<char> = None;
214        let mut capture = false;
215        let mut promotion: Option<char> = None;
216        let mut check = false;
217        let mut checkmate = false;
218
219        while let Some((i, c)) = chars.next() {
220            match c {
221                c if c.is_ascii_uppercase() => {
222                    Self::handle_piece_char(c, &mut piece_char, notation_str, i)?;
223                }
224                'x' => {
225                    Self::handle_capture_char(&mut capture, notation_str, i)?;
226                }
227                c if c.is_ascii_lowercase() || c.is_ascii_digit() => {
228                    rank_file_chars.push(c);
229                }
230                '=' => {
231                    promotion = Self::handle_promotion_char(&mut chars, notation_str, i)?;
232                }
233                '+' => {
234                    Self::handle_check_char(&mut check, notation_str, i)?;
235                }
236                '#' => {
237                    Self::handle_checkmate_char(&mut checkmate, notation_str, i)?;
238                }
239                _ => {
240                    let err = PGNParseError::NotationParseError(format!(
241                        "Invalid character in notation (char: '{}' at index: {})",
242                        c, i
243                    ));
244                    log_and_return_error!(err)
245                }
246            }
247        }
248
249        // set piece char if it is valid
250        self.set_piece_char(piece_char)?;
251
252        // set rank and file chars, checking if there are any disambiguating chars
253        self.set_rank_file_chars(&rank_file_chars)?;
254
255        // set promotion char if it is valid
256        self.set_promotion_char(promotion)?;
257
258        // set boolean flags
259        self.capture = capture;
260        self.check = check;
261        self.checkmate = checkmate;
262
263        Ok(())
264    }
265
266    fn set_piece_char(&mut self, piece_char: Option<char>) -> Result<(), PGNParseError> {
267        if let Some(piece) = piece_char {
268            if is_valid_piece(piece) {
269                self.piece = Some(piece);
270            } else {
271                let err =
272                    PGNParseError::NotationParseError(format!("Invalid piece char ({})", piece));
273                log_and_return_error!(err)
274            }
275        }
276        Ok(())
277    }
278
279    fn set_promotion_char(&mut self, promotion: Option<char>) -> Result<(), PGNParseError> {
280        if let Some(promotion) = promotion {
281            if is_valid_promotion(promotion) {
282                self.promotion = Some(promotion);
283            } else {
284                let err = PGNParseError::NotationParseError(format!(
285                    "Invalid promotion piece char ({})",
286                    promotion
287                ));
288                log_and_return_error!(err)
289            }
290        }
291        Ok(())
292    }
293
294    fn set_rank_file_chars(&mut self, rank_file_chars: &[char]) -> Result<(), PGNParseError> {
295        let to_file: char;
296        let to_rank: char;
297        let mut dis_file = None;
298        let mut dis_rank = None;
299        match rank_file_chars.len() {
300            2 => {
301                to_file = rank_file_chars[0];
302                to_rank = rank_file_chars[1];
303            }
304            3 => {
305                if rank_file_chars[0].is_ascii_lowercase() {
306                    dis_file = Some(rank_file_chars[0]); // disambiguating file
307                } else {
308                    dis_rank = Some(rank_file_chars[0]); // disambiguating rank
309                }
310                to_file = rank_file_chars[1];
311                to_rank = rank_file_chars[2];
312            }
313            4 => {
314                dis_file = Some(rank_file_chars[0]);
315                dis_rank = Some(rank_file_chars[1]);
316                to_file = rank_file_chars[2];
317                to_rank = rank_file_chars[3];
318            }
319            _ => {
320                let err = PGNParseError::NotationParseError(format!(
321                    "Invalid move notation char(s) ({:?})",
322                    rank_file_chars
323                ));
324                log_and_return_error!(err)
325            }
326        }
327        if !is_valid_file(to_file)
328            || !is_valid_rank(to_rank)
329            || dis_file.map_or(false, |c| !is_valid_file(c))
330            || dis_rank.map_or(false, |c| !is_valid_rank(c))
331        {
332            let err = PGNParseError::NotationParseError(format!(
333                "Invalid rank or file char(s) in vec: ({:?})",
334                rank_file_chars
335            ));
336            log_and_return_error!(err)
337        } else {
338            self.to_file = to_file;
339            self.to_rank = to_rank;
340            self.dis_file = dis_file;
341            self.dis_rank = dis_rank;
342        }
343        Ok(())
344    }
345
346    fn validate_ascii(notation_str: &str) -> Result<(), PGNParseError> {
347        if !notation_str.is_ascii() {
348            let err = PGNParseError::NotationParseError(format!(
349                "Invalid notation string: ({}) is not valid ascii",
350                notation_str
351            ));
352            log_and_return_error!(err)
353        }
354        Ok(())
355    }
356
357    fn validate_length(notation_str: &str) -> Result<(), PGNParseError> {
358        let str_len = notation_str.len();
359        if !(2..=8).contains(&str_len) {
360            let err =
361                PGNParseError::NotationParseError(format!("Invalid notation length ({})", str_len));
362            log_and_return_error!(err)
363        }
364        Ok(())
365    }
366
367    fn handle_piece_char(
368        c: char,
369        piece_char: &mut Option<char>,
370        notation_str: &str,
371        i: usize,
372    ) -> Result<(), PGNParseError> {
373        if piece_char.is_none() {
374            *piece_char = Some(c);
375        } else {
376            let err = PGNParseError::NotationParseError(format!(
377                "Invalid notation, multiple uppercase piece chars (char: '{}' at index: {})",
378                notation_str, i
379            ));
380            log_and_return_error!(err)
381        }
382        Ok(())
383    }
384
385    fn handle_capture_char(
386        capture: &mut bool,
387        notation_str: &str,
388        i: usize,
389    ) -> Result<(), PGNParseError> {
390        if !*capture {
391            // must be at least 2 more chars after 'x' for a valid capture
392            if (notation_str.len() - i) < 3 {
393                let err = PGNParseError::NotationParseError(format!(
394                    "Invalid notation, no rank or file after capture char (char: '{}' at index: {})",
395                    notation_str, i
396                ));
397                log_and_return_error!(err)
398            } else {
399                *capture = true;
400            }
401        } else {
402            let err = PGNParseError::NotationParseError(format!(
403                "Invalid notation, multiple capture chars (char: '{}' at index: {})",
404                notation_str, i
405            ));
406            log_and_return_error!(err)
407        }
408        Ok(())
409    }
410
411    fn handle_promotion_char(
412        chars: &mut std::str::CharIndices,
413        notation_str: &str,
414        i: usize,
415    ) -> Result<Option<char>, PGNParseError> {
416        let n = chars.next();
417        match n {
418            Some((_, c)) => match c {
419                'Q' | 'R' | 'B' | 'N' => Ok(Some(c)),
420                _ => {
421                    let err = PGNParseError::NotationParseError(format!(
422                        "Invalid promotion piece (char: '{}' at index: {})",
423                        c, i
424                    ));
425                    log_and_return_error!(err)
426                }
427            },
428            None => {
429                let err = PGNParseError::NotationParseError(format!(
430                    "Invalid notation, no promotion piece after '=' (char: '{}' at index: {})",
431                    notation_str, i
432                ));
433                log_and_return_error!(err)
434            }
435        }
436    }
437
438    fn handle_check_char(
439        check: &mut bool,
440        notation_str: &str,
441        i: usize,
442    ) -> Result<(), PGNParseError> {
443        if !*check {
444            *check = true;
445        } else {
446            let err = PGNParseError::NotationParseError(format!(
447                "Invalid notation, multiple check chars (char: '{}' at index: {})",
448                notation_str, i
449            ));
450            log_and_return_error!(err)
451        }
452        Ok(())
453    }
454
455    fn handle_checkmate_char(
456        checkmate: &mut bool,
457        notation_str: &str,
458        i: usize,
459    ) -> Result<(), PGNParseError> {
460        if !*checkmate {
461            *checkmate = true;
462        } else {
463            let err = PGNParseError::NotationParseError(format!(
464                "Invalid notation, multiple checkmate chars (char: '{}' at index: {})",
465                notation_str, i
466            ));
467            log_and_return_error!(err)
468        }
469        Ok(())
470    }
471}
472
473impl Notation {
474    // tries to find a move, and disambiguates as best as possible, for use in PGN import format so if it is missing some disambiguating information but the move can still be identified, it is fine
475    pub fn to_move_with_context(
476        &self,
477        bs_context: &board::BoardState,
478    ) -> Result<Move, PGNParseError> {
479        let legal_moves = extract_legal_moves(bs_context)?;
480        let possible_moves = self.filter_possible_moves(legal_moves);
481        match possible_moves.len() {
482            1 => Ok(*possible_moves[0]),
483            len if len > 1 => {
484                let mut dis_file_possible_idxs = None;
485                let mut dis_rank_possible_idxs = None;
486
487                if let Some(dis_file_char) = self.dis_file {
488                    dis_file_possible_idxs =
489                        Some(file_notation_to_indexes_unchecked(dis_file_char));
490                }
491                if let Some(dis_rank_char) = self.dis_rank {
492                    dis_rank_possible_idxs =
493                        Some(rank_notation_to_indexes_unchecked(dis_rank_char));
494                }
495
496                let mut possible_dis_moves = Vec::new();
497                for mv in &possible_moves {
498                    if let (Some(dis_file_idxs), Some(dis_rank_idxs)) =
499                        (dis_file_possible_idxs, dis_rank_possible_idxs)
500                    {
501                        if dis_file_idxs.contains(&mv.from) && dis_rank_idxs.contains(&mv.from) {
502                            possible_dis_moves.push(*mv);
503                        }
504                    } else if let Some(dis_file_idxs) = dis_file_possible_idxs {
505                        if dis_file_idxs.contains(&mv.from) {
506                            possible_dis_moves.push(*mv);
507                        }
508                    } else if let Some(dis_rank_idxs) = dis_rank_possible_idxs {
509                        if dis_rank_idxs.contains(&mv.from) {
510                            possible_dis_moves.push(*mv);
511                        }
512                    }
513                }
514
515                if possible_dis_moves.len() == 1 {
516                    Ok(*possible_dis_moves[0])
517                } else {
518                    let err = PGNParseError::MoveNotFound(format!(
519                "No legal move found for notation ({}) in BoardState (hash: {}) => Could not use notation to disambiguate between multiple possible moves: {:?}",
520                self,
521                hash_to_string(bs_context.board_hash),
522                possible_moves
523                ));
524                    log_and_return_error!(err)
525                }
526            }
527            _ => {
528                let err = PGNParseError::MoveNotFound(format!(
529                    "No legal move found for notation ({}) in BoardState (hash: {})",
530                    self,
531                    hash_to_string(bs_context.board_hash)
532                ));
533                log_and_return_error!(err)
534            }
535        }
536    }
537
538    fn get_piece_type(&self) -> Option<PieceType> {
539        match self.piece {
540            Some('N') => Some(PieceType::Knight),
541            Some('B') => Some(PieceType::Bishop),
542            Some('R') => Some(PieceType::Rook),
543            Some('Q') => Some(PieceType::Queen),
544            Some('K') => Some(PieceType::King),
545            Some(_) => {
546                unreachable!("Invalid piece char in get_piece_type function")
547            }
548            None => None,
549        }
550    }
551
552    fn get_promotion_piece_type(&self) -> Option<PieceType> {
553        match self.promotion {
554            Some('Q') => Some(PieceType::Queen),
555            Some('R') => Some(PieceType::Rook),
556            Some('B') => Some(PieceType::Bishop),
557            Some('N') => Some(PieceType::Knight),
558            Some(_) => {
559                unreachable!("Invalid promotion char in get_promotion_piece_type function")
560            }
561            None => None,
562        }
563    }
564
565    fn get_castle_side(&self) -> Option<CastleSide> {
566        self.castle_str
567            .as_ref()
568            .map(|castle_str| match castle_str.as_str() {
569                "O-O" => CastleSide::Short,
570                "O-O-O" => CastleSide::Long,
571                _ => {
572                    unreachable!("Invalid castle string in get_castle_side function");
573                }
574            })
575    }
576
577    fn filter_possible_moves<'a>(&self, moves: &'a [Move]) -> Vec<&'a Move> {
578        moves
579            .iter()
580            .filter(|mv| {
581                if let Some(castle_side) = self.get_castle_side() {
582                    if let MoveType::Castle(cm) = mv.move_type {
583                        return cm.get_castle_side() == castle_side;
584                    }
585                }
586
587                if let Some(piece) = self.get_piece_type() {
588                    if mv.piece.ptype != piece {
589                        return false;
590                    }
591                } else {
592                    // PAWN HANDLING - no piece char can only be a castle move which is already handled, or a pawn move
593                    if mv.piece.ptype != PieceType::Pawn {
594                        return false;
595                    }
596                }
597
598                if self.to_file != index_to_file_notation(mv.to)
599                    || self.to_rank != index_to_rank_notation_unchecked(mv.to)
600                {
601                    return false;
602                }
603                if self.capture && !mv.move_type.is_capture() {
604                    return false;
605                }
606                if let Some(promotion) = self.get_promotion_piece_type() {
607                    if let MoveType::Promotion(promotion_ptype, _) = mv.move_type {
608                        if promotion_ptype != promotion {
609                            return false;
610                        }
611                    } else {
612                        return false;
613                    }
614                }
615                // if move passes all checks, return true
616                true
617            })
618            .collect::<Vec<&Move>>()
619    }
620}
621
622// get legal moves from BoardState, on error return BoardStateError wrapped in PGNParseError
623fn extract_legal_moves(bs: &board::BoardState) -> Result<&[Move], PGNParseError> {
624    match bs.get_legal_moves() {
625        Ok(moves) => Ok(moves),
626        Err(e) => {
627            let err = PGNParseError::NotationParseError(format!(
628                "Error getting legal moves in BoardState: {}",
629                e
630            ));
631            log_and_return_error!(err)
632        }
633    }
634}
635
636#[inline]
637fn ptype_to_piece_char(ptype: &PieceType) -> Option<char> {
638    match ptype {
639        PieceType::Pawn => None,
640        PieceType::Knight => Some('N'),
641        PieceType::Bishop => Some('B'),
642        PieceType::Rook => Some('R'),
643        PieceType::Queen => Some('Q'),
644        PieceType::King => Some('K'),
645    }
646}
647
648#[inline]
649fn mv_type_to_promotion_char(mv_type: &MoveType) -> Option<char> {
650    if let MoveType::Promotion(promotion, _) = mv_type {
651        match promotion {
652            PieceType::Queen => Some('Q'),
653            PieceType::Rook => Some('R'),
654            PieceType::Bishop => Some('B'),
655            PieceType::Knight => Some('N'),
656            _ => unreachable!("Invalid MoveType. Not possible from crate::movegen"),
657        }
658    } else {
659        None
660    }
661}
662
663#[inline]
664fn is_valid_file(file: char) -> bool {
665    file.is_ascii_lowercase() && ('a'..='h').contains(&file)
666}
667
668#[inline]
669fn is_valid_rank(rank: char) -> bool {
670    rank.is_ascii_digit() && ('1'..='8').contains(&rank)
671}
672
673#[inline]
674fn is_valid_piece(piece: char) -> bool {
675    let valid_pieces = ['P', 'N', 'B', 'R', 'Q', 'K'];
676    piece.is_ascii_uppercase() && valid_pieces.contains(&piece)
677}
678
679#[inline]
680fn is_valid_promotion(promotion: char) -> bool {
681    let valid_promotions = ['Q', 'R', 'B', 'N'];
682    promotion.is_ascii_uppercase() && valid_promotions.contains(&promotion)
683}
684
685#[inline]
686fn index_to_file_notation(i: usize) -> char {
687    match i % 8 {
688        0 => 'a',
689        1 => 'b',
690        2 => 'c',
691        3 => 'd',
692        4 => 'e',
693        5 => 'f',
694        6 => 'g',
695        7 => 'h',
696        _ => ' ',
697    }
698}
699
700#[inline]
701fn index_to_rank_notation_unchecked(i: usize) -> char {
702    let rank_num = 8 - i / 8;
703    char::from_digit(rank_num.try_into().unwrap(), 10).unwrap()
704}
705
706fn rank_notation_to_indexes_unchecked(r: char) -> [usize; 8] {
707    let rank_num = r.to_digit(10).unwrap() as usize;
708    let rank_starts = [56, 48, 40, 32, 24, 16, 8, 0]; // 1st to 8th rank starting indexes
709    let mut indexes = [0; 8];
710    for (i, j) in indexes.iter_mut().enumerate() {
711        *j = rank_starts[rank_num - 1] + i;
712    }
713    indexes
714}
715
716fn file_notation_to_indexes_unchecked(f: char) -> [usize; 8] {
717    let file_offset = match f {
718        'a' => 0,
719        'b' => 1,
720        'c' => 2,
721        'd' => 3,
722        'e' => 4,
723        'f' => 5,
724        'g' => 6,
725        'h' => 7,
726        _ => unreachable!(),
727    };
728    let mut indexes = [0; 8];
729    for (i, j) in indexes.iter_mut().enumerate() {
730        *j = file_offset + i * 8;
731    }
732    indexes
733}
734
735#[cfg(test)]
736mod test {
737    use super::*;
738
739    #[test]
740    fn test_notation_new() {
741        let notation = Notation::new();
742
743        assert!(notation.piece.is_none());
744        assert!(notation.dis_file.is_none());
745        assert!(notation.dis_rank.is_none());
746        assert!(!notation.capture);
747        assert_eq!(notation.to_file, ' ');
748        assert_eq!(notation.to_rank, ' ');
749        assert!(notation.promotion.is_none());
750        assert!(!notation.check);
751        assert!(!notation.checkmate);
752    }
753
754    #[test]
755    fn test_notation_to_string() {
756        let mut notation = Notation::new();
757        notation.piece = Some('N');
758        notation.to_file = 'f';
759        notation.to_rank = '3';
760        notation.capture = true;
761
762        assert_eq!(notation.to_string(), "Nxf3");
763    }
764
765    #[test]
766    fn test_notation_from_mv_with_context() {
767        let bs = board::BoardState::new_starting();
768        let mv = Move {
769            piece: Piece {
770                ptype: PieceType::Knight,
771                pcolour: PieceColour::White,
772            },
773            from: 62,
774            to: 45,
775            move_type: MoveType::Normal,
776        };
777        let notation = Notation::from_mv_with_context(&bs, &mv);
778        assert!(notation.is_ok());
779        assert_eq!(notation.unwrap().to_string(), "Nf3");
780    }
781
782    #[test]
783    fn test_notation_from_str() -> Result<(), PGNParseError> {
784        let notation_str = "Qf3xf5+";
785        let notation = Notation::from_str(notation_str);
786        match notation {
787            Ok(notation) => {
788                assert_eq!(notation.piece, Some('Q'));
789                assert_eq!(notation.to_file, 'f');
790                assert_eq!(notation.to_rank, '5');
791                assert!(notation.capture);
792                assert!(notation.check);
793                assert!(notation.dis_file.is_some());
794                assert!(notation.dis_rank.is_some());
795                assert_eq!(notation.dis_file.unwrap(), 'f');
796                assert_eq!(notation.dis_rank.unwrap(), '3');
797                Ok(())
798            }
799            Err(e) => Err(e),
800        }
801    }
802
803    #[test]
804    fn test_notation_from_str_castle() -> Result<(), PGNParseError> {
805        let notation_str = "O-O";
806        let notation = Notation::from_str(notation_str)?;
807        assert_eq!(notation.castle_str, Some("O-O".to_string()));
808        assert!(!notation.check);
809        assert!(!notation.checkmate);
810
811        let notation_str = "O-O+";
812        let notation = Notation::from_str(notation_str)?;
813        assert_eq!(notation.castle_str, Some("O-O".to_string()));
814        assert!(notation.check);
815        assert!(!notation.checkmate);
816
817        let notation_str = "O-O-O#";
818        let notation = Notation::from_str(notation_str)?;
819        assert_eq!(notation.castle_str, Some("O-O-O".to_string()));
820        assert!(!notation.check);
821        assert!(notation.checkmate);
822
823        Ok(())
824    }
825
826    #[test]
827    fn test_notation_from_str_promotion() -> Result<(), PGNParseError> {
828        let notation_str = "e8=Q";
829        let notation = Notation::from_str(notation_str)?;
830        assert_eq!(notation.piece, None);
831        assert_eq!(notation.to_file, 'e');
832        assert_eq!(notation.to_rank, '8');
833        assert_eq!(notation.promotion, Some('Q'));
834        assert!(!notation.capture);
835        assert!(!notation.check);
836        assert!(!notation.checkmate);
837
838        let notation_str = "e8=Q+";
839        let notation = Notation::from_str(notation_str)?;
840        assert_eq!(notation.piece, None);
841        assert_eq!(notation.to_file, 'e');
842        assert_eq!(notation.to_rank, '8');
843        assert_eq!(notation.promotion, Some('Q'));
844        assert!(!notation.capture);
845        assert!(notation.check);
846        assert!(!notation.checkmate);
847
848        let notation_str = "e8=Q#";
849        let notation = Notation::from_str(notation_str)?;
850        assert_eq!(notation.piece, None);
851        assert_eq!(notation.to_file, 'e');
852        assert_eq!(notation.to_rank, '8');
853        assert_eq!(notation.promotion, Some('Q'));
854        assert!(!notation.capture);
855        assert!(!notation.check);
856        assert!(notation.checkmate);
857
858        Ok(())
859    }
860
861    #[test]
862    fn test_notation_from_str_invalid() {
863        let notation_str = "Qf9";
864        let notation = Notation::from_str(notation_str);
865        assert!(notation.is_err());
866
867        let notation_str = "Qz3";
868        let notation = Notation::from_str(notation_str);
869        assert!(notation.is_err());
870
871        let notation_str = "Qf3x";
872        let notation = Notation::from_str(notation_str);
873        assert!(notation.is_err());
874
875        let notation_str = "Qf3=";
876        let notation = Notation::from_str(notation_str);
877        assert!(notation.is_err());
878
879        let notation_str = "Qf3++";
880        let notation = Notation::from_str(notation_str);
881        assert!(notation.is_err());
882
883        let notation_str = "Qf3##";
884        let notation = Notation::from_str(notation_str);
885        assert!(notation.is_err());
886    }
887
888    #[test]
889    fn test_notation_to_move_with_context() {
890        let bs = board::BoardState::new_starting();
891        let notation = Notation::from_str("Nf3").unwrap();
892        let mv = notation.to_move_with_context(&bs);
893        assert!(mv.is_ok());
894        let mv = mv.unwrap();
895        assert_eq!(mv.piece.ptype, PieceType::Knight);
896        assert_eq!(mv.from, 62);
897        assert_eq!(mv.to, 45);
898
899        let notation = Notation::from_str("e4").unwrap();
900        let mv = notation.to_move_with_context(&bs);
901        assert!(mv.is_ok());
902        let mv = mv.unwrap();
903        assert_eq!(mv.piece.ptype, PieceType::Pawn);
904        assert_eq!(mv.from, 52);
905        assert_eq!(mv.to, 36);
906    }
907
908    #[test]
909    fn test_index_to_file_notation() {
910        assert_eq!(index_to_file_notation(0), 'a');
911        assert_eq!(index_to_file_notation(7), 'h');
912        assert_eq!(index_to_file_notation(35), 'd');
913    }
914
915    #[test]
916    fn test_index_to_rank_notation() {
917        assert_eq!(index_to_rank_notation_unchecked(0), '8');
918        assert_eq!(index_to_rank_notation_unchecked(7), '8');
919        assert_eq!(index_to_rank_notation_unchecked(35), '4');
920    }
921
922    #[test]
923    fn test_rank_notation_to_indexes_unchecked() {
924        assert_eq!(
925            rank_notation_to_indexes_unchecked('1'),
926            [56, 57, 58, 59, 60, 61, 62, 63]
927        );
928        assert_eq!(
929            rank_notation_to_indexes_unchecked('2'),
930            [48, 49, 50, 51, 52, 53, 54, 55]
931        );
932        assert_eq!(
933            rank_notation_to_indexes_unchecked('3'),
934            [40, 41, 42, 43, 44, 45, 46, 47]
935        );
936        assert_eq!(
937            rank_notation_to_indexes_unchecked('4'),
938            [32, 33, 34, 35, 36, 37, 38, 39]
939        );
940        assert_eq!(
941            rank_notation_to_indexes_unchecked('5'),
942            [24, 25, 26, 27, 28, 29, 30, 31]
943        );
944        assert_eq!(
945            rank_notation_to_indexes_unchecked('6'),
946            [16, 17, 18, 19, 20, 21, 22, 23]
947        );
948        assert_eq!(
949            rank_notation_to_indexes_unchecked('7'),
950            [8, 9, 10, 11, 12, 13, 14, 15]
951        );
952        assert_eq!(
953            rank_notation_to_indexes_unchecked('8'),
954            [0, 1, 2, 3, 4, 5, 6, 7]
955        );
956    }
957
958    #[test]
959    fn test_file_notation_to_indexes_unchecked() {
960        assert_eq!(
961            file_notation_to_indexes_unchecked('a'),
962            [0, 8, 16, 24, 32, 40, 48, 56]
963        );
964        assert_eq!(
965            file_notation_to_indexes_unchecked('b'),
966            [1, 9, 17, 25, 33, 41, 49, 57]
967        );
968        assert_eq!(
969            file_notation_to_indexes_unchecked('c'),
970            [2, 10, 18, 26, 34, 42, 50, 58]
971        );
972        assert_eq!(
973            file_notation_to_indexes_unchecked('d'),
974            [3, 11, 19, 27, 35, 43, 51, 59]
975        );
976        assert_eq!(
977            file_notation_to_indexes_unchecked('e'),
978            [4, 12, 20, 28, 36, 44, 52, 60]
979        );
980        assert_eq!(
981            file_notation_to_indexes_unchecked('f'),
982            [5, 13, 21, 29, 37, 45, 53, 61]
983        );
984        assert_eq!(
985            file_notation_to_indexes_unchecked('g'),
986            [6, 14, 22, 30, 38, 46, 54, 62]
987        );
988        assert_eq!(
989            file_notation_to_indexes_unchecked('h'),
990            [7, 15, 23, 31, 39, 47, 55, 63]
991        );
992    }
993}