Skip to main content

dds_bridge/
hand.rs

1//! Card primitives: ranks, cards, holdings, and hands.
2//!
3//! [`Rank`] is a non-zero byte in `2..=14` (J/Q/K/A as 11/12/13/14).
4//! [`Card`] pairs a [`Rank`] with a [`Suit`] and packs into a
5//! single byte.  [`Holding`] is a 13-bit bitset of ranks within one suit,
6//! and [`Hand`] is four holdings packed into a `u64`.  Set operations on
7//! holdings and hands are exposed through the standard bitwise operators.
8//!
9//! Iteration yields cards in descending suit order (spades first) and
10//! descending rank order within each suit, matching the conventional
11//! display order.
12//!
13//! # Panic policy
14//!
15//! [`Rank::new`] panics when the input is outside `2..=14` and has
16//! [`Rank::try_new`] for fallible construction.  In const contexts the panic
17//! becomes a compile-time error.
18
19use crate::Suit;
20use core::fmt::{self, Write as _};
21use core::iter::FusedIterator;
22use core::num::NonZero;
23use core::ops;
24use core::str::FromStr;
25use thiserror::Error;
26
27/// Error indicating an invalid rank
28///
29/// The rank of a card must be in `2..=14`, where J, Q, K, A are denoted as 11,
30/// 12, 13, 14 respectively.
31#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32#[error("{0} is not a valid rank (2..=14)")]
33pub struct InvalidRank(u8);
34
35/// The rank of a card, from 2 to 14, where J, Q, K, A are internally denoted as
36/// 11, 12, 13, 14 respectively.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "serde", serde(transparent))]
40#[repr(transparent)]
41pub struct Rank(NonZero<u8>);
42
43impl Rank {
44    /// Ace
45    pub const A: Self = Self(NonZero::new(14).unwrap());
46
47    /// King
48    pub const K: Self = Self(NonZero::new(13).unwrap());
49
50    /// Queen
51    pub const Q: Self = Self(NonZero::new(12).unwrap());
52
53    /// Jack
54    pub const J: Self = Self(NonZero::new(11).unwrap());
55
56    /// Ten
57    pub const T: Self = Self(NonZero::new(10).unwrap());
58
59    /// Create a rank from a number
60    ///
61    /// # Panics
62    ///
63    /// When the rank is not in `2..=14`.  In const contexts, this is a
64    /// compile-time error.
65    #[must_use]
66    #[inline]
67    pub const fn new(rank: u8) -> Self {
68        match Self::try_new(rank) {
69            Ok(r) => r,
70            Err(_) => panic!("rank must be in 2..=14"),
71        }
72    }
73
74    /// Try to create a rank from a number
75    ///
76    /// # Errors
77    ///
78    /// When the rank is not in `2..=14`.
79    #[inline]
80    pub const fn try_new(rank: u8) -> Result<Self, InvalidRank> {
81        match NonZero::new(rank) {
82            Some(nonzero) if rank >= 2 && rank <= 14 => Ok(Self(nonzero)),
83            _ => Err(InvalidRank(rank)),
84        }
85    }
86
87    /// Get the stored rank as [`u8`]
88    #[must_use]
89    #[inline]
90    pub const fn get(self) -> u8 {
91        self.0.get()
92    }
93
94    /// Display character for this rank
95    #[must_use]
96    #[inline]
97    pub const fn letter(self) -> char {
98        b"23456789TJQKA"[self.get() as usize - 2] as char
99    }
100}
101
102impl fmt::Display for Rank {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_char(self.letter())
105    }
106}
107
108/// Error returned when parsing a [`Rank`] fails
109#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
110#[error("Invalid rank: expected 2-10, T, J, Q, K, A")]
111pub struct ParseRankError;
112
113impl FromStr for Rank {
114    type Err = ParseRankError;
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        match s.to_ascii_uppercase().as_str() {
117            "A" => Ok(Self::A),
118            "K" => Ok(Self::K),
119            "Q" => Ok(Self::Q),
120            "J" => Ok(Self::J),
121            "T" | "10" => Ok(Self::T),
122            "9" => Ok(Self::new(9)),
123            "8" => Ok(Self::new(8)),
124            "7" => Ok(Self::new(7)),
125            "6" => Ok(Self::new(6)),
126            "5" => Ok(Self::new(5)),
127            "4" => Ok(Self::new(4)),
128            "3" => Ok(Self::new(3)),
129            "2" => Ok(Self::new(2)),
130            _ => Err(ParseRankError),
131        }
132    }
133}
134
135/// A playing card
136///
137/// Internally packed as `(rank << 2) | suit` in a single byte.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
139#[cfg_attr(
140    feature = "serde",
141    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
142)]
143pub struct Card {
144    /// The suit of the card
145    pub suit: Suit,
146    /// The rank of the card
147    pub rank: Rank,
148}
149
150impl fmt::Display for Card {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "{}{}", self.suit, self.rank)
153    }
154}
155
156/// Error returned when parsing a [`Card`] fails
157#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum ParseCardError {
160    /// Invalid suit in card
161    #[error("Invalid suit in card: expected one of C, D, H, S, ♣, ♦, ♥, ♠, ♧, ♢, ♡, ♤")]
162    Suit,
163    /// Invalid rank in card
164    #[error("Invalid rank in card: expected 2-10, T, J, Q, K, A")]
165    Rank,
166}
167
168impl FromStr for Card {
169    type Err = ParseCardError;
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        let rank_len = if s.ends_with("10") {
172            2
173        } else {
174            s.chars().next_back().map_or(0, char::len_utf8)
175        };
176        let border = s.len().saturating_sub(rank_len);
177        let (suit, rank) = s.split_at(border);
178        let suit: Suit = suit.parse().map_err(|_| ParseCardError::Suit)?;
179        let rank: Rank = rank.parse().map_err(|_| ParseCardError::Rank)?;
180        Ok(Self { suit, rank })
181    }
182}
183
184/// A set of cards of the same suit
185#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
186#[cfg_attr(
187    feature = "serde",
188    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
189)]
190#[repr(transparent)]
191pub struct Holding(u16);
192
193/// Iterator over the ranks in a [`Holding`], yielding [`Rank`]s in descending order
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct HoldingIter {
196    rest: u16,
197    cursor: u8,
198}
199
200impl Iterator for HoldingIter {
201    type Item = Rank;
202
203    fn next(&mut self) -> Option<Self::Item> {
204        if self.rest == 0 {
205            return None;
206        }
207
208        // For non-zero u16, leading_zeros is in 0..=15, fitting in u8.
209        // Bit 15 is never set in a valid Holding (max rank is 14), so pos <= 14.
210        #[allow(clippy::cast_possible_truncation)]
211        let pos = 15 - self.rest.leading_zeros() as u8;
212        self.rest &= !(1u16 << pos);
213        let rank = self.cursor + pos;
214
215        // SAFETY: rank is in 2..=14 by construction from a valid Holding
216        Some(Rank(unsafe { core::num::NonZero::new_unchecked(rank) }))
217    }
218
219    fn size_hint(&self) -> (usize, Option<usize>) {
220        let count = self.rest.count_ones() as usize;
221        (count, Some(count))
222    }
223
224    fn count(self) -> usize {
225        self.rest.count_ones() as usize
226    }
227}
228
229impl DoubleEndedIterator for HoldingIter {
230    fn next_back(&mut self) -> Option<Self::Item> {
231        if self.rest == 0 {
232            return None;
233        }
234
235        // 1. Trailing zeros are in the range of 0..=15, which fits in `u8`
236        // 2. Trailing zeros cannot be 15 since the bitset is from a `Holding`
237        #[allow(clippy::cast_possible_truncation)]
238        let step = self.rest.trailing_zeros() as u8 + 1;
239        self.rest >>= step;
240        self.cursor += step;
241
242        // SAFETY: cursor is in 2..=14 by construction from a valid Holding
243        Some(Rank(unsafe {
244            core::num::NonZero::new_unchecked(self.cursor - 1)
245        }))
246    }
247}
248
249impl ExactSizeIterator for HoldingIter {
250    fn len(&self) -> usize {
251        self.rest.count_ones() as usize
252    }
253}
254
255impl FusedIterator for HoldingIter {}
256
257impl Holding {
258    /// The empty set
259    pub const EMPTY: Self = Self(0);
260
261    /// The set containing all possible ranks (2..=14)
262    pub const ALL: Self = Self(0x7FFC);
263
264    /// The number of cards in the holding
265    #[must_use]
266    #[inline]
267    pub const fn len(self) -> usize {
268        self.0.count_ones() as usize
269    }
270
271    /// Whether the holding is empty
272    #[must_use]
273    #[inline]
274    pub const fn is_empty(self) -> bool {
275        self.0 == 0
276    }
277
278    /// Whether the holding contains a rank
279    #[must_use]
280    #[inline]
281    pub const fn contains(self, rank: Rank) -> bool {
282        self.0 & 1 << rank.get() != 0
283    }
284
285    /// Insert a rank into the holding, returning whether it was newly inserted
286    #[inline]
287    pub const fn insert(&mut self, rank: Rank) -> bool {
288        let insertion = 1 << rank.get() & Self::ALL.0;
289        let inserted = insertion & !self.0 != 0;
290        self.0 |= insertion;
291        inserted
292    }
293
294    /// Remove a rank from the holding, returning whether it was present
295    #[inline]
296    pub const fn remove(&mut self, rank: Rank) -> bool {
297        let removed = self.contains(rank);
298        self.0 &= !(1 << rank.get());
299        removed
300    }
301
302    /// Toggle a rank in the holding, returning whether it is now present
303    #[inline]
304    pub const fn toggle(&mut self, rank: Rank) -> bool {
305        self.0 ^= 1 << rank.get() & Self::ALL.0;
306        self.contains(rank)
307    }
308
309    /// Conditionally insert/remove a rank from the holding
310    #[inline]
311    pub const fn set(&mut self, rank: Rank, condition: bool) {
312        let flag = 1 << rank.get();
313        let mask = (condition as u16).wrapping_neg();
314        self.0 = (self.0 & !flag) | (mask & flag);
315    }
316
317    /// Iterate over the ranks in the holding
318    #[inline]
319    #[must_use]
320    pub const fn iter(self) -> HoldingIter {
321        HoldingIter {
322            rest: self.0,
323            cursor: 0,
324        }
325    }
326
327    /// As a bitset of ranks
328    #[must_use]
329    #[inline]
330    pub const fn to_bits(self) -> u16 {
331        self.0
332    }
333
334    /// Create a holding from a bitset of ranks, retaining invalid ranks
335    #[must_use]
336    #[inline]
337    pub const fn from_bits_retain(bits: u16) -> Self {
338        Self(bits)
339    }
340
341    /// Whether the holding contains an invalid rank
342    #[must_use]
343    #[inline]
344    pub const fn contains_unknown_bits(self) -> bool {
345        self.0 & Self::ALL.0 != self.0
346    }
347
348    /// Create a holding from a bitset of ranks, checking for invalid ranks
349    #[must_use]
350    #[inline]
351    pub const fn from_bits(bits: u16) -> Option<Self> {
352        if bits & Self::ALL.0 == bits {
353            Some(Self(bits))
354        } else {
355            None
356        }
357    }
358
359    /// Create a holding from a bitset of ranks, removing invalid ranks
360    #[must_use]
361    #[inline]
362    pub const fn from_bits_truncate(bits: u16) -> Self {
363        Self(bits & Self::ALL.0)
364    }
365
366    /// Create a holding from a rank
367    #[must_use]
368    #[inline]
369    pub const fn from_rank(rank: Rank) -> Self {
370        Self(1 << rank.get())
371    }
372}
373
374impl IntoIterator for Holding {
375    type Item = Rank;
376    type IntoIter = HoldingIter;
377
378    fn into_iter(self) -> HoldingIter {
379        self.iter()
380    }
381}
382
383impl ops::BitAnd for Holding {
384    type Output = Self;
385
386    #[inline]
387    fn bitand(self, rhs: Self) -> Self {
388        Self(self.0 & rhs.0)
389    }
390}
391
392impl ops::BitOr for Holding {
393    type Output = Self;
394
395    #[inline]
396    fn bitor(self, rhs: Self) -> Self {
397        Self(self.0 | rhs.0)
398    }
399}
400
401impl ops::BitXor for Holding {
402    type Output = Self;
403
404    #[inline]
405    fn bitxor(self, rhs: Self) -> Self {
406        Self(self.0 ^ rhs.0)
407    }
408}
409
410impl ops::Not for Holding {
411    type Output = Self;
412
413    #[inline]
414    fn not(self) -> Self {
415        Self::from_bits_truncate(!self.0)
416    }
417}
418
419impl ops::Sub for Holding {
420    type Output = Self;
421
422    #[inline]
423    fn sub(self, rhs: Self) -> Self {
424        Self(self.0 & !rhs.0)
425    }
426}
427
428impl ops::BitAndAssign for Holding {
429    #[inline]
430    fn bitand_assign(&mut self, rhs: Self) {
431        *self = *self & rhs;
432    }
433}
434
435impl ops::BitOrAssign for Holding {
436    #[inline]
437    fn bitor_assign(&mut self, rhs: Self) {
438        *self = *self | rhs;
439    }
440}
441
442impl ops::BitXorAssign for Holding {
443    #[inline]
444    fn bitxor_assign(&mut self, rhs: Self) {
445        *self = *self ^ rhs;
446    }
447}
448
449impl ops::SubAssign for Holding {
450    #[inline]
451    fn sub_assign(&mut self, rhs: Self) {
452        *self = *self - rhs;
453    }
454}
455
456/// Show cards in descending order
457///
458/// 1. The ten is shown as `T` for PBN compatibility.
459/// 2. This implementation ignores formatting flags for simplicity and speed.
460///    If you want to pad or align the output, use [`fmt::Formatter::pad`].
461impl fmt::Display for Holding {
462    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
463        for rank in (2u8..15).rev() {
464            if self.0 & 1 << rank != 0 {
465                f.write_char(b"23456789TJQKA"[rank as usize - 2] as char)?;
466            }
467        }
468        Ok(())
469    }
470}
471
472/// An error which can be returned when parsing a [`Holding`]
473#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
474#[non_exhaustive]
475pub enum ParseHoldingError {
476    /// Ranks are not all valid or in descending order
477    #[error("Ranks are not all valid or in descending order")]
478    InvalidRanks,
479
480    /// The same rank appears more than once
481    #[error("The same rank appears more than once")]
482    RepeatedRank,
483
484    /// A suit contains more than 13 cards
485    #[error("A suit contains more than 13 cards")]
486    TooManyCards,
487}
488
489/// An error which can be returned when parsing a [`Hand`]
490#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
491#[non_exhaustive]
492pub enum ParseHandError {
493    /// Error in a holding
494    #[error(transparent)]
495    Holding(#[from] ParseHoldingError),
496
497    /// The hand does not contain 4 suits
498    #[error("The hand does not contain 4 suits")]
499    NotFourSuits,
500}
501
502impl FromStr for Holding {
503    type Err = ParseHoldingError;
504
505    fn from_str(s: &str) -> Result<Self, Self::Err> {
506        // 13 cards + 1 extra char for "10"
507        if s.len() > 14 {
508            return Err(ParseHoldingError::TooManyCards);
509        }
510
511        let bytes = s.as_bytes();
512        let mut i = 0;
513        let mut prev_rank: u8 = 15;
514        let mut explicit = Self::EMPTY;
515
516        while i < bytes.len() {
517            let c = bytes[i].to_ascii_uppercase();
518            let rank: u8 = match c {
519                b'A' => 14,
520                b'K' => 13,
521                b'Q' => 12,
522                b'J' => 11,
523                b'T' => 10,
524                b'1' => {
525                    if bytes.get(i + 1) != Some(&b'0') {
526                        return Err(ParseHoldingError::InvalidRanks);
527                    }
528                    i += 1;
529                    10
530                }
531                b'2'..=b'9' => c - b'0',
532                b'X' => break,
533                _ => return Err(ParseHoldingError::InvalidRanks),
534            };
535
536            if rank >= prev_rank {
537                return Err(ParseHoldingError::InvalidRanks);
538            }
539            prev_rank = rank;
540
541            // SAFETY: rank is in 2..=14 by construction above
542            let r = Rank(unsafe { core::num::NonZero::new_unchecked(rank) });
543            explicit.insert(r);
544            i += 1;
545        }
546
547        let spot_count = bytes.len() - i;
548        if bytes[i..].iter().any(|&b| !b.eq_ignore_ascii_case(&b'x')) {
549            return Err(ParseHoldingError::InvalidRanks);
550        }
551        if spot_count > 13 {
552            return Err(ParseHoldingError::TooManyCards);
553        }
554
555        let spots = Self::from_bits_truncate((4u16 << spot_count) - 4);
556
557        if explicit & spots == Self::EMPTY {
558            Ok(explicit | spots)
559        } else {
560            Err(ParseHoldingError::RepeatedRank)
561        }
562    }
563}
564
565/// A hand of playing cards
566#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
567#[cfg_attr(
568    feature = "serde",
569    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
570)]
571pub struct Hand([Holding; 4]);
572
573/// Iterator over the cards in a [`Hand`], yielding [`Card`]s in descending
574/// suit order and descending rank order within each suit
575#[derive(Debug, Clone, PartialEq, Eq)]
576pub struct HandIter {
577    suits: [HoldingIter; 4],
578    fwd: u8,
579    bwd: u8,
580}
581
582impl Iterator for HandIter {
583    type Item = Card;
584
585    fn next(&mut self) -> Option<Self::Item> {
586        loop {
587            if self.fwd > 3 {
588                return None;
589            }
590            let suit = Suit::ASC[self.fwd as usize];
591            if let Some(rank) = self.suits[self.fwd as usize].next() {
592                return Some(Card { suit, rank });
593            }
594            if self.fwd == self.bwd {
595                self.fwd = 4;
596                return None;
597            }
598            self.fwd -= 1;
599        }
600    }
601
602    fn size_hint(&self) -> (usize, Option<usize>) {
603        let count = self.len();
604        (count, Some(count))
605    }
606
607    fn count(self) -> usize {
608        self.len()
609    }
610}
611
612impl DoubleEndedIterator for HandIter {
613    fn next_back(&mut self) -> Option<Self::Item> {
614        loop {
615            if self.fwd > 3 {
616                return None;
617            }
618            let suit = Suit::ASC[self.bwd as usize];
619            if let Some(rank) = self.suits[self.bwd as usize].next_back() {
620                return Some(Card { suit, rank });
621            }
622            if self.fwd == self.bwd {
623                self.fwd = 4;
624                return None;
625            }
626            self.bwd += 1;
627        }
628    }
629}
630
631impl ExactSizeIterator for HandIter {
632    fn len(&self) -> usize {
633        if self.fwd > 3 {
634            return 0;
635        }
636        (self.bwd as usize..=self.fwd as usize)
637            .map(|i| self.suits[i].len())
638            .sum()
639    }
640}
641
642impl FusedIterator for HandIter {}
643
644impl ops::Index<Suit> for Hand {
645    type Output = Holding;
646
647    #[inline]
648    fn index(&self, suit: Suit) -> &Holding {
649        &self.0[suit as usize]
650    }
651}
652
653impl ops::IndexMut<Suit> for Hand {
654    #[inline]
655    fn index_mut(&mut self, suit: Suit) -> &mut Holding {
656        &mut self.0[suit as usize]
657    }
658}
659
660impl Hand {
661    /// As a bitset of cards
662    #[must_use]
663    #[inline]
664    pub const fn to_bits(self) -> u64 {
665        unsafe { core::mem::transmute(self.0) }
666    }
667
668    /// Create a hand from a bitset of cards, retaining invalid cards
669    #[must_use]
670    #[inline]
671    pub const fn from_bits_retain(bits: u64) -> Self {
672        unsafe { core::mem::transmute(bits) }
673    }
674
675    /// Whether the hand contains an invalid card
676    #[must_use]
677    #[inline]
678    pub const fn contains_unknown_bits(self) -> bool {
679        self.to_bits() & Self::ALL.to_bits() != self.to_bits()
680    }
681
682    /// Create a hand from a bitset of cards, checking for invalid cards
683    #[must_use]
684    #[inline]
685    pub const fn from_bits(bits: u64) -> Option<Self> {
686        if bits & Self::ALL.to_bits() == bits {
687            Some(Self::from_bits_retain(bits))
688        } else {
689            None
690        }
691    }
692
693    /// Create a hand from a bitset of cards, removing invalid cards
694    #[must_use]
695    #[inline]
696    pub const fn from_bits_truncate(bits: u64) -> Self {
697        Self::from_bits_retain(bits & Self::ALL.to_bits())
698    }
699
700    /// Create a hand from four holdings in suit order (clubs, diamonds, hearts, spades)
701    #[must_use]
702    #[inline]
703    pub const fn new(clubs: Holding, diamonds: Holding, hearts: Holding, spades: Holding) -> Self {
704        Self([clubs, diamonds, hearts, spades])
705    }
706
707    /// The empty hand
708    pub const EMPTY: Self = Self([Holding::EMPTY; 4]);
709
710    /// The hand containing all 52 cards
711    pub const ALL: Self = Self([Holding::ALL; 4]);
712
713    /// The number of cards in the hand
714    #[must_use]
715    #[inline]
716    pub const fn len(self) -> usize {
717        self.to_bits().count_ones() as usize
718    }
719
720    /// Whether the hand is empty
721    #[must_use]
722    #[inline]
723    pub const fn is_empty(self) -> bool {
724        self.to_bits() == 0
725    }
726
727    /// Whether the hand contains a card
728    #[must_use]
729    #[inline]
730    pub fn contains(self, card: Card) -> bool {
731        self[card.suit].contains(card.rank)
732    }
733
734    /// Insert a card into the hand, returning whether it was newly inserted
735    #[inline]
736    pub fn insert(&mut self, card: Card) -> bool {
737        self[card.suit].insert(card.rank)
738    }
739
740    /// Remove a card from the hand, returning whether it was present
741    #[inline]
742    pub fn remove(&mut self, card: Card) -> bool {
743        self[card.suit].remove(card.rank)
744    }
745
746    /// Toggle a card in the hand, returning whether it is now present
747    #[inline]
748    pub fn toggle(&mut self, card: Card) -> bool {
749        self[card.suit].toggle(card.rank)
750    }
751
752    /// Conditionally insert/remove a card from the hand
753    #[inline]
754    pub fn set(&mut self, card: Card, condition: bool) {
755        self[card.suit].set(card.rank, condition);
756    }
757
758    /// Iterate over the cards in the hand
759    #[inline]
760    #[must_use]
761    pub const fn iter(self) -> HandIter {
762        HandIter {
763            suits: [
764                self.0[0].iter(),
765                self.0[1].iter(),
766                self.0[2].iter(),
767                self.0[3].iter(),
768            ],
769            fwd: 3,
770            bwd: 0,
771        }
772    }
773}
774
775impl IntoIterator for Hand {
776    type Item = Card;
777    type IntoIter = HandIter;
778
779    #[inline]
780    fn into_iter(self) -> HandIter {
781        self.iter()
782    }
783}
784
785impl FromIterator<Card> for Hand {
786    fn from_iter<I: IntoIterator<Item = Card>>(iter: I) -> Self {
787        iter.into_iter().fold(Self::EMPTY, |mut hand, card| {
788            hand.insert(card);
789            hand
790        })
791    }
792}
793
794/// PBN-compatible display of a hand
795///
796/// This implementation ignores formatting flags for simplicity and speed.
797impl fmt::Display for Hand {
798    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
799        if self.is_empty() {
800            return f.write_char('-');
801        }
802
803        self[Suit::Spades].fmt(f)?;
804        f.write_char('.')?;
805
806        self[Suit::Hearts].fmt(f)?;
807        f.write_char('.')?;
808
809        self[Suit::Diamonds].fmt(f)?;
810        f.write_char('.')?;
811
812        self[Suit::Clubs].fmt(f)
813    }
814}
815
816impl FromStr for Hand {
817    type Err = ParseHandError;
818
819    fn from_str(s: &str) -> Result<Self, Self::Err> {
820        // 52 cards + 4 tens + 3 dots
821        if s.len() > 52 + 4 + 3 {
822            return Err(ParseHoldingError::TooManyCards.into());
823        }
824
825        if s == "-" {
826            return Ok(Self::EMPTY);
827        }
828
829        let holdings: Result<Vec<_>, _> = s.split('.').map(Holding::from_str).rev().collect();
830
831        Ok(Self(
832            holdings?
833                .try_into()
834                .map_err(|_| ParseHandError::NotFourSuits)?,
835        ))
836    }
837}
838
839impl ops::BitAnd for Hand {
840    type Output = Self;
841
842    #[inline]
843    fn bitand(self, rhs: Self) -> Self {
844        Self::from_bits_retain(self.to_bits() & rhs.to_bits())
845    }
846}
847
848impl ops::BitOr for Hand {
849    type Output = Self;
850
851    #[inline]
852    fn bitor(self, rhs: Self) -> Self {
853        Self::from_bits_retain(self.to_bits() | rhs.to_bits())
854    }
855}
856
857impl ops::BitXor for Hand {
858    type Output = Self;
859
860    #[inline]
861    fn bitxor(self, rhs: Self) -> Self {
862        Self::from_bits_retain(self.to_bits() ^ rhs.to_bits())
863    }
864}
865
866impl ops::Not for Hand {
867    type Output = Self;
868
869    #[inline]
870    fn not(self) -> Self {
871        Self::from_bits_truncate(!self.to_bits())
872    }
873}
874
875impl ops::Sub for Hand {
876    type Output = Self;
877
878    #[inline]
879    fn sub(self, rhs: Self) -> Self {
880        Self::from_bits_retain(self.to_bits() & !rhs.to_bits())
881    }
882}
883
884impl ops::BitAndAssign for Hand {
885    #[inline]
886    fn bitand_assign(&mut self, rhs: Self) {
887        *self = *self & rhs;
888    }
889}
890
891impl ops::BitOrAssign for Hand {
892    #[inline]
893    fn bitor_assign(&mut self, rhs: Self) {
894        *self = *self | rhs;
895    }
896}
897
898impl ops::BitXorAssign for Hand {
899    #[inline]
900    fn bitxor_assign(&mut self, rhs: Self) {
901        *self = *self ^ rhs;
902    }
903}
904
905impl ops::SubAssign for Hand {
906    #[inline]
907    fn sub_assign(&mut self, rhs: Self) {
908        *self = *self - rhs;
909    }
910}