chess_notation_parser/
square.rs

1//! Enum representation of a chess board square
2
3use std::fmt;
4
5/// # Chess board square
6///
7/// Square are identified with `<file><rank>` format:
8/// - `file` - Lowercase alphabet character from range `[A-H]`.
9///            Sometimes referred to as columns.
10/// - `rank` - Numeric character from range `[1-8]`.
11///            Sometimes referred to as rows.
12///
13/// Setup of white pieces is done on ranks `1` and `2`,
14/// whilist setup of black pieces is done on ranks `7` and `8`.
15///
16/// ## Example
17/// ```
18/// use chess_notation_parser::Square;
19///
20/// // Creation of squares is done with <&str>try_from() function
21/// assert_eq!(Ok(Square::A1), Square::try_from("a1"));
22///
23/// // Only lowercase letters should be used
24/// assert_eq!(Err("Invalid square input"), Square::try_from("A1"));
25///
26/// // Invalid input is rejected
27/// assert_eq!(Err("Invalid square input"), Square::try_from("A9"));
28/// assert_eq!(Err("Invalid square input"), Square::try_from("K1"));
29/// ```
30#[derive(Debug, Hash, PartialOrd, Ord, PartialEq, Copy, Clone, Eq)]
31#[rustfmt::skip]
32pub enum Square {
33    A8, B8, C8, D8, E8, F8, G8, H8,
34    A7, B7, C7, D7, E7, F7, G7, H7,
35    A6, B6, C6, D6, E6, F6, G6, H6,
36    A5, B5, C5, D5, E5, F5, G5, H5,
37    A4, B4, C4, D4, E4, F4, G4, H4,
38    A3, B3, C3, D3, E3, F3, G3, H3,
39    A2, B2, C2, D2, E2, F2, G2, H2,
40    A1, B1, C1, D1, E1, F1, G1, H1,
41}
42
43const RANK_1: &[&str] = &["a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1"];
44const RANK_2: &[&str] = &["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"];
45const RANK_3: &[&str] = &["a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3"];
46const RANK_4: &[&str] = &["a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4"];
47const RANK_5: &[&str] = &["a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5"];
48const RANK_6: &[&str] = &["a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6"];
49const RANK_7: &[&str] = &["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"];
50const RANK_8: &[&str] = &["a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8"];
51
52const FILE_A: &[&str] = &["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8"];
53const FILE_B: &[&str] = &["b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8"];
54const FILE_C: &[&str] = &["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8"];
55const FILE_D: &[&str] = &["d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8"];
56const FILE_E: &[&str] = &["e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8"];
57const FILE_F: &[&str] = &["f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8"];
58const FILE_G: &[&str] = &["g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8"];
59const FILE_H: &[&str] = &["h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8"];
60
61impl Square {
62    /// Get a set of squares representing certain file
63    ///
64    /// # Example
65    /// ```
66    /// use chess_notation_parser::Square;
67    ///
68    /// let file_a = Square::get_file('a').unwrap();
69    /// let mut iter = file_a.into_iter();
70    ///
71    /// assert_eq!(Some(Square::A1), iter.next());
72    /// assert_eq!(Some(Square::A2), iter.next());
73    /// assert_eq!(Some(Square::A3), iter.next());
74    /// assert_eq!(Some(Square::A4), iter.next());
75    /// assert_eq!(Some(Square::A5), iter.next());
76    /// assert_eq!(Some(Square::A6), iter.next());
77    /// assert_eq!(Some(Square::A7), iter.next());
78    /// assert_eq!(Some(Square::A8), iter.next());
79    /// assert_eq!(None, iter.next());
80    /// ```
81    pub fn get_file(file: char) -> Result<Vec<Square>, &'static str> {
82        let file = match file {
83            'a' => FILE_A,
84            'b' => FILE_B,
85            'c' => FILE_C,
86            'd' => FILE_D,
87            'e' => FILE_E,
88            'f' => FILE_F,
89            'g' => FILE_G,
90            'h' => FILE_H,
91            _ => return Err("Invalid 'file' character received"),
92        };
93
94        Ok(file
95            .iter() // We are safe to use unwrap if we reached this point
96            .map(|square| Square::try_from(*square).unwrap())
97            .collect::<Vec<Square>>())
98    }
99
100    /// Get a set of squares representing certain rank
101    ///
102    /// # Example
103    /// ```
104    /// use chess_notation_parser::Square;
105    ///
106    /// let rank_a = Square::get_rank('2').unwrap();
107    /// let mut iter = rank_a.into_iter();
108    ///
109    /// assert_eq!(Some(Square::A2), iter.next());
110    /// assert_eq!(Some(Square::B2), iter.next());
111    /// assert_eq!(Some(Square::C2), iter.next());
112    /// assert_eq!(Some(Square::D2), iter.next());
113    /// assert_eq!(Some(Square::E2), iter.next());
114    /// assert_eq!(Some(Square::F2), iter.next());
115    /// assert_eq!(Some(Square::G2), iter.next());
116    /// assert_eq!(Some(Square::H2), iter.next());
117    /// assert_eq!(None, iter.next());
118    /// ```
119    pub fn get_rank(rank: char) -> Result<Vec<Square>, &'static str> {
120        let rank = match rank {
121            '8' => RANK_8,
122            '7' => RANK_7,
123            '5' => RANK_5,
124            '6' => RANK_6,
125            '4' => RANK_4,
126            '3' => RANK_3,
127            '2' => RANK_2,
128            '1' => RANK_1,
129            _ => return Err("Invalid 'rank' character received"),
130        };
131
132        Ok(rank
133            .iter() // We are safe to use unwrap if we reached this point
134            .map(|square| Square::try_from(*square).unwrap())
135            .collect::<Vec<Square>>())
136    }
137
138    /// Get relative neighbor using relative `(x,y)` coordinates
139    ///
140    /// # Example
141    /// ```
142    /// use chess_notation_parser::Square;
143    ///
144    /// let d4 = Square::D4;
145    ///
146    /// let d2 = d4.get_relative_neighbor(0, -2).unwrap();
147    /// assert_eq!(d2, Square::D2);
148    ///
149    /// let e5 = d4.get_relative_neighbor(1, 1).unwrap();
150    /// assert_eq!(e5, Square::E5);
151    /// ```
152    pub fn get_relative_neighbor(
153        &self,
154        x: i8,
155        y: i8,
156    ) -> Result<Square, &'static str> {
157        let dst = *self as i8;
158
159        let dst_mod = dst % 8;
160        let l_border = match dst_mod {
161            0 => 0,
162            dst_mod => -dst_mod,
163        };
164        let r_border = 8 - dst_mod;
165        if x < l_border || x >= r_border {
166            return Err("Square out of range");
167        }
168
169        let y = dst - y * 8;
170
171        let result = x + y;
172        if result < 0 || result > Square::H1 as i8 {
173            return Err("Square out of range");
174        }
175
176        use std::mem::transmute;
177        let dst: Self = unsafe { transmute(result as i8) };
178        Ok(dst)
179    }
180
181    /// Get file char
182    pub fn get_file_char(&self) -> char {
183        use Square::*;
184        match self {
185            A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 => 'a',
186            B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 => 'b',
187            C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 => 'c',
188            D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 => 'd',
189            E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 => 'e',
190            F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 => 'f',
191            G1 | G2 | G3 | G4 | G5 | G6 | G7 | G8 => 'g',
192            H1 | H2 | H3 | H4 | H5 | H6 | H7 | H8 => 'h',
193        }
194    }
195
196    /// Get rank char
197    pub fn get_rank_char(&self) -> char {
198        use Square::*;
199        match self {
200            A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 => '8',
201            A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 => '7',
202            A5 | B5 | C5 | D5 | E5 | F5 | G5 | H5 => '5',
203            A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 => '6',
204            A4 | B4 | C4 | D4 | E4 | F4 | G4 | H4 => '4',
205            A3 | B3 | C3 | D3 | E3 | F3 | G3 | H3 => '3',
206            A2 | B2 | C2 | D2 | E2 | F2 | G2 | H2 => '2',
207            A1 | B1 | C1 | D1 | E1 | F1 | G1 | H1 => '1',
208        }
209    }
210}
211
212impl From<u8> for Square {
213    fn from(square: u8) -> Self {
214        // Now, some real and honest work done here
215        #[rustfmt::skip]
216        const SQUARES: [Square; 64] = [
217            Square::A8, Square::B8, Square::C8, Square::D8,
218            Square::E8, Square::F8, Square::G8, Square::H8,
219            Square::A7, Square::B7, Square::C7, Square::D7,
220            Square::E7, Square::F7, Square::G7, Square::H7,
221            Square::A6, Square::B6, Square::C6, Square::D6,
222            Square::E6, Square::F6, Square::G6, Square::H6,
223            Square::A5, Square::B5, Square::C5, Square::D5,
224            Square::E5, Square::F5, Square::G5, Square::H5,
225            Square::A4, Square::B4, Square::C4, Square::D4,
226            Square::E4, Square::F4, Square::G4, Square::H4,
227            Square::A3, Square::B3, Square::C3, Square::D3,
228            Square::E3, Square::F3, Square::G3, Square::H3,
229            Square::A2, Square::B2, Square::C2, Square::D2,
230            Square::E2, Square::F2, Square::G2, Square::H2,
231            Square::A1, Square::B1, Square::C1, Square::D1,
232            Square::E1, Square::F1, Square::G1, Square::H1,
233        ];
234
235        let i = square as usize;
236        assert!(i < SQUARES.len(), "Square index={} is out of range", i);
237        SQUARES[i]
238    }
239}
240
241impl TryFrom<&str> for Square {
242    type Error = &'static str;
243
244    fn try_from(square: &str) -> Result<Self, Self::Error> {
245        if square.len() != 2 {
246            return Err("Invalid square length");
247        }
248
249        // Ain't this silly. But should be fast! Hopefully...
250        match square {
251            "a1" => Ok(Self::A1),
252            "a2" => Ok(Self::A2),
253            "a3" => Ok(Self::A3),
254            "a4" => Ok(Self::A4),
255            "a5" => Ok(Self::A5),
256            "a6" => Ok(Self::A6),
257            "a7" => Ok(Self::A7),
258            "a8" => Ok(Self::A8),
259
260            "b1" => Ok(Self::B1),
261            "b2" => Ok(Self::B2),
262            "b3" => Ok(Self::B3),
263            "b4" => Ok(Self::B4),
264            "b5" => Ok(Self::B5),
265            "b6" => Ok(Self::B6),
266            "b7" => Ok(Self::B7),
267            "b8" => Ok(Self::B8),
268
269            "c1" => Ok(Self::C1),
270            "c2" => Ok(Self::C2),
271            "c3" => Ok(Self::C3),
272            "c4" => Ok(Self::C4),
273            "c5" => Ok(Self::C5),
274            "c6" => Ok(Self::C6),
275            "c7" => Ok(Self::C7),
276            "c8" => Ok(Self::C8),
277
278            "d1" => Ok(Self::D1),
279            "d2" => Ok(Self::D2),
280            "d3" => Ok(Self::D3),
281            "d4" => Ok(Self::D4),
282            "d5" => Ok(Self::D5),
283            "d6" => Ok(Self::D6),
284            "d7" => Ok(Self::D7),
285            "d8" => Ok(Self::D8),
286
287            "e1" => Ok(Self::E1),
288            "e2" => Ok(Self::E2),
289            "e3" => Ok(Self::E3),
290            "e4" => Ok(Self::E4),
291            "e5" => Ok(Self::E5),
292            "e6" => Ok(Self::E6),
293            "e7" => Ok(Self::E7),
294            "e8" => Ok(Self::E8),
295
296            "f1" => Ok(Self::F1),
297            "f2" => Ok(Self::F2),
298            "f3" => Ok(Self::F3),
299            "f4" => Ok(Self::F4),
300            "f5" => Ok(Self::F5),
301            "f6" => Ok(Self::F6),
302            "f7" => Ok(Self::F7),
303            "f8" => Ok(Self::F8),
304
305            "g1" => Ok(Self::G1),
306            "g2" => Ok(Self::G2),
307            "g3" => Ok(Self::G3),
308            "g4" => Ok(Self::G4),
309            "g5" => Ok(Self::G5),
310            "g6" => Ok(Self::G6),
311            "g7" => Ok(Self::G7),
312            "g8" => Ok(Self::G8),
313
314            "h1" => Ok(Self::H1),
315            "h2" => Ok(Self::H2),
316            "h3" => Ok(Self::H3),
317            "h4" => Ok(Self::H4),
318            "h5" => Ok(Self::H5),
319            "h6" => Ok(Self::H6),
320            "h7" => Ok(Self::H7),
321            "h8" => Ok(Self::H8),
322
323            _ => Err("Invalid square input"),
324        }
325    }
326}
327
328impl fmt::Display for Square {
329    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
330        write!(f, "{}{}", self.get_file_char(), self.get_rank_char())
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn try_from_invalid_length() {
340        let err = Err("Invalid square length");
341
342        assert_eq!(err, Square::try_from("h"));
343        assert_eq!(err, Square::try_from("a10"));
344    }
345
346    #[test]
347    fn get_rank_with_wrong_char_failure() {
348        let err = Err("Invalid 'rank' character received");
349
350        // We allow only lowercase letters
351        assert_eq!(err, Square::get_rank('9'));
352
353        // Garbage letter
354        assert_eq!(err, Square::get_rank('z'));
355    }
356
357    #[test]
358    fn get_file_with_wrong_char_failure() {
359        let err = Err("Invalid 'file' character received");
360
361        // We allow only lowercase letters
362        assert_eq!(err, Square::get_file('A'));
363
364        // Garbage letter
365        assert_eq!(err, Square::get_file('z'));
366    }
367
368    #[test]
369    #[should_panic]
370    fn from_u8() {
371        // The first enum member
372        let square = Square::A8;
373        assert_eq!(square as u8, 0);
374        assert_eq!(square, Square::from(square as u8));
375
376        // The last enum member
377        let square = Square::H1;
378        assert_eq!(square as u8, 63);
379        assert_eq!(square, Square::from(square as u8));
380
381        // Any integer after the last enum member must panic if given to
382        // `from` function
383        let _panic = Square::from(Square::H1 as u8 + 1);
384    }
385
386    #[test]
387    fn get_rank_success() {
388        let rank_str_char_pairs = [
389            (RANK_1, '1'),
390            (RANK_2, '2'),
391            (RANK_3, '3'),
392            (RANK_4, '4'),
393            (RANK_5, '5'),
394            (RANK_6, '6'),
395            (RANK_7, '7'),
396            (RANK_8, '8'),
397        ];
398
399        let c_test_rank_pair = |rank_strings: &[&str], rank_char| {
400            let rank: Vec<Square> = rank_strings
401                .iter()
402                .map(|square| Square::try_from(*square).unwrap())
403                .collect();
404            assert_eq!(rank, Square::get_rank(rank_char).unwrap());
405        };
406
407        rank_str_char_pairs
408            .iter()
409            .for_each(|(rank_strings, rank_char)| {
410                c_test_rank_pair(rank_strings, *rank_char);
411            });
412    }
413
414    #[test]
415    fn get_file_success() {
416        let file_str_char_pairs = [
417            (FILE_A, 'a'),
418            (FILE_B, 'b'),
419            (FILE_C, 'c'),
420            (FILE_D, 'd'),
421            (FILE_E, 'e'),
422            (FILE_F, 'f'),
423            (FILE_G, 'g'),
424            (FILE_H, 'h'),
425        ];
426
427        let c_test_file_pair = |file_strings: &[&str], file_char| {
428            let file: Vec<Square> = file_strings
429                .iter()
430                .map(|square| Square::try_from(*square).unwrap())
431                .collect();
432            assert_eq!(file, Square::get_file(file_char).unwrap());
433        };
434
435        file_str_char_pairs
436            .iter()
437            .for_each(|(file_strings, file_char)| {
438                c_test_file_pair(file_strings, *file_char);
439            });
440    }
441
442    #[test]
443    fn get_relative_neighbor_suceess() {
444        let square = Square::A1;
445        assert_eq!(square.get_relative_neighbor(0, 0), Ok(Square::A1));
446        assert_eq!(square.get_relative_neighbor(1, 0), Ok(Square::B1));
447        assert_eq!(square.get_relative_neighbor(0, 1), Ok(Square::A2));
448        assert_eq!(square.get_relative_neighbor(1, 1), Ok(Square::B2));
449        assert_eq!(square.get_relative_neighbor(2, 2), Ok(Square::C3));
450        assert_eq!(square.get_relative_neighbor(4, 4), Ok(Square::E5));
451        assert_eq!(square.get_relative_neighbor(7, 7), Ok(Square::H8));
452
453        let square1 = Square::D4;
454        let square2 = Square::E5;
455        assert_eq!(
456            square1.get_relative_neighbor(1, 0),
457            square2.get_relative_neighbor(0, -1)
458        );
459
460        let square = Square::H8;
461        assert_eq!(square.get_relative_neighbor(-1, 0), Ok(Square::G8));
462        assert_eq!(square.get_relative_neighbor(0, -1), Ok(Square::H7));
463        assert_eq!(square.get_relative_neighbor(-1, -1), Ok(Square::G7));
464        assert_eq!(square.get_relative_neighbor(-3, -7), Ok(Square::E1));
465    }
466
467    #[test]
468    fn get_relative_neighbor_failure() {
469        for square in Square::get_rank('1').unwrap().into_iter() {
470            assert_eq!(
471                square.get_relative_neighbor(0, -1),
472                Err("Square out of range")
473            );
474        }
475        for square in Square::get_rank('8').unwrap().into_iter() {
476            assert_eq!(
477                square.get_relative_neighbor(0, 1),
478                Err("Square out of range")
479            );
480        }
481        for square in Square::get_rank('7').unwrap().into_iter() {
482            assert_eq!(
483                square.get_relative_neighbor(0, 3),
484                Err("Square out of range")
485            );
486        }
487
488        for square in Square::get_file('a').unwrap().into_iter() {
489            assert_eq!(
490                square.get_relative_neighbor(-1, 0),
491                Err("Square out of range")
492            );
493        }
494        for square in Square::get_file('h').unwrap().into_iter() {
495            assert_eq!(
496                square.get_relative_neighbor(1, 0),
497                Err("Square out of range")
498            );
499        }
500        for square in Square::get_file('g').unwrap().into_iter() {
501            assert_eq!(
502                square.get_relative_neighbor(2, 0),
503                Err("Square out of range")
504            );
505        }
506    }
507}