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'`). The
16/// field is private and every constructor enforces the range, so the invariant
17/// cannot be violated from outside the crate.
18///
19/// It is what makes the crate's arithmetic total. Three places shift a byte by
20/// 32 to change case, and each would panic on overflow in a debug build; the
21/// invariant keeps every one of them inside `65..=122`, far from either end of
22/// a `u8`. Nothing in the type system enforces that, so the tests sweep every
23/// public constructor to prove it.
24///
25/// Ordering is alphabetical (`A < B < … < Z`).
26#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Letter(u8);
28
29impl Letter {
30    /// Every abbreviation, in alphabetical order (`A` through `Z`).
31    pub const ALL: [Self; 26] = [
32        Self(b'A'),
33        Self(b'B'),
34        Self(b'C'),
35        Self(b'D'),
36        Self(b'E'),
37        Self(b'F'),
38        Self(b'G'),
39        Self(b'H'),
40        Self(b'I'),
41        Self(b'J'),
42        Self(b'K'),
43        Self(b'L'),
44        Self(b'M'),
45        Self(b'N'),
46        Self(b'O'),
47        Self(b'P'),
48        Self(b'Q'),
49        Self(b'R'),
50        Self(b'S'),
51        Self(b'T'),
52        Self(b'U'),
53        Self(b'V'),
54        Self(b'W'),
55        Self(b'X'),
56        Self(b'Y'),
57        Self(b'Z'),
58    ];
59
60    /// Decodes a raw ASCII byte into a [`Letter`] and the [`Side`] its case
61    /// implies.
62    ///
63    /// Returns `None` for any byte that is not an ASCII letter. This is the
64    /// lossless decoder used by the token parser: the byte it was given can
65    /// always be rebuilt from the pair it returns.
66    ///
67    /// Note this takes a *byte*. Reaching for it with `c as u8` on a `char`
68    /// silently truncates to the low eight bits and can turn a non-ASCII
69    /// character into a letter that was never there — `'Ł'` (`U+0141`) becomes
70    /// `0x41`, the byte `b'A'`. Use [`Letter::try_from_char`] for a `char`; it
71    /// matches on the character before casting and so cannot be fooled.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use sashite_sin::{Letter, Side};
77    ///
78    /// let (letter, side) = Letter::from_ascii(b'c').unwrap();
79    /// assert_eq!(letter.as_char(), 'C');
80    /// assert_eq!(side, Side::Second);
81    ///
82    /// assert!(Letter::from_ascii(b'1').is_none());
83    ///
84    /// // The `char` door is the safe one for non-ASCII input.
85    /// assert!(Letter::try_from_char('\u{0141}').is_err());
86    /// ```
87    #[must_use]
88    pub const fn from_ascii(byte: u8) -> Option<(Self, Side)> {
89        match byte {
90            b'A'..=b'Z' => Some((Self(byte), Side::First)),
91            // `byte` is at least `b'a'` (97) in this arm, so the subtraction
92            // cannot underflow and lands back inside `b'A'..=b'Z'`.
93            b'a'..=b'z' => Some((Self(byte - 32), Side::Second)),
94            _ => None,
95        }
96    }
97
98    /// Builds a [`Letter`] from a `char`, folding case.
99    ///
100    /// Both `'C'` and `'c'` yield the same `Letter`; the case (which encodes
101    /// side) is not retained.
102    ///
103    /// Case folding here is ASCII-only and deliberately so: the range patterns
104    /// compare Unicode scalar values, so characters that *case-fold* to an
105    /// ASCII letter — `'ſ'` (`U+017F`), `'K'` (`U+212A`) — are still rejected.
106    /// Only the 52 characters the grammar names are abbreviations.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`ParseError::InvalidLetter`] if `c` is not an ASCII letter.
111    /// This is the only variant this function can produce: a `char` has no
112    /// length to be wrong about.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use sashite_sin::Letter;
118    ///
119    /// assert_eq!(Letter::try_from_char('j').unwrap().as_char(), 'J');
120    /// assert!(Letter::try_from_char('+').is_err());
121    /// assert!(Letter::try_from_char('\u{017F}').is_err()); // folds to 's'
122    /// ```
123    #[allow(clippy::cast_possible_truncation)] // guarded: `c` is ASCII here
124    pub const fn try_from_char(c: char) -> Result<Self, ParseError> {
125        match c {
126            'A'..='Z' => Ok(Self(c as u8)),
127            // The arm bounds `c` at `'a'` (97), so the cast is exact and the
128            // subtraction cannot underflow.
129            'a'..='z' => Ok(Self(c as u8 - 32)),
130            _ => Err(ParseError::InvalidLetter),
131        }
132    }
133
134    /// Returns the abbreviation as an uppercase `char`.
135    #[must_use]
136    pub const fn as_char(self) -> char {
137        self.0 as char
138    }
139
140    /// Returns the abbreviation as its raw uppercase ASCII byte.
141    #[must_use]
142    pub const fn as_ascii(self) -> u8 {
143        self.0
144    }
145
146    /// Returns the ASCII byte as it appears in a token for the given side:
147    /// uppercase for [`Side::First`], lowercase for [`Side::Second`].
148    ///
149    /// The type invariant bounds the byte at `b'Z'` (90), so adding 32 reaches
150    /// at most `b'z'` (122) and cannot overflow a `u8` — which matters because
151    /// that overflow would be a panic in a debug build.
152    #[must_use]
153    pub(crate) const fn to_ascii(self, side: Side) -> u8 {
154        match side {
155            Side::First => self.0,
156            Side::Second => self.0 + 32,
157        }
158    }
159}
160
161impl TryFrom<char> for Letter {
162    type Error = ParseError;
163
164    fn try_from(c: char) -> Result<Self, Self::Error> {
165        Self::try_from_char(c)
166    }
167}
168
169impl core::fmt::Debug for Letter {
170    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171        write!(f, "Letter({:?})", self.as_char())
172    }
173}