playin-cards 0.2.0

♠ Library for French-suited playing cards
Documentation
use std::str::FromStr;
use std::{error, fmt};

use crate::{Card, Rank, Suit};

/// Error Type for parsing failures
#[non_exhaustive]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ParseError {
    InvalidChar,
    TooShort,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::TooShort => write!(f, "Input string too short"),
            Self::InvalidChar => write!(f, "Invalid character encountered"),
        }
    }
}

impl error::Error for ParseError {}

type Result<T> = std::result::Result<T, ParseError>;

impl TryFrom<char> for Rank {
    type Error = ParseError;
    /// Simple conversion from `char` to `Rank`. Valid input values are `chars`
    /// in `A23456789TJQK`. Error is returned for all other inputs.
    fn try_from(c: char) -> Result<Self> {
        match c {
            '2' => Ok(Self::Two),
            '3' => Ok(Self::Three),
            '4' => Ok(Self::Four),
            '5' => Ok(Self::Five),
            '6' => Ok(Self::Six),
            '7' => Ok(Self::Seven),
            '8' => Ok(Self::Eight),
            '9' => Ok(Self::Nine),
            'T' => Ok(Self::Ten),
            'J' => Ok(Self::Jack),
            'Q' => Ok(Self::Queen),
            'K' => Ok(Self::King),
            'A' => Ok(Self::Ace),
            _ => Err(ParseError::InvalidChar),
        }
    }
}

impl TryFrom<char> for Suit {
    type Error = ParseError;
    /// Convert a `char` to a `Suit` according to the following mapping:
    /// ```text
    /// char    Suit
    /// cC♣  => Clubs
    /// sS♠  => Spades
    /// hH♥  => Hearts
    /// dD♦  => Diamonds
    /// ```
    /// If `c` is not in the left side, return error variant
    ///
    fn try_from(c: char) -> Result<Self> {
        match c {
            'C' | 'c' | '' => Ok(Self::Clubs),
            'S' | 's' | '' => Ok(Self::Spades),
            'H' | 'h' | '' => Ok(Self::Hearts),
            'D' | 'd' | '' => Ok(Self::Diamonds),
            _ => Err(ParseError::InvalidChar),
        }
    }
}

impl TryFrom<char> for Card {
    type Error = ParseError;
    fn try_from(c: char) -> Result<Self> {
        let suit = match c as u32 & 0x000F0 {
            0x000A0 => Suit::Spades,
            0x000B0 => Suit::Hearts,
            0x000C0 => Suit::Diamonds,
            0x000D0 => Suit::Clubs,
            _ => return Err(ParseError::InvalidChar),
        };
        let rank = match c as u32 & 0x0000F {
            0x00001 => Rank::Ace,
            0x00002 => Rank::Two,
            0x00003 => Rank::Three,
            0x00004 => Rank::Four,
            0x00005 => Rank::Five,
            0x00006 => Rank::Six,
            0x00007 => Rank::Seven,
            0x00008 => Rank::Eight,
            0x00009 => Rank::Nine,
            0x0000A => Rank::Ten,
            0x0000B => Rank::Jack,
            0x0000D => Rank::Queen,
            0x0000E => Rank::King,
            _ => return Err(ParseError::InvalidChar),
        };
        Ok(Card::Regular { rank, suit })
    }
}

impl FromStr for Card {
    type Err = ParseError;
    /// Parse a 2-char ASCII `str` to a `Card`
    fn from_str(s: &str) -> Result<Self> {
        if s.len() < 2 {
            return Err(ParseError::TooShort);
        }
        let mut chars = s.chars();
        let rank = Rank::try_from(chars.next().unwrap())?;
        let suit = Suit::try_from(chars.next().unwrap())?;
        Ok(Self::Regular { rank, suit })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::format::tests::{ASCII_REPRS, MIXED_REPRS, UNICODE_REPRS};
    use crate::test::FULL_DECK_CARDS;

    #[test]
    fn invlaid_char() {
        assert_eq!("L4".parse::<Card>(), Err(ParseError::InvalidChar));
    }
    #[test]
    fn tooshort() {
        assert_eq!("T".parse::<Card>(), Err(ParseError::TooShort));
        assert_eq!("J".parse::<Card>(), Err(ParseError::TooShort));
        assert_eq!("L".parse::<Card>(), Err(ParseError::TooShort));
        assert_eq!("5".parse::<Card>(), Err(ParseError::TooShort));
        assert_eq!("C".parse::<Card>(), Err(ParseError::TooShort));
    }
    #[test]
    fn cards_mixed() {
        for (s, card) in MIXED_REPRS.iter().zip(FULL_DECK_CARDS) {
            assert_eq!(s.parse(), Ok(card))
        }
    }
    #[test]
    fn cards_ascii() {
        for (s, card) in ASCII_REPRS.iter().zip(FULL_DECK_CARDS) {
            assert_eq!(s.parse(), Ok(card));
        }
    }
    #[test]
    fn cards_unicode() {
        for (c, card) in UNICODE_REPRS.chars().zip(FULL_DECK_CARDS) {
            assert_eq!(Card::try_from(c), Ok(card));
        }
    }
}