1use std::fmt;
6use std::str::FromStr;
7
8use crate::generated::enums::{Alias, Condition, WrittenUnit};
9use crate::Error;
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
16pub enum Locale {
17 Mng,
19}
20
21impl Locale {
22 pub const ALL: [Locale; 1] = [Locale::Mng];
24
25 pub const fn as_str(self) -> &'static str {
27 match self {
28 Locale::Mng => "MNG",
29 }
30 }
31}
32
33impl FromStr for Locale {
34 type Err = Error;
35
36 fn from_str(name: &str) -> Result<Locale, Error> {
37 Locale::ALL
38 .iter()
39 .copied()
40 .find(|locale| locale.as_str() == name)
41 .ok_or_else(|| Error::UnknownName {
42 kind: "locale",
43 name: name.to_owned(),
44 })
45 }
46}
47
48impl fmt::Display for Locale {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
56pub enum Position {
57 Isol,
59 Init,
61 Medi,
63 Fina,
65}
66
67impl Position {
68 pub const ALL: [Position; 4] = [
70 Position::Isol,
71 Position::Init,
72 Position::Medi,
73 Position::Fina,
74 ];
75
76 pub const fn as_str(self) -> &'static str {
78 match self {
79 Position::Isol => "isol",
80 Position::Init => "init",
81 Position::Medi => "medi",
82 Position::Fina => "fina",
83 }
84 }
85}
86
87impl FromStr for Position {
88 type Err = Error;
89
90 fn from_str(name: &str) -> Result<Position, Error> {
91 Position::ALL
92 .iter()
93 .copied()
94 .find(|position| position.as_str() == name)
95 .ok_or_else(|| Error::UnknownName {
96 kind: "position",
97 name: name.to_owned(),
98 })
99 }
100}
101
102impl fmt::Display for Position {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(self.as_str())
105 }
106}
107
108#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
114pub enum UnitPosition {
115 Isol,
117 Init,
119 Medi,
121 Fina,
123 Control,
125}
126
127impl UnitPosition {
128 pub const ALL: [UnitPosition; 5] = [
130 UnitPosition::Isol,
131 UnitPosition::Init,
132 UnitPosition::Medi,
133 UnitPosition::Fina,
134 UnitPosition::Control,
135 ];
136
137 pub const fn as_str(self) -> &'static str {
139 match self {
140 UnitPosition::Isol => "isol",
141 UnitPosition::Init => "init",
142 UnitPosition::Medi => "medi",
143 UnitPosition::Fina => "fina",
144 UnitPosition::Control => "control",
145 }
146 }
147
148 pub const fn as_position(self) -> Option<Position> {
150 match self {
151 UnitPosition::Isol => Some(Position::Isol),
152 UnitPosition::Init => Some(Position::Init),
153 UnitPosition::Medi => Some(Position::Medi),
154 UnitPosition::Fina => Some(Position::Fina),
155 UnitPosition::Control => None,
156 }
157 }
158}
159
160impl From<Position> for UnitPosition {
161 fn from(position: Position) -> UnitPosition {
162 match position {
163 Position::Isol => UnitPosition::Isol,
164 Position::Init => UnitPosition::Init,
165 Position::Medi => UnitPosition::Medi,
166 Position::Fina => UnitPosition::Fina,
167 }
168 }
169}
170
171impl FromStr for UnitPosition {
172 type Err = Error;
173
174 fn from_str(name: &str) -> Result<UnitPosition, Error> {
175 UnitPosition::ALL
176 .iter()
177 .copied()
178 .find(|position| position.as_str() == name)
179 .ok_or_else(|| Error::UnknownName {
180 kind: "unit position",
181 name: name.to_owned(),
182 })
183 }
184}
185
186impl fmt::Display for UnitPosition {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(self.as_str())
189 }
190}
191
192#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
194pub enum Fvs {
195 Fvs1,
197 Fvs2,
199 Fvs3,
201 Fvs4,
203}
204
205impl Fvs {
206 pub const ALL: [Fvs; 4] = [Fvs::Fvs1, Fvs::Fvs2, Fvs::Fvs3, Fvs::Fvs4];
208
209 pub const fn cp(self) -> u32 {
211 match self {
212 Fvs::Fvs1 => 0x180B,
213 Fvs::Fvs2 => 0x180C,
214 Fvs::Fvs3 => 0x180D,
215 Fvs::Fvs4 => 0x180F,
216 }
217 }
218
219 pub const fn as_char(self) -> char {
221 match self {
222 Fvs::Fvs1 => '\u{180B}',
223 Fvs::Fvs2 => '\u{180C}',
224 Fvs::Fvs3 => '\u{180D}',
225 Fvs::Fvs4 => '\u{180F}',
226 }
227 }
228
229 pub const fn index(self) -> u8 {
231 match self {
232 Fvs::Fvs1 => 1,
233 Fvs::Fvs2 => 2,
234 Fvs::Fvs3 => 3,
235 Fvs::Fvs4 => 4,
236 }
237 }
238
239 pub const fn from_cp(cp: u32) -> Option<Fvs> {
241 match cp {
242 0x180B => Some(Fvs::Fvs1),
243 0x180C => Some(Fvs::Fvs2),
244 0x180D => Some(Fvs::Fvs3),
245 0x180F => Some(Fvs::Fvs4),
246 _ => None,
247 }
248 }
249
250 pub const fn from_index(index: u8) -> Option<Fvs> {
252 match index {
253 1 => Some(Fvs::Fvs1),
254 2 => Some(Fvs::Fvs2),
255 3 => Some(Fvs::Fvs3),
256 4 => Some(Fvs::Fvs4),
257 _ => None,
258 }
259 }
260}
261
262pub(crate) struct Letter {
266 pub cp: u32,
267 pub alias: Alias,
268 pub variants: &'static [Variant],
269}
270
271pub(crate) struct Variant {
273 pub position: Position,
274 pub fvs: Option<Fvs>,
275 pub written: &'static [WrittenUnit],
276 pub default: bool,
277 pub conditions: &'static [Condition],
278}
279
280pub(crate) struct Categories {
282 pub vowel: &'static [Alias],
283 pub consonant: &'static [Alias],
284 pub vowel_masculine: &'static [Alias],
285 pub vowel_feminine: &'static [Alias],
286 pub vowel_neuter: &'static [Alias],
287}
288
289#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
291pub(crate) enum ParticleSym {
292 Mvs,
294 Alias(Alias),
296 Unknown,
299}
300
301pub(crate) struct Particle {
303 pub key: &'static [ParticleSym],
304 pub indices: &'static [usize],
305}
306
307pub(crate) struct LocaleData {
309 pub letters: &'static [Letter],
310 pub categories: Categories,
311 pub particles: &'static [Particle],
312}
313
314pub(crate) struct UnitEntry {
316 pub position: Position,
317 pub units: &'static [WrittenUnit],
318 pub cp: u32,
319 pub fvs: Option<Fvs>,
320}
321
322pub(crate) struct NormalizeData {
324 pub canonical_version: &'static str,
325 pub unit_enc_max_len: usize,
326 pub unit_table: &'static [UnitEntry],
327 pub velar_fem: &'static [UnitEntry],
328 pub velar_fem_units: &'static [WrittenUnit],
329 pub masc_to_fem: &'static [(u32, u32)],
331 pub positioned_units: &'static [(WrittenUnit, Position)],
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn locale_names_round_trip() {
340 for locale in Locale::ALL {
341 assert_eq!(locale.as_str().parse::<Locale>().unwrap(), locale);
342 }
343 assert!("MNGx".parse::<Locale>().is_err());
344 assert!("mng".parse::<Locale>().is_err());
345 for gone in ["TOD", "SIB", "MCH"] {
346 assert!(gone.parse::<Locale>().is_err(), "{gone}");
347 }
348 }
349
350 #[test]
351 fn position_names_round_trip() {
352 for position in Position::ALL {
353 assert_eq!(position.as_str().parse::<Position>().unwrap(), position);
354 assert_eq!(UnitPosition::from(position).as_position(), Some(position));
355 }
356 assert_eq!(
357 "control".parse::<UnitPosition>().unwrap(),
358 UnitPosition::Control
359 );
360 assert_eq!(UnitPosition::Control.as_position(), None);
361 assert!("control".parse::<Position>().is_err());
362 assert!("middle".parse::<UnitPosition>().is_err());
363 }
364
365 #[test]
366 fn fvs_code_points() {
367 for fvs in Fvs::ALL {
368 assert_eq!(Fvs::from_cp(fvs.cp()), Some(fvs));
369 assert_eq!(Fvs::from_index(fvs.index()), Some(fvs));
370 assert_eq!(fvs.as_char() as u32, fvs.cp());
371 }
372 assert_eq!(Fvs::from_cp(0x180E), None);
373 assert_eq!(Fvs::from_index(0), None);
374 }
375 #[test]
376 fn generated_enums_round_trip() {
377 for unit in WrittenUnit::ALL {
378 assert_eq!(unit.as_str().parse::<WrittenUnit>().unwrap(), unit);
379 assert_eq!(unit.to_string(), unit.as_str());
380 }
381 for condition in Condition::ALL {
382 assert_eq!(condition.as_str().parse::<Condition>().unwrap(), condition);
383 }
384 for alias in Alias::ALL {
385 assert_eq!(alias.as_str().parse::<Alias>().unwrap(), alias);
386 }
387 assert!(WrittenUnit::Mvs.is_structural());
388 assert!(WrittenUnit::Nirugu.is_structural());
389 assert!(WrittenUnit::Zwj.is_structural());
390 assert!(!WrittenUnit::A.is_structural());
391 assert_eq!(Condition::ChachlagOnsetGb.as_str(), "chachlag_onset_gb");
392 assert_eq!(Alias::K2.as_str(), "k2");
393 assert_eq!(Alias::Oe.as_str(), "oe");
394 assert!("mvs".parse::<WrittenUnit>().is_err());
395 assert!("MVS".parse::<WrittenUnit>().is_err());
396 }
397
398 #[test]
399 fn generated_tables_have_the_expected_shape() {
400 use crate::generated::{mng, mng_normalize};
401 assert_eq!(mng::DATA.letters.len(), 35);
402 assert_eq!(mng::DATA.particles.len(), 47);
403 assert_eq!(
404 mng::DATA.categories.vowel_masculine,
405 &[Alias::A, Alias::O, Alias::U]
406 );
407 let variants: usize = mng::DATA.letters.iter().map(|l| l.variants.len()).sum();
408 assert_eq!(variants, 216);
409 assert_eq!(mng_normalize::DATA.canonical_version, "mng-canonical/3");
410 assert_eq!(mng_normalize::DATA.unit_enc_max_len, 3);
411 assert_eq!(mng_normalize::DATA.unit_table.len(), 151);
412 assert_eq!(mng_normalize::DATA.velar_fem.len(), 15);
413 assert_eq!(mng_normalize::DATA.positioned_units.len(), 95);
414 assert_eq!(
415 mng_normalize::DATA.masc_to_fem,
416 &[(0x1820, 0x1821), (0x1823, 0x1825), (0x1824, 0x1826)]
417 );
418 for letter in mng::DATA.letters {
420 for position in Position::ALL {
421 let defaults = letter
422 .variants
423 .iter()
424 .filter(|v| v.position == position && v.default)
425 .count();
426 let any = letter.variants.iter().any(|v| v.position == position);
427 assert_eq!(
428 defaults,
429 usize::from(any),
430 "U+{:04X} {:?}",
431 letter.cp,
432 position
433 );
434 }
435 }
436 }
437}