1use core::fmt;
18use core::str::FromStr;
19
20use thiserror::Error;
21
22const BOARD_WIDTH: u8 = 8;
23
24pub const SQUARE_COUNT: usize = (BOARD_WIDTH as usize) * (BOARD_WIDTH as usize);
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
29#[repr(u8)]
30pub enum File {
31 A,
33
34 B,
36
37 C,
39
40 D,
42
43 E,
45
46 F,
48
49 G,
51
52 H,
54}
55
56impl File {
57 pub const fn index(self) -> u8 {
68 self as u8
69 }
70
71 const fn from_ascii(byte: u8) -> Option<Self> {
72 match byte {
73 b'a' | b'A' => Some(Self::A),
74 b'b' | b'B' => Some(Self::B),
75 b'c' | b'C' => Some(Self::C),
76 b'd' | b'D' => Some(Self::D),
77 b'e' | b'E' => Some(Self::E),
78 b'f' | b'F' => Some(Self::F),
79 b'g' | b'G' => Some(Self::G),
80 b'h' | b'H' => Some(Self::H),
81 _ => None,
82 }
83 }
84}
85
86impl fmt::Display for File {
87 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88 formatter.write_str(match self {
89 Self::A => "a",
90 Self::B => "b",
91 Self::C => "c",
92 Self::D => "d",
93 Self::E => "e",
94 Self::F => "f",
95 Self::G => "g",
96 Self::H => "h",
97 })
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
103#[repr(u8)]
104pub enum Rank {
105 One,
107
108 Two,
110
111 Three,
113
114 Four,
116
117 Five,
119
120 Six,
122
123 Seven,
125
126 Eight,
128}
129
130impl Rank {
131 pub const fn index(self) -> u8 {
142 self as u8
143 }
144
145 const fn from_ascii(byte: u8) -> Option<Self> {
146 match byte {
147 b'1' => Some(Self::One),
148 b'2' => Some(Self::Two),
149 b'3' => Some(Self::Three),
150 b'4' => Some(Self::Four),
151 b'5' => Some(Self::Five),
152 b'6' => Some(Self::Six),
153 b'7' => Some(Self::Seven),
154 b'8' => Some(Self::Eight),
155 _ => None,
156 }
157 }
158}
159
160impl fmt::Display for Rank {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 formatter.write_str(match self {
163 Self::One => "1",
164 Self::Two => "2",
165 Self::Three => "3",
166 Self::Four => "4",
167 Self::Five => "5",
168 Self::Six => "6",
169 Self::Seven => "7",
170 Self::Eight => "8",
171 })
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
177#[repr(transparent)]
178pub struct Square(u8);
179
180impl Square {
181 pub(crate) const ALL: [Self; SQUARE_COUNT] = {
184 let mut squares = [Self(0); SQUARE_COUNT];
185 let mut index = 0;
186 while index < SQUARE_COUNT {
187 squares[index] = Self(index as u8);
188 index += 1;
189 }
190 squares
191 };
192
193 pub const fn new(file: File, rank: Rank) -> Self {
204 Self(rank.index() * BOARD_WIDTH + file.index())
205 }
206
207 pub const fn from_index(index: usize) -> Option<Self> {
220 if index < SQUARE_COUNT {
221 Some(Self(index as u8))
222 } else {
223 None
224 }
225 }
226
227 pub const fn index(self) -> usize {
238 self.0 as usize
239 }
240
241 pub const fn file(self) -> File {
251 match self.0 % BOARD_WIDTH {
252 0 => File::A,
253 1 => File::B,
254 2 => File::C,
255 3 => File::D,
256 4 => File::E,
257 5 => File::F,
258 6 => File::G,
259 7 => File::H,
260 _ => unreachable!(),
261 }
262 }
263
264 pub const fn rank(self) -> Rank {
274 match self.0 / BOARD_WIDTH {
275 0 => Rank::One,
276 1 => Rank::Two,
277 2 => Rank::Three,
278 3 => Rank::Four,
279 4 => Rank::Five,
280 5 => Rank::Six,
281 6 => Rank::Seven,
282 7 => Rank::Eight,
283 _ => unreachable!(),
284 }
285 }
286}
287
288impl fmt::Display for Square {
289 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(formatter, "{}{}", self.file(), self.rank())
291 }
292}
293
294impl From<Square> for usize {
295 fn from(square: Square) -> Self {
296 square.index()
297 }
298}
299
300impl FromStr for Square {
301 type Err = ParseSquareError;
302
303 fn from_str(value: &str) -> Result<Self, Self::Err> {
304 let [file, rank] = value.as_bytes() else {
305 return Err(ParseSquareError);
306 };
307 let file = File::from_ascii(*file).ok_or(ParseSquareError)?;
308 let rank = Rank::from_ascii(*rank).ok_or(ParseSquareError)?;
309
310 Ok(Self::new(file, rank))
311 }
312}
313
314#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
325#[error("invalid square notation")]
326pub struct ParseSquareError;
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn squares_use_canonical_a1_to_h8_indexes() {
334 assert_eq!(Square::ALL.len(), SQUARE_COUNT);
335 assert_eq!(Square::ALL[0], Square::new(File::A, Rank::One));
336 assert_eq!(
337 Square::ALL[SQUARE_COUNT - 1],
338 Square::new(File::H, Rank::Eight)
339 );
340 assert_eq!(Square::new(File::A, Rank::One).index(), 0);
341 assert_eq!(Square::new(File::H, Rank::Eight).index(), 63);
342 assert_eq!(Square::from_index(64), None);
343 }
344
345 #[test]
346 fn square_notation_parses_case_insensitively() {
347 assert_eq!(
348 "H8".parse::<Square>(),
349 Ok(Square::new(File::H, Rank::Eight))
350 );
351 assert_eq!("a1".parse::<Square>(), Ok(Square::new(File::A, Rank::One)));
352 }
353
354 #[test]
355 fn invalid_square_notation_is_rejected() {
356 assert_eq!("i1".parse::<Square>(), Err(ParseSquareError));
357 assert_eq!("a9".parse::<Square>(), Err(ParseSquareError));
358 assert_eq!("a10".parse::<Square>(), Err(ParseSquareError));
359 }
360}