Skip to main content

sashite_sin/
identifier.rs

1//! The central SIN identifier type.
2
3use crate::encode::EncodedSin;
4use crate::error::ParseError;
5use crate::letter::Letter;
6use crate::side::Side;
7
8/// A parsed SIN token: a player's identity at the level of notation.
9///
10/// An `Identifier` bundles the two attributes a token encodes — a [`Letter`]
11/// abbreviation and a [`Side`] — into a single 2-byte `Copy` value.
12/// Construction from typed components via [`Identifier::new`] is total: every
13/// combination is a valid token, so it cannot fail.
14///
15/// The derived total ordering compares attributes in the order letter → side.
16///
17/// # Examples
18///
19/// ```
20/// # fn main() -> Result<(), sashite_sin::ParseError> {
21/// use sashite_sin::{Identifier, Side};
22///
23/// let chinese: Identifier = "c".parse()?;
24/// assert_eq!(chinese.letter().as_char(), 'C');
25/// assert_eq!(chinese.side(), Side::Second);
26/// assert_eq!(chinese.to_char(), 'c');
27///
28/// // Transformations are cheap and infallible; the value is `Copy`.
29/// assert_eq!(chinese.flipped().to_char(), 'C');
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub struct Identifier {
35    letter: Letter,
36    side: Side,
37}
38
39impl Identifier {
40    /// Builds an identifier from its two typed components.
41    ///
42    /// This is infallible: because each component type is valid by
43    /// construction, every combination denotes a valid SIN token.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use sashite_sin::{Identifier, Letter, Side};
49    ///
50    /// let p = Identifier::new(Letter::try_from_char('C').unwrap(), Side::Second);
51    /// assert_eq!(p.encode().as_str(), "c");
52    /// ```
53    #[must_use]
54    pub const fn new(letter: Letter, side: Side) -> Self {
55        Self { letter, side }
56    }
57
58    /// Parses a string slice into an identifier.
59    ///
60    /// # Errors
61    ///
62    /// Returns a [`ParseError`] if `input` is not a valid SIN token (empty,
63    /// too long, or not a single ASCII letter).
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// # fn main() -> Result<(), sashite_sin::ParseError> {
69    /// use sashite_sin::Identifier;
70    ///
71    /// let western = Identifier::parse("W")?;
72    /// assert!(western.is_first());
73    /// assert_eq!(western.letter().as_char(), 'W');
74    /// # Ok(())
75    /// # }
76    /// ```
77    pub const fn parse(input: &str) -> Result<Self, ParseError> {
78        crate::parse::parse(input)
79    }
80
81    /// Reports whether `input` is a valid SIN token, without allocating or
82    /// constructing an identifier on the caller's side.
83    #[must_use]
84    pub const fn is_valid(input: &str) -> bool {
85        Self::parse(input).is_ok()
86    }
87
88    /// Returns the canonical, allocation-free string encoding of this token.
89    #[must_use]
90    pub fn encode(self) -> EncodedSin {
91        EncodedSin::from_identifier(self)
92    }
93
94    /// Returns the token as its single cased character: uppercase for
95    /// [`Side::First`], lowercase for [`Side::Second`].
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// # fn main() -> Result<(), sashite_sin::ParseError> {
101    /// use sashite_sin::Identifier;
102    ///
103    /// assert_eq!(Identifier::parse("J")?.to_char(), 'J');
104    /// assert_eq!(Identifier::parse("j")?.to_char(), 'j');
105    /// # Ok(())
106    /// # }
107    /// ```
108    #[must_use]
109    pub const fn to_char(self) -> char {
110        self.letter.to_ascii(self.side) as char
111    }
112
113    // --- Accessors ---
114
115    /// Returns the player-style abbreviation (always uppercase).
116    #[must_use]
117    pub const fn letter(self) -> Letter {
118        self.letter
119    }
120
121    /// Returns the side the player belongs to.
122    #[must_use]
123    pub const fn side(self) -> Side {
124        self.side
125    }
126
127    // --- Side queries ---
128
129    /// Reports whether the side is [`Side::First`].
130    #[must_use]
131    pub const fn is_first(self) -> bool {
132        matches!(self.side, Side::First)
133    }
134
135    /// Reports whether the side is [`Side::Second`].
136    #[must_use]
137    pub const fn is_second(self) -> bool {
138        matches!(self.side, Side::Second)
139    }
140
141    // --- Transformations (return a new value; the type is `Copy`) ---
142
143    /// Returns a copy with the abbreviation replaced.
144    #[must_use]
145    pub const fn with_letter(self, letter: Letter) -> Self {
146        Self::new(letter, self.side)
147    }
148
149    /// Returns a copy with the side replaced.
150    #[must_use]
151    pub const fn with_side(self, side: Side) -> Self {
152        Self::new(self.letter, side)
153    }
154
155    /// Returns a copy belonging to the opposite [`Side`].
156    #[must_use]
157    pub const fn flipped(self) -> Self {
158        self.with_side(self.side.flip())
159    }
160}
161
162impl core::fmt::Display for Identifier {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        f.write_str(self.encode().as_str())
165    }
166}
167
168impl core::str::FromStr for Identifier {
169    type Err = ParseError;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        Self::parse(s)
173    }
174}
175
176impl TryFrom<&str> for Identifier {
177    type Error = ParseError;
178
179    fn try_from(s: &str) -> Result<Self, Self::Error> {
180        Self::parse(s)
181    }
182}
183
184impl TryFrom<&[u8]> for Identifier {
185    type Error = ParseError;
186
187    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
188        crate::parse::parse_bytes(bytes)
189    }
190}