open_pql/base/
suit4.rs

1use super::{
2    BitAnd, BitOr, Card64, N_SUITS, PQLCardCount, SUIT_NAMES, Suit, fmt,
3};
4
5#[cfg(any(test, feature = "benchmark"))]
6#[macro_export]
7macro_rules! s4 {
8    ($s:expr) => {
9        $crate::Suit4::from(
10            $s.chars()
11                .filter(|c| !c.is_whitespace())
12                .map(|c| $crate::Suit::try_from(c).unwrap())
13                .collect::<Vec<_>>()
14                .as_ref(),
15        )
16    };
17}
18
19/// Suit Masks
20///
21/// A compact bit-set representation for storing multiple suits using a single u8.
22/// Each bit represents whether a specific suit is present in the set.
23/// Supports efficient set operations like union, intersection, and membership testing.
24///
25/// # Memory Layout:
26/// ```text
27/// [8, 0]:   xxxxCDHS // x: unused
28/// ```
29///
30/// # Examples
31///
32/// ```
33/// use open_pql::{Suit::*, Suit4};
34///
35/// let mut suits = Suit4::default();
36/// suits.set(S);
37/// suits.set(H);
38///
39/// assert!(suits.contains_suit(S));
40/// assert_eq!(suits.count(), 2);
41/// ```
42#[derive(Copy, Clone, PartialEq, Eq, BitAnd, BitOr, Default)]
43pub struct Suit4(u8);
44
45impl Suit4 {
46    /// Constructs [Suit4] from u8 representation [xxxxxxSS]
47    #[must_use]
48    #[inline]
49    pub(crate) const fn from_u8(v: u8) -> Self {
50        Self(v)
51    }
52
53    /// Returns the inner [u8]
54    #[allow(unused)]
55    #[must_use]
56    #[inline]
57    pub(crate) const fn to_u8(self) -> u8 {
58        self.0
59    }
60
61    #[inline]
62    const fn from_suit(s: Suit) -> Self {
63        Self(1 << s as u8)
64    }
65
66    /// Checks whether all suit masks are unset
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use open_pql::{Suit, Suit4};
72    ///
73    /// let empty: Suit4 = Suit4::default();
74    /// let not_empty: Suit4 = Suit4::from(Suit::D);
75    ///
76    /// assert!(empty.is_empty());
77    /// assert!(!not_empty.is_empty());
78    /// ```
79    pub const fn is_empty(self) -> bool {
80        self.0 == 0
81    }
82
83    /// Mark a [Suit]
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use open_pql::{Suit, Suit4};
89    ///
90    /// let mut suits: Suit4 = Suit4::default();
91    /// suits.set(Suit::D);
92    ///
93    /// assert_eq!(suits, Suit4::from(Suit::D));
94    /// ```
95    #[inline]
96    pub const fn set(&mut self, s: Suit) {
97        self.0 |= 1 << s as u8;
98    }
99
100    /// Unmark a [Suit]
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use open_pql::{Suit, Suit4};
106    ///
107    /// let mut suits: Suit4 = Suit4::from(Suit::D);
108    /// suits.unset(Suit::D);
109    ///
110    /// assert_eq!(suits, Suit4::default());
111    /// ```
112    #[inline]
113    pub const fn unset(&mut self, s: Suit) {
114        self.0 &= !(1 << s as u8);
115    }
116
117    /// Checks whether a [Suit] is marked
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use open_pql::{Suit, Suit4};
123    ///
124    /// let mut suits: Suit4 = Suit4::from(Suit::D);
125    ///
126    /// assert!(suits.contains_suit(Suit::D));
127    /// assert!(!suits.contains_suit(Suit::H));
128    /// ```
129    #[must_use]
130    #[inline]
131    pub const fn contains_suit(self, s: Suit) -> bool {
132        let v = 1u8 << (s as u8);
133        v == v & self.0
134    }
135
136    /// Returns the number of marked suits
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use open_pql::{Suit, Suit4};
142    ///
143    /// let mut suits: Suit4 = Suit4::from(Suit::D);
144    ///
145    /// assert_eq!(suits.count(), 1);
146    /// ```
147    #[must_use]
148    #[inline]
149    pub const fn count(&self) -> PQLCardCount {
150        self.0.count_ones().to_le_bytes()[0]
151    }
152}
153
154impl From<Suit> for Suit4 {
155    fn from(s: Suit) -> Self {
156        Self::from_suit(s)
157    }
158}
159
160impl From<&[Suit]> for Suit4 {
161    fn from(ss: &[Suit]) -> Self {
162        let mut res = Self::default();
163
164        for s in ss {
165            res.set(*s);
166        }
167
168        res
169    }
170}
171
172pub fn u8_to_suit_str(v: u8) -> String {
173    let to_c = |i: u8| {
174        if v & 1 << i == 0 {
175            '\0'
176        } else {
177            SUIT_NAMES[i as usize]
178        }
179    };
180
181    (0..N_SUITS)
182        .map(to_c)
183        .filter(|c| c.is_alphanumeric())
184        .collect::<String>()
185}
186
187impl fmt::Debug for Suit4 {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        f.debug_tuple("Suit4")
190            .field(&format_args!("{}", u8_to_suit_str(self.0)))
191            .finish()
192    }
193}
194
195impl From<Card64> for Suit4 {
196    fn from(c: Card64) -> Self {
197        // TODO: refactor
198
199        let [s, h, d, c] = Suit::ARR_ALL.map(|s| c.count_by_suit(s) > 0);
200
201        Self(
202            u8::from(s)
203                | u8::from(h) << 1
204                | u8::from(d) << 2
205                | u8::from(c) << 3,
206        )
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::*;
214
215    #[test]
216    fn test_empty() {
217        assert_eq!(Suit4::default(), Suit4(0));
218        assert_eq!(Suit4::default(), Suit4(0));
219        assert!(Suit4::default().is_empty());
220        assert!(Suit4::default().is_empty());
221        assert!(!Suit4(1).is_empty());
222    }
223
224    #[quickcheck]
225    fn test_set_and_contains(s: Suit) {
226        let mut suits = Suit4::default();
227
228        suits.set(s);
229
230        assert!(!suits.is_empty());
231        assert!(suits.contains_suit(s));
232
233        suits.unset(s);
234
235        assert!(suits.is_empty());
236        assert!(!suits.contains_suit(s));
237    }
238
239    #[quickcheck]
240    fn test_u8(i: u8) -> TestResult {
241        if i > 0b1111 {
242            return TestResult::discard();
243        }
244
245        assert_eq!(Suit4(i), Suit4::from_u8(i));
246        assert_eq!(i, Suit4(i).to_u8());
247
248        TestResult::passed()
249    }
250
251    #[quickcheck]
252    fn test_from_suit(s1: Suit, s2: Suit) {
253        let suits = Suit4::from(s1);
254
255        assert!(suits.contains_suit(s1));
256
257        let suits = Suit4::from([s1, s2].as_ref());
258
259        assert!(suits.contains_suit(s1));
260        assert!(suits.contains_suit(s2));
261    }
262
263    #[quickcheck]
264    fn test_bit_and(s1: Suit, s2: Suit) {
265        let a = Suit4::from(s1);
266        let b = Suit4::from(s2);
267
268        assert_eq!((a & b).is_empty(), s1 != s2);
269    }
270
271    #[quickcheck]
272    fn test_bit_or(s1: Suit, s2: Suit) {
273        let a = Suit4::from(s1);
274        let b = Suit4::from(s2);
275
276        assert!((a | b).contains_suit(s1));
277        assert!((a | b).contains_suit(s2));
278    }
279
280    #[quickcheck]
281    fn test_count(s1: Suit, s2: Suit) {
282        let suits = Suit4::from([s1, s2].as_ref());
283
284        let count = if s1 == s2 { 1 } else { 2 };
285
286        assert_eq!(count, suits.count());
287    }
288
289    #[quickcheck]
290    fn test_from_card64(cards: Vec<Card>) -> TestResult {
291        let mut suits = Suit4::default();
292
293        for i in 0..cards.len() {
294            suits.set(cards[i].suit);
295
296            let c64: Card64 = cards[0..=i].into();
297
298            assert_eq!(Suit4::from(c64), suits);
299        }
300
301        TestResult::passed()
302    }
303
304    #[test]
305    fn test_debug() {
306        let s = format!("{:?}", s4!("s") | s4!("d"));
307        assert_eq!(s, "Suit4(sd)");
308    }
309
310    #[test]
311    #[cfg(debug_assertions)]
312    fn test_suit_from_u8_debug_assert() {
313        use std::panic;
314
315        for i in 0..N_SUITS {
316            let result = panic::catch_unwind(|| {
317                Suit::from_u8(i);
318            });
319            assert!(result.is_ok(), "Suit::from_u8({i}) should not panic");
320        }
321
322        let result = panic::catch_unwind(|| {
323            Suit::from_u8(N_SUITS);
324        });
325        assert!(result.is_err(), "Suit::from_u8({N_SUITS}) should panic");
326
327        let result = panic::catch_unwind(|| {
328            Suit::from_u8(100);
329        });
330        assert!(result.is_err(), "Suit::from_u8(100) should panic");
331    }
332}