open_pql/base/
suit.rs

1use super::{Display, FromStr, Hash, N_SUITS, ParseError, mem};
2
3/// Enum for Suits
4///
5/// Represents the four suits in a standard deck of playing cards.
6/// Suits are ordered Spades < Hearts < Diamonds < Clubs for consistent comparison.
7/// Each suit has a unique numeric value (0-3) and character representation.
8///
9/// # Examples
10///
11/// ```
12/// use open_pql::{Suit, Suit::*};
13///
14/// let suit = S; // Spades
15/// assert_eq!(suit.to_string(), "s");
16/// assert_eq!(suit as u8, 0);
17///
18/// let parsed: Suit = "h".parse().unwrap();
19/// assert_eq!(parsed, H);
20/// ```
21#[derive(
22    Copy, Clone, PartialEq, Eq, Debug, Ord, PartialOrd, Hash, Display, Default,
23)]
24pub enum Suit {
25    /// <span class="stab emoji">♠️</span> Spade
26    #[display("s")]
27    #[default]
28    S = 0,
29    /// <span class="stab emoji">♥️</span> Heart
30    #[display("h")]
31    H,
32    /// <span class="stab emoji">♦️</span> Diamond
33    #[display("d")]
34    D,
35    /// <span class="stab emoji">♣️</span> Club
36    #[display("c")]
37    C,
38}
39
40impl Suit {
41    /// All possible suits
42    pub const ARR_ALL: [Self; N_SUITS as usize] =
43        [Self::S, Self::H, Self::D, Self::C];
44
45    /// Creates a suit from a u8 value (0-3)
46    pub(crate) fn from_u8(v: u8) -> Self {
47        debug_assert!(v < N_SUITS, "invalid suit: {v}");
48        unsafe { mem::transmute(v) }
49    }
50
51    /// Creates a suit from a character
52    pub const fn from_char(c: char) -> Option<Self> {
53        match c {
54            'S' | 's' => Some(Self::S),
55            'H' | 'h' => Some(Self::H),
56            'D' | 'd' => Some(Self::D),
57            'C' | 'c' => Some(Self::C),
58            _ => None,
59        }
60    }
61}
62
63impl TryFrom<char> for Suit {
64    type Error = ParseError;
65
66    fn try_from(c: char) -> Result<Self, Self::Error> {
67        match c {
68            'S' | 's' => Ok(Self::S),
69            'H' | 'h' => Ok(Self::H),
70            'D' | 'd' => Ok(Self::D),
71            'C' | 'c' => Ok(Self::C),
72            _ => Err(ParseError::InvalidSuit(c.into())),
73        }
74    }
75}
76
77impl FromStr for Suit {
78    type Err = ParseError;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        let mut cs = s.chars().filter(|c| !c.is_whitespace());
82        if let Some(c) = cs.next()
83            && let Ok(s) = Self::try_from(c)
84            && cs.next().is_none()
85        {
86            return Ok(s);
87        }
88        Err(ParseError::InvalidSuit(s.into()))
89    }
90}
91
92impl From<Suit> for char {
93    fn from(value: Suit) -> Self {
94        match value {
95            Suit::S => 's',
96            Suit::H => 'h',
97            Suit::D => 'd',
98            Suit::C => 'c',
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::*;
107
108    impl Arbitrary for Suit {
109        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
110            *g.choose(&Self::ARR_ALL).unwrap()
111        }
112    }
113
114    #[test]
115    fn test_consts() {
116        assert_eq!(Suit::ARR_ALL, [Suit::S, Suit::H, Suit::D, Suit::C]);
117    }
118
119    #[test]
120    fn test_as_int() {
121        assert_eq!(Suit::S as i8, 0);
122        assert_eq!(Suit::H as i8, 1);
123        assert_eq!(Suit::D as i8, 2);
124        assert_eq!(Suit::C as i8, 3);
125    }
126
127    #[test]
128    fn test_from_char() {
129        assert_eq!(Ok(Suit::S), 's'.try_into());
130        assert_eq!(Ok(Suit::H), 'h'.try_into());
131        assert_eq!(Ok(Suit::D), 'd'.try_into());
132        assert_eq!(Ok(Suit::C), 'c'.try_into());
133
134        assert_eq!(Ok(Suit::S), 'S'.try_into());
135        assert_eq!(Ok(Suit::H), 'H'.try_into());
136        assert_eq!(Ok(Suit::D), 'D'.try_into());
137        assert_eq!(Ok(Suit::C), 'C'.try_into());
138
139        assert_eq!(
140            Err(ParseError::InvalidSuit("?".into())),
141            Suit::try_from('?')
142        );
143    }
144
145    #[test]
146    fn test_from_char_option() {
147        assert_eq!(Some(Suit::S), Suit::from_char('s'));
148        assert_eq!(Some(Suit::H), Suit::from_char('h'));
149        assert_eq!(Some(Suit::D), Suit::from_char('d'));
150        assert_eq!(Some(Suit::C), Suit::from_char('c'));
151
152        assert_eq!(Some(Suit::S), Suit::from_char('S'));
153        assert_eq!(Some(Suit::H), Suit::from_char('H'));
154        assert_eq!(Some(Suit::D), Suit::from_char('D'));
155        assert_eq!(Some(Suit::C), Suit::from_char('C'));
156
157        assert_eq!(None, Suit::from_char('?'));
158        assert_eq!(None, Suit::from_char('1'));
159        assert_eq!(None, Suit::from_char('X'));
160    }
161
162    #[test]
163    fn test_from_str() {
164        assert_eq!(Ok(Suit::S), " s ".parse());
165        assert_eq!(
166            Err(ParseError::InvalidSuit("sS".into())),
167            "sS".parse::<Suit>()
168        );
169        assert!("".parse::<Suit>().is_err());
170        assert!("?".parse::<Suit>().is_err());
171    }
172
173    #[test]
174    fn test_to_string() {
175        assert_eq!("s", &Suit::S.to_string());
176        assert_eq!("h", &Suit::H.to_string());
177        assert_eq!("d", &Suit::D.to_string());
178        assert_eq!("c", &Suit::C.to_string());
179    }
180
181    #[test]
182    fn test_to_char() {
183        assert_eq!('s', char::from(Suit::S));
184        assert_eq!('h', char::from(Suit::H));
185        assert_eq!('d', char::from(Suit::D));
186        assert_eq!('c', char::from(Suit::C));
187    }
188}