openpql_prelude/card/
rank.rs

1use super::{CardCount, Display, FromStr, Hash, Idx, ParseError};
2#[cfg(feature = "python")]
3use crate::python::*;
4
5/// Card rank representation.
6///
7/// Represents card ranks from 2 to Ace, with parsing support and conversion utilities.
8#[cfg_attr(feature = "python", pyclass(eq, ord, str, frozen, hash))]
9#[derive(
10    Copy, Clone, PartialEq, Eq, Debug, Ord, PartialOrd, Hash, Display, Default,
11)]
12pub enum Rank {
13    #[default]
14    #[display("2")]
15    R2 = 0,
16    #[display("3")]
17    R3,
18    #[display("4")]
19    R4,
20    #[display("5")]
21    R5,
22    #[display("6")]
23    R6,
24    #[display("7")]
25    R7,
26    #[display("8")]
27    R8,
28    #[display("9")]
29    R9,
30    #[display("T")]
31    RT,
32    #[display("J")]
33    RJ,
34    #[display("Q")]
35    RQ,
36    #[display("K")]
37    RK,
38    #[display("A")]
39    RA,
40}
41
42impl Rank {
43    /// Number of ranks in a standard deck
44    pub const N_RANKS: CardCount = 13;
45
46    /// Number of ranks in a short deck
47    pub const N_RANKS_SD: CardCount = 9;
48
49    /// Character representations for ranks
50    pub const CHARS: [char; Self::N_RANKS as usize] = [
51        '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A',
52    ];
53
54    /// Array of all 13 ranks.
55    const ARR_ALL: [Self; Self::N_RANKS as usize] = [
56        Self::R2,
57        Self::R3,
58        Self::R4,
59        Self::R5,
60        Self::R6,
61        Self::R7,
62        Self::R8,
63        Self::R9,
64        Self::RT,
65        Self::RJ,
66        Self::RQ,
67        Self::RK,
68        Self::RA,
69    ];
70
71    /// Array of all 9 ranks in a short deck (6+).
72    const ARR_ALL_SD: [Self; Self::N_RANKS_SD as usize] = [
73        Self::R6,
74        Self::R7,
75        Self::R8,
76        Self::R9,
77        Self::RT,
78        Self::RJ,
79        Self::RQ,
80        Self::RK,
81        Self::RA,
82    ];
83
84    /// Converts a character to a rank, returning `None` if invalid.
85    #[inline]
86    pub const fn from_char(c: char) -> Option<Self> {
87        match c {
88            '2' => Some(Self::R2),
89            '3' => Some(Self::R3),
90            '4' => Some(Self::R4),
91            '5' => Some(Self::R5),
92            '6' => Some(Self::R6),
93            '7' => Some(Self::R7),
94            '8' => Some(Self::R8),
95            '9' => Some(Self::R9),
96            't' | 'T' => Some(Self::RT),
97            'j' | 'J' => Some(Self::RJ),
98            'q' | 'Q' => Some(Self::RQ),
99            'k' | 'K' => Some(Self::RK),
100            'a' | 'A' => Some(Self::RA),
101            _ => None,
102        }
103    }
104
105    #[inline]
106    pub const fn to_char(self) -> char {
107        Self::CHARS[self as usize]
108    }
109
110    #[inline]
111    pub const fn all<const SD: bool>() -> &'static [Self] {
112        const {
113            if SD {
114                &Self::ARR_ALL_SD
115            } else {
116                &Self::ARR_ALL
117            }
118        }
119    }
120
121    #[inline]
122    pub(crate) const fn eq(self, other: Self) -> bool {
123        self as Idx == other as Idx
124    }
125}
126
127impl TryFrom<char> for Rank {
128    type Error = ParseError;
129
130    fn try_from(c: char) -> Result<Self, Self::Error> {
131        Self::from_char(c).ok_or_else(|| ParseError::InvalidRank(c.into()))
132    }
133}
134
135impl FromStr for Rank {
136    type Err = ParseError;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        let mut cs = s.chars().filter(|c| !c.is_whitespace());
140        if let Some(c) = cs.next()
141            && let Ok(r) = Self::try_from(c)
142            && cs.next().is_none()
143        {
144            return Ok(r);
145        }
146        Err(ParseError::InvalidRank(s.into()))
147    }
148}
149
150#[cfg(feature = "python")]
151#[pymethods]
152impl Rank {
153    #[staticmethod]
154    fn from_str(s: &str) -> PyResult<Self> {
155        Ok(s.parse::<Self>()?)
156    }
157}
158
159#[cfg(any(test, feature = "quickcheck"))]
160impl quickcheck::Arbitrary for Rank {
161    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
162        *g.choose(&Self::ARR_ALL).unwrap()
163    }
164}
165
166#[cfg(test)]
167#[cfg_attr(coverage_nightly, coverage(off))]
168mod tests {
169    use super::*;
170
171    #[quickcheck]
172    fn test_all(rank: Rank) {
173        if rank >= Rank::R6 {
174            assert!(Rank::all::<true>().contains(&rank));
175        }
176
177        assert!(Rank::all::<false>().contains(&rank));
178    }
179
180    #[test]
181    fn test_as_int() {
182        assert_eq!(Rank::R2 as Idx, 0);
183        assert_eq!(Rank::R3 as Idx, 1);
184        assert_eq!(Rank::R4 as Idx, 2);
185        assert_eq!(Rank::R5 as Idx, 3);
186        assert_eq!(Rank::R6 as Idx, 4);
187        assert_eq!(Rank::R7 as Idx, 5);
188        assert_eq!(Rank::R8 as Idx, 6);
189        assert_eq!(Rank::R9 as Idx, 7);
190        assert_eq!(Rank::RT as Idx, 8);
191        assert_eq!(Rank::RJ as Idx, 9);
192        assert_eq!(Rank::RQ as Idx, 10);
193        assert_eq!(Rank::RK as Idx, 11);
194        assert_eq!(Rank::RA as Idx, 12);
195    }
196
197    #[test]
198    fn test_from_char() {
199        assert_eq!('2'.try_into(), Ok(Rank::R2));
200        assert_eq!('3'.try_into(), Ok(Rank::R3));
201        assert_eq!('4'.try_into(), Ok(Rank::R4));
202        assert_eq!('5'.try_into(), Ok(Rank::R5));
203        assert_eq!('6'.try_into(), Ok(Rank::R6));
204        assert_eq!('7'.try_into(), Ok(Rank::R7));
205        assert_eq!('8'.try_into(), Ok(Rank::R8));
206        assert_eq!('9'.try_into(), Ok(Rank::R9));
207
208        assert_eq!('T'.try_into(), Ok(Rank::RT));
209        assert_eq!('J'.try_into(), Ok(Rank::RJ));
210        assert_eq!('Q'.try_into(), Ok(Rank::RQ));
211        assert_eq!('K'.try_into(), Ok(Rank::RK));
212        assert_eq!('A'.try_into(), Ok(Rank::RA));
213
214        assert_eq!('t'.try_into(), Ok(Rank::RT));
215        assert_eq!('j'.try_into(), Ok(Rank::RJ));
216        assert_eq!('q'.try_into(), Ok(Rank::RQ));
217        assert_eq!('k'.try_into(), Ok(Rank::RK));
218        assert_eq!('a'.try_into(), Ok(Rank::RA));
219
220        assert_eq!(
221            Rank::try_from('?'),
222            Err(ParseError::InvalidRank("?".into())),
223        );
224    }
225
226    #[test]
227    fn test_from_str() {
228        assert_eq!(" 2 ".parse(), Ok(Rank::R2));
229        assert_eq!(
230            "23".parse::<Rank>(),
231            Err(ParseError::InvalidRank("23".into())),
232        );
233        assert!("".parse::<Rank>().is_err());
234        assert!("?".parse::<Rank>().is_err());
235    }
236
237    #[test]
238    fn test_to_string() {
239        assert_eq!(
240            Rank::ARR_ALL
241                .iter()
242                .map(Rank::to_string)
243                .collect::<String>(),
244            "23456789TJQKA",
245        );
246    }
247
248    #[test]
249    fn test_to_char() {
250        assert_eq!(
251            Rank::ARR_ALL.map(Rank::to_char).iter().collect::<String>(),
252            "23456789TJQKA",
253        );
254    }
255}