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}