//! Standard algebraic notation. //! //! SAN notation is the FIDE standard for writing moves. //! //! In its simplest form, it consists of the type of the moving piece followed by the target //! square. This may be ambiguous, in which cases the origin file and/or rank is also specified. //! The notation is shortened for pawns, and extra information may be added, to specify a capture //! or a check. //! //! Examples: *`e4`*, *`Qxd8#`*, *`O-O`*, *`h7h8=Q`* use crate::board::*; use crate::position::*; /// **The standard algebraic notation of a move.** /// /// /// When converting [`San`] notation to a playable [`Move`], the optional capture flag (*x*) and /// suffix (*+* or *#*) are ignored (as they are redundant). Thus, conversion will not fail if they /// are incorrectly set. Similarly, conversion will not fail when the move is unnecessarily /// disambiguated. For example, *Ke1xd1* and *Kd1* will always be equivalent, even if there is no /// piece on *d1*. /// /// SAN notation can be obtained from a legal move using the [`Move::to_san`] method. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct San { pub(crate) inner: SanInner, pub(crate) suffix: Option, } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum SanInner { Castle(CastlingSide), Normal { role: Role, file: Option, rank: Option, capture: bool, target: Square, promotion: Option, }, } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum SanSuffix { Check, Checkmate, } impl std::fmt::Display for San { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.inner { SanInner::Castle(CastlingSide::Short) => write!(f, "O-O")?, SanInner::Castle(CastlingSide::Long) => write!(f, "O-O-O")?, SanInner::Normal { role, file, rank, capture, target, promotion, } => { if role != Role::Pawn { write!(f, "{}", role.to_char_uppercase())?; } if let Some(file) = file { write!(f, "{}", file.to_char())?; } if let Some(rank) = rank { write!(f, "{}", rank.to_char())?; } if capture { write!(f, "x")?; } write!(f, "{}", target)?; if let Some(promotion) = promotion { write!(f, "={}", promotion.to_char_uppercase())?; } } } match self.suffix { Some(SanSuffix::Check) => write!(f, "+")?, Some(SanSuffix::Checkmate) => write!(f, "#")?, None => (), } Ok(()) } } impl std::fmt::Debug for San { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple("San").field(&self.to_string()).finish() } } impl std::str::FromStr for San { type Err = ParseSanError; #[inline] fn from_str(s: &str) -> Result { San::from_ascii(s.as_bytes()).ok_or(ParseSanError) } } /// A syntax error when parsing [`San`] notation. #[derive(Debug)] pub struct ParseSanError; impl std::fmt::Display for ParseSanError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("invalid SAN syntax") } } impl std::error::Error for ParseSanError {} /// An error while converting [`San`] notation to a playable [`Move`]. #[derive(Debug, PartialEq, Eq)] pub enum InvalidSan { /// There is no move on the position that matches the SAN notation. Illegal, /// There is more than one move on the position that matches the SAN notation. Ambiguous, } impl std::fmt::Display for InvalidSan { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let details = match self { Self::Illegal => "illegal move", Self::Ambiguous => "ambiguous move", }; write!(f, "invalid SAN ({details})") } } impl std::error::Error for InvalidSan {} impl San { /// Tries to convert SAN notation to a playable move. /// /// This function ignores the suffix and the capture flag. It also accepts unnecessarily /// desambiguated moves. #[inline] pub fn to_move<'l>(&self, position: &'l Position) -> Result, InvalidSan> { position.move_from_san(self) } /// Tries to read SAN notation from ascii text. pub fn from_ascii(s: &[u8]) -> Option { let mut r = s.iter().copied().rev(); let mut cur = r.next()?; let suffix = match cur { b'+' => { cur = r.next()?; Some(SanSuffix::Check) } b'#' => { cur = r.next()?; Some(SanSuffix::Checkmate) } _ => None, }; let inner = match cur { b'O' => SanInner::Castle({ let b'-' = r.next()? else { return None }; let b'O' = r.next()? else { return None }; match r.next() { None => CastlingSide::Short, Some(b'-') => { let b'O' = r.next()? else { return None }; r.next().is_none().then_some(())?; CastlingSide::Long } Some(_) => return None, } }), _ => { let promotion = Role::from_ascii(cur); if promotion.is_some() { (r.next()? == b'=').then_some(())?; cur = r.next()?; } let target_rank = Rank::from_ascii(cur)?; let target_file = File::from_ascii(r.next()?)?; let target = Square::from_coords(target_file, target_rank); let mut cur = r.next(); let capture = cur == Some(b'x'); if capture { cur = r.next(); } let rank = cur.and_then(Rank::from_ascii); if rank.is_some() { cur = r.next(); } let file = cur.and_then(File::from_ascii); if file.is_some() { cur = r.next(); } let role = match cur { Some(a) => { cur = r.next(); Role::from_ascii(a)? } None => Role::Pawn, }; cur.is_none().then_some(())?; (role != Role::Pawn || file.is_some() || !capture).then_some(())?; (role == Role::Pawn || promotion.is_none()).then_some(())?; SanInner::Normal { role, file, rank, capture, target, promotion, } } }; Some(Self { inner, suffix }) } }