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