Skip to main content

sashite_sin/
letter.rs

1//! Player-style abbreviation: a single ASCII letter, side-agnostic.
2
3use crate::error::ParseError;
4use crate::side::Side;
5
6/// The single-letter abbreviation of a player style.
7///
8/// A `Letter` is the *identity* part of a SIN token, independent of side. Per
9/// the specification the abbreviation is case-insensitive (`C` and `c` denote
10/// the same style), so a `Letter` is always stored uppercase; the case of the
11/// original token is carried separately by [`Side`].
12///
13/// # Invariant
14///
15/// The wrapped byte is always an uppercase ASCII letter (`b'A'..=b'Z'`). Every
16/// constructor enforces this, so the invariant cannot be violated from outside
17/// the crate.
18///
19/// Ordering is alphabetical (`A < B < … < Z`).
20#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct Letter(u8);
22
23impl Letter {
24    /// Every abbreviation, in alphabetical order (`A` through `Z`).
25    pub const ALL: [Self; 26] = [
26        Self(b'A'),
27        Self(b'B'),
28        Self(b'C'),
29        Self(b'D'),
30        Self(b'E'),
31        Self(b'F'),
32        Self(b'G'),
33        Self(b'H'),
34        Self(b'I'),
35        Self(b'J'),
36        Self(b'K'),
37        Self(b'L'),
38        Self(b'M'),
39        Self(b'N'),
40        Self(b'O'),
41        Self(b'P'),
42        Self(b'Q'),
43        Self(b'R'),
44        Self(b'S'),
45        Self(b'T'),
46        Self(b'U'),
47        Self(b'V'),
48        Self(b'W'),
49        Self(b'X'),
50        Self(b'Y'),
51        Self(b'Z'),
52    ];
53
54    /// Decodes a raw ASCII byte into a [`Letter`] and the [`Side`] its case
55    /// implies.
56    ///
57    /// Returns `None` for any byte that is not an ASCII letter. This is the
58    /// lossless decoder used by the token parser.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use sashite_sin::{Letter, Side};
64    ///
65    /// let (letter, side) = Letter::from_ascii(b'c').unwrap();
66    /// assert_eq!(letter.as_char(), 'C');
67    /// assert_eq!(side, Side::Second);
68    ///
69    /// assert!(Letter::from_ascii(b'1').is_none());
70    /// ```
71    #[must_use]
72    pub const fn from_ascii(byte: u8) -> Option<(Self, Side)> {
73        match byte {
74            b'A'..=b'Z' => Some((Self(byte), Side::First)),
75            b'a'..=b'z' => Some((Self(byte - 32), Side::Second)),
76            _ => None,
77        }
78    }
79
80    /// Builds a [`Letter`] from a `char`, folding case.
81    ///
82    /// Both `'C'` and `'c'` yield the same `Letter`; the case (which encodes
83    /// side) is not retained.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`ParseError::InvalidLetter`] if `c` is not an ASCII letter.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use sashite_sin::Letter;
93    ///
94    /// assert_eq!(Letter::try_from_char('j').unwrap().as_char(), 'J');
95    /// assert!(Letter::try_from_char('+').is_err());
96    /// ```
97    #[allow(clippy::cast_possible_truncation)] // guarded: `c` is ASCII here
98    pub const fn try_from_char(c: char) -> Result<Self, ParseError> {
99        match c {
100            'A'..='Z' => Ok(Self(c as u8)),
101            'a'..='z' => Ok(Self(c as u8 - 32)),
102            _ => Err(ParseError::InvalidLetter),
103        }
104    }
105
106    /// Returns the abbreviation as an uppercase `char`.
107    #[must_use]
108    pub const fn as_char(self) -> char {
109        self.0 as char
110    }
111
112    /// Returns the abbreviation as its raw uppercase ASCII byte.
113    #[must_use]
114    pub const fn as_ascii(self) -> u8 {
115        self.0
116    }
117
118    /// Returns the ASCII byte as it appears in a token for the given side:
119    /// uppercase for [`Side::First`], lowercase for [`Side::Second`].
120    #[must_use]
121    pub(crate) const fn to_ascii(self, side: Side) -> u8 {
122        match side {
123            Side::First => self.0,
124            Side::Second => self.0 + 32,
125        }
126    }
127}
128
129impl TryFrom<char> for Letter {
130    type Error = ParseError;
131
132    fn try_from(c: char) -> Result<Self, Self::Error> {
133        Self::try_from_char(c)
134    }
135}
136
137impl core::fmt::Debug for Letter {
138    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139        write!(f, "Letter({:?})", self.as_char())
140    }
141}