hexgame/
coords.rs

1use std::error;
2use std::fmt;
3
4/// The type of a single coordinate (row or column).
5pub type CoordValue = u8;
6
7/// Coordinates of a single cell of the board.
8/// `hexgame` uses a zero-based (row, column)-format analogous to matrix-indices.
9///
10/// The following diagram shows on the left the format used by `Coords` and on the right
11/// the "c4" format similar to Chess that is commonly used in the literature.
12/// Note that the order of row-index and column-index is swapped between both formats:
13/// The marked cell has coordinates (1, 3) and d2, respectively.
14///
15/// ```text
16///  0  1  2  3  4           a  b  c  d  e
17/// 0\.  .  .  .  .\0       1\.  .  .  .  .\1
18///  1\.  .  .  ●  .\1       2\.  .  .  ●  .\2
19///   2\.  .  .  .  .\2       3\.  .  .  .  .\3
20///    3\.  .  .  .  .\3       4\.  .  .  .  .\4
21///     4\.  .  .  .  .\4       5\.  .  .  .  .\5
22///        0  1  2  3  4           a  b  c  d  e
23/// ```
24///
25/// The `from_str` and `to_string` methods can be used to convert between the formats.
26///
27/// ```
28/// # use hexgame::Coords;
29/// use std::str::FromStr;
30/// let coords = Coords::new(7, 0);
31/// // Note the different order!
32/// assert_eq!(coords.to_string(), "a8");
33///
34/// let other_coords = Coords::from_str("a8").unwrap();
35/// assert_eq!(coords, other_coords);
36/// ```
37#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38pub struct Coords {
39    /// Zero-based row index, counted from top to bottom.
40    pub row: CoordValue,
41    /// Zero-based column index, counted from left to right.
42    pub column: CoordValue,
43}
44
45impl Coords {
46    /// Create a new Coords instance. Watch out: Order of parameters is different from
47    /// the commonly used "c4" format.
48    pub fn new(row: CoordValue, column: CoordValue) -> Self {
49        Self { row, column }
50    }
51
52    /// Return whether this coordinate exist on a board of the given size.
53    pub fn is_on_board_with_size(&self, size: CoordValue) -> bool {
54        self.row < size && self.column < size
55    }
56}
57
58impl std::str::FromStr for Coords {
59    type Err = ParseCoordsError;
60
61    /// Parse a coordinate from "c4" format.
62    fn from_str(string: &str) -> Result<Self, Self::Err> {
63        let column = string.chars().next().and_then(parse_column_char);
64        let row = string
65            .get(1..)
66            .and_then(|s| s.parse::<CoordValue>().ok())
67            .filter(|&row| 0 < row)
68            .map(|row| row - 1);
69
70        match row.zip(column) {
71            Some((row, column)) => Ok(Coords { row, column }),
72            None => Err(ParseCoordsError {
73                description: format!("Invalid coordinates: {}", string),
74            }),
75        }
76    }
77}
78
79/// Convert a character to a single coordinate (characters are used to denote column indices in the "c4" format).
80/// Return None if `c` is not in the range a-z.
81pub fn parse_column_char(c: char) -> Option<CoordValue> {
82    if ('a'..='z').contains(&c) {
83        Some(((c as u8) - b'a') as CoordValue)
84    } else {
85        None
86    }
87}
88
89/// Convert a single coordinate (typically the column index) to the character that is used in the "c4" format.
90pub fn to_column_char(column: CoordValue) -> char {
91    (b'a' + (column as u8)) as char
92}
93
94impl fmt::Display for Coords {
95    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
96        write!(f, "{}{}", to_column_char(self.column), self.row + 1)
97    }
98}
99
100/// Returned by `Coords::from_str` if the string cannot be parsed.
101#[derive(Debug)]
102pub struct ParseCoordsError {
103    description: String,
104}
105
106impl fmt::Display for ParseCoordsError {
107    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
108        write!(f, "{}", self.description)
109    }
110}
111
112impl error::Error for ParseCoordsError {}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use std::str::FromStr;
118
119    #[test]
120    fn test_to_string() {
121        assert_eq!(Coords::new(0, 0).to_string(), "a1");
122        assert_eq!(Coords::new(12, 5).to_string(), "f13");
123    }
124
125    #[test]
126    fn test_from_str() {
127        assert_eq!(Coords::from_str("a1").unwrap(), Coords::new(0, 0));
128        assert_eq!(Coords::from_str("f13").unwrap(), Coords::new(12, 5));
129    }
130
131    #[test]
132    fn test_from_invalid_strings() {
133        assert!(Coords::from_str("").is_err());
134        assert!(Coords::from_str("abc").is_err());
135        assert!(Coords::from_str("A2").is_err());
136        assert!(Coords::from_str("a0").is_err());
137        assert!(Coords::from_str("ä2").is_err());
138    }
139}