1type Token = ([char; 4], u8);
5
6static FC_0: &[Token] = &[
7 (['b', '\0', '\0', '\0'], 1),
8 (['d', '\0', '\0', '\0'], 1),
9 (['đ', '\0', '\0', '\0'], 1),
10 (['g', '\0', '\0', '\0'], 1),
11 (['g', 'h', '\0', '\0'], 2),
12 (['m', '\0', '\0', '\0'], 1),
13 (['n', '\0', '\0', '\0'], 1),
14 (['n', 'h', '\0', '\0'], 2),
15 (['p', '\0', '\0', '\0'], 1),
16 (['p', 'h', '\0', '\0'], 2),
17 (['r', '\0', '\0', '\0'], 1),
18 (['s', '\0', '\0', '\0'], 1),
19 (['t', '\0', '\0', '\0'], 1),
20 (['t', 'r', '\0', '\0'], 2),
21 (['v', '\0', '\0', '\0'], 1),
22 (['z', '\0', '\0', '\0'], 1),
23];
24static FC_1: &[Token] = &[
25 (['c', '\0', '\0', '\0'], 1),
26 (['h', '\0', '\0', '\0'], 1),
27 (['k', '\0', '\0', '\0'], 1),
28 (['k', 'h', '\0', '\0'], 2),
29 (['q', 'u', '\0', '\0'], 2),
30 (['t', 'h', '\0', '\0'], 2),
31];
32static FC_2: &[Token] = &[
33 (['c', 'h', '\0', '\0'], 2),
34 (['g', 'i', '\0', '\0'], 2),
35 (['l', '\0', '\0', '\0'], 1),
36 (['n', 'g', '\0', '\0'], 2),
37 (['n', 'g', 'h', '\0'], 3),
38 (['x', '\0', '\0', '\0'], 1),
39];
40static FC_3: &[Token] = &[(['đ', '\0', '\0', '\0'], 1), (['l', '\0', '\0', '\0'], 1)];
41static FC_4: &[Token] = &[(['h', '\0', '\0', '\0'], 1)];
42static FC_ROWS: &[&[Token]] = &[FC_0, FC_1, FC_2, FC_3, FC_4];
43
44static VO_0: &[Token] = &[
45 (['ê', '\0', '\0', '\0'], 1),
46 (['i', '\0', '\0', '\0'], 1),
47 (['u', 'a', '\0', '\0'], 2),
48 (['u', 'ê', '\0', '\0'], 2),
49 (['u', 'y', '\0', '\0'], 2),
50 (['y', '\0', '\0', '\0'], 1),
51];
52static VO_1: &[Token] = &[
53 (['a', '\0', '\0', '\0'], 1),
54 (['i', 'ê', '\0', '\0'], 2),
55 (['o', 'a', '\0', '\0'], 2),
56 (['u', 'e', '\0', '\0'], 2),
57 (['u', 'y', 'ê', '\0'], 3),
58 (['y', 'ê', '\0', '\0'], 2),
59];
60static VO_2: &[Token] = &[
61 (['â', '\0', '\0', '\0'], 1),
62 (['ă', '\0', '\0', '\0'], 1),
63 (['e', '\0', '\0', '\0'], 1),
64 (['o', '\0', '\0', '\0'], 1),
65 (['o', 'o', '\0', '\0'], 2),
66 (['ô', '\0', '\0', '\0'], 1),
67 (['ơ', '\0', '\0', '\0'], 1),
68 (['o', 'e', '\0', '\0'], 2),
69 (['u', '\0', '\0', '\0'], 1),
70 (['ư', '\0', '\0', '\0'], 1),
71 (['u', 'â', '\0', '\0'], 2),
72 (['u', 'ô', '\0', '\0'], 2),
73 (['ư', 'ơ', '\0', '\0'], 2),
74];
75static VO_3: &[Token] = &[(['o', 'ă', '\0', '\0'], 2)];
76static VO_4: &[Token] = &[(['u', 'ơ', '\0', '\0'], 2)];
77static VO_5: &[Token] = &[
78 (['a', 'i', '\0', '\0'], 2),
79 (['a', 'o', '\0', '\0'], 2),
80 (['a', 'u', '\0', '\0'], 2),
81 (['â', 'u', '\0', '\0'], 2),
82 (['a', 'y', '\0', '\0'], 2),
83 (['â', 'y', '\0', '\0'], 2),
84 (['e', 'o', '\0', '\0'], 2),
85 (['ê', 'u', '\0', '\0'], 2),
86 (['i', 'a', '\0', '\0'], 2),
87 (['i', 'ê', 'u', '\0'], 3),
88 (['i', 'u', '\0', '\0'], 2),
89 (['o', 'a', 'i', '\0'], 3),
90 (['o', 'a', 'o', '\0'], 3),
91 (['o', 'a', 'y', '\0'], 3),
92 (['o', 'e', 'o', '\0'], 3),
93 (['o', 'i', '\0', '\0'], 2),
94 (['ô', 'i', '\0', '\0'], 2),
95 (['ơ', 'i', '\0', '\0'], 2),
96 (['ư', 'a', '\0', '\0'], 2),
97 (['u', 'â', 'y', '\0'], 3),
98 (['u', 'i', '\0', '\0'], 2),
99 (['ư', 'i', '\0', '\0'], 2),
100 (['u', 'ô', 'i', '\0'], 3),
101 (['ư', 'ơ', 'i', '\0'], 3),
102 (['ư', 'ơ', 'u', '\0'], 3),
103 (['ư', 'u', '\0', '\0'], 2),
104 (['u', 'y', 'a', '\0'], 3),
105 (['u', 'y', 'u', '\0'], 3),
106 (['y', 'ê', 'u', '\0'], 3),
107];
108static VO_6: &[Token] = &[(['ă', '\0', '\0', '\0'], 1)];
109static VO_7: &[Token] = &[(['i', '\0', '\0', '\0'], 1)];
110static VO_ROWS: &[&[Token]] = &[VO_0, VO_1, VO_2, VO_3, VO_4, VO_5, VO_6, VO_7];
111
112static LC_0: &[Token] = &[(['c', 'h', '\0', '\0'], 2), (['n', 'h', '\0', '\0'], 2)];
113static LC_1: &[Token] = &[(['c', '\0', '\0', '\0'], 1), (['n', 'g', '\0', '\0'], 2)];
114static LC_2: &[Token] = &[
115 (['m', '\0', '\0', '\0'], 1),
116 (['n', '\0', '\0', '\0'], 1),
117 (['p', '\0', '\0', '\0'], 1),
118 (['t', '\0', '\0', '\0'], 1),
119];
120static LC_3: &[Token] = &[(['k', '\0', '\0', '\0'], 1)];
121static LC_4: &[Token] = &[(['c', '\0', '\0', '\0'], 1)];
122static LC_ROWS: &[&[Token]] = &[LC_0, LC_1, LC_2, LC_3, LC_4];
123
124const CV_ALLOWED_MASKS: [u16; 5] = [
125 (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 5),
126 (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5),
127 (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 5),
128 1 << 6,
129 1 << 7,
130];
131
132const VC_ALLOWED_MASKS: [u16; 8] = [
133 (1 << 0) | (1 << 1) | (1 << 2), (1 << 0) | (1 << 1) | (1 << 2), (1 << 0) | (1 << 1) | (1 << 2), (1 << 1) | (1 << 2), 0, 0, 1 << 3, 1 << 4, ];
142
143pub fn lower(c: char) -> char {
146 c.to_lowercase().next().unwrap_or(c)
147}
148
149pub fn toneless_base(c: char) -> char {
152 match lower(c) {
154 'a' | 'à' | 'á' | 'ả' | 'ã' | 'ạ' => 'a',
155 'ă' | 'ằ' | 'ắ' | 'ẳ' | 'ẵ' | 'ặ' => 'ă',
156 'â' | 'ầ' | 'ấ' | 'ẩ' | 'ẫ' | 'ậ' => 'â',
157 'e' | 'è' | 'é' | 'ẻ' | 'ẽ' | 'ẹ' => 'e',
158 'ê' | 'ề' | 'ế' | 'ể' | 'ễ' | 'ệ' => 'ê',
159 'i' | 'ì' | 'í' | 'ỉ' | 'ĩ' | 'ị' => 'i',
160 'o' | 'ò' | 'ó' | 'ỏ' | 'õ' | 'ọ' => 'o',
161 'ô' | 'ồ' | 'ố' | 'ổ' | 'ỗ' | 'ộ' => 'ô',
162 'ơ' | 'ờ' | 'ớ' | 'ở' | 'ỡ' | 'ợ' => 'ơ',
163 'u' | 'ù' | 'ú' | 'ủ' | 'ũ' | 'ụ' => 'u',
164 'ư' | 'ừ' | 'ứ' | 'ử' | 'ữ' | 'ự' => 'ư',
165 'y' | 'ỳ' | 'ý' | 'ỷ' | 'ỹ' | 'ỵ' => 'y',
166 other => other,
167 }
168}
169
170pub fn tone_of(c: char) -> u8 {
172 match lower(c) {
173 'à' | 'ằ' | 'ầ' | 'è' | 'ề' | 'ì' | 'ò' | 'ồ' | 'ờ' | 'ù' | 'ừ' | 'ỳ' => {
174 1
175 }
176 'á' | 'ắ' | 'ấ' | 'é' | 'ế' | 'í' | 'ó' | 'ố' | 'ớ' | 'ú' | 'ứ' | 'ý' => {
177 2
178 }
179 'ả' | 'ẳ' | 'ẩ' | 'ẻ' | 'ể' | 'ỉ' | 'ỏ' | 'ổ' | 'ở' | 'ủ' | 'ử' | 'ỷ' => {
180 3
181 }
182 'ã' | 'ẵ' | 'ẫ' | 'ẽ' | 'ễ' | 'ĩ' | 'õ' | 'ỗ' | 'ỡ' | 'ũ' | 'ữ' | 'ỹ' => {
183 4
184 }
185 'ạ' | 'ặ' | 'ậ' | 'ẹ' | 'ệ' | 'ị' | 'ọ' | 'ộ' | 'ợ' | 'ụ' | 'ự' | 'ỵ' => {
186 5
187 }
188 _ => 0,
189 }
190}
191
192pub fn apply_tone(c: char, t: u8) -> char {
194 let base = toneless_base(c);
195 let table: &[(char, [char; 6])] = &[
196 ('a', ['a', 'à', 'á', 'ả', 'ã', 'ạ']),
197 ('ă', ['ă', 'ằ', 'ắ', 'ẳ', 'ẵ', 'ặ']),
198 ('â', ['â', 'ầ', 'ấ', 'ẩ', 'ẫ', 'ậ']),
199 ('e', ['e', 'è', 'é', 'ẻ', 'ẽ', 'ẹ']),
200 ('ê', ['ê', 'ề', 'ế', 'ể', 'ễ', 'ệ']),
201 ('i', ['i', 'ì', 'í', 'ỉ', 'ĩ', 'ị']),
202 ('o', ['o', 'ò', 'ó', 'ỏ', 'õ', 'ọ']),
203 ('ô', ['ô', 'ồ', 'ố', 'ổ', 'ỗ', 'ộ']),
204 ('ơ', ['ơ', 'ờ', 'ớ', 'ở', 'ỡ', 'ợ']),
205 ('u', ['u', 'ù', 'ú', 'ủ', 'ũ', 'ụ']),
206 ('ư', ['ư', 'ừ', 'ứ', 'ử', 'ữ', 'ự']),
207 ('y', ['y', 'ỳ', 'ý', 'ỷ', 'ỹ', 'ỵ']),
208 ];
209 for &(b, ref tones) in table {
210 if base == b {
211 let ti = (t as usize).min(5);
212 let ch = tones[ti];
213 return if c.is_uppercase() {
214 ch.to_uppercase().next().unwrap_or(ch)
215 } else {
216 ch
217 };
218 }
219 }
220 c
221}
222
223pub fn apply_mark(c: char, m: u8) -> char {
225 let base = toneless_base(lower(c));
226 let result = match (base, m) {
227 ('a', 1) => 'â',
228 ('a', 2) => 'ă',
229 ('e', 1) => 'ê',
230 ('o', 1) => 'ô',
231 ('o', 3) => 'ơ',
232 ('u', 3) => 'ư',
233 ('u', 1) => 'ư', ('d', 4) => 'đ',
235 _ => return c,
236 };
237 let tone = tone_of(c);
238 let result = if tone > 0 {
239 apply_tone(result, tone)
240 } else {
241 result
242 };
243 if c.is_uppercase() {
244 result.to_uppercase().next().unwrap_or(result)
245 } else {
246 result
247 }
248}
249
250pub fn is_vowel(c: char) -> bool {
251 matches!(
252 lower(c),
253 'a' | 'à'
254 | 'á'
255 | 'ả'
256 | 'ã'
257 | 'ạ'
258 | 'ă'
259 | 'ằ'
260 | 'ắ'
261 | 'ẳ'
262 | 'ẵ'
263 | 'ặ'
264 | 'â'
265 | 'ầ'
266 | 'ấ'
267 | 'ẩ'
268 | 'ẫ'
269 | 'ậ'
270 | 'e'
271 | 'è'
272 | 'é'
273 | 'ẻ'
274 | 'ẽ'
275 | 'ẹ'
276 | 'ê'
277 | 'ề'
278 | 'ế'
279 | 'ể'
280 | 'ễ'
281 | 'ệ'
282 | 'i'
283 | 'ì'
284 | 'í'
285 | 'ỉ'
286 | 'ĩ'
287 | 'ị'
288 | 'o'
289 | 'ò'
290 | 'ó'
291 | 'ỏ'
292 | 'õ'
293 | 'ọ'
294 | 'ô'
295 | 'ồ'
296 | 'ố'
297 | 'ổ'
298 | 'ỗ'
299 | 'ộ'
300 | 'ơ'
301 | 'ờ'
302 | 'ớ'
303 | 'ở'
304 | 'ỡ'
305 | 'ợ'
306 | 'u'
307 | 'ù'
308 | 'ú'
309 | 'ủ'
310 | 'ũ'
311 | 'ụ'
312 | 'ư'
313 | 'ừ'
314 | 'ứ'
315 | 'ử'
316 | 'ữ'
317 | 'ự'
318 | 'y'
319 | 'ỳ'
320 | 'ý'
321 | 'ỷ'
322 | 'ỹ'
323 | 'ỵ'
324 )
325}
326
327fn lookup_mask(rows: &[&[Token]], input: &[char], full: bool, complete: bool) -> u16 {
330 let input_len = input.len() as u8;
331 let mut ret = 0u16;
332 for (index, tokens) in rows.iter().enumerate() {
333 for (t_chars, t_len) in *tokens {
334 if *t_len < input_len {
335 continue;
336 }
337 if full && *t_len > input_len {
338 continue;
339 }
340 let mut is_match = true;
341 for i in 0..input.len() {
342 let ic = input[i];
343 let tc = t_chars[i];
344 if ic != tc && (complete || toneless_base(tc) != toneless_base(ic)) {
345 is_match = false;
346 break;
347 }
348 }
349 if is_match {
350 ret |= 1u16 << index;
351 break;
352 }
353 }
354 }
355 ret
356}
357
358fn is_valid_cv(fc_mask: u16, vo_mask: u16) -> bool {
359 let mut mask = fc_mask;
360 while mask != 0 {
361 let idx = mask.trailing_zeros() as usize;
362 if idx < CV_ALLOWED_MASKS.len() && (CV_ALLOWED_MASKS[idx] & vo_mask) != 0 {
363 return true;
364 }
365 mask &= mask - 1;
366 }
367 false
368}
369
370fn is_valid_vc(vo_mask: u16, lc_mask: u16) -> bool {
371 let mut mask = vo_mask;
372 while mask != 0 {
373 let idx = mask.trailing_zeros() as usize;
374 if idx < VC_ALLOWED_MASKS.len() && (VC_ALLOWED_MASKS[idx] & lc_mask) != 0 {
375 return true;
376 }
377 mask &= mask - 1;
378 }
379 false
380}
381
382fn split_cvc(s: &str) -> (Vec<char>, Vec<char>, Vec<char>) {
386 let chars: Vec<char> = s.chars().map(|c| toneless_base(lower(c))).collect();
387 if chars.is_empty() {
388 return (vec![], vec![], vec![]);
389 }
390
391 let mut lc_start = chars.len();
393 while lc_start > 0 {
394 if is_vowel(chars[lc_start - 1]) {
395 break;
396 }
397 lc_start -= 1;
398 }
399
400 let mut vo_start = lc_start;
402 while vo_start > 0 {
403 if !is_vowel(chars[vo_start - 1]) {
404 break;
405 }
406 vo_start -= 1;
407 }
408
409 let mut fc: Vec<char> = chars[..vo_start].to_vec();
410 let mut vo: Vec<char> = chars[vo_start..lc_start].to_vec();
411 let lc: Vec<char> = chars[lc_start..].to_vec();
412
413 if !vo.is_empty() && fc.len() == 1 && fc[0] == 'g' && vo[0] == 'i' {
418 fc.push(vo.remove(0)); if vo.is_empty() && !lc.is_empty() {
420 return (fc, vo, lc); }
422 }
423
424 if !vo.is_empty() && fc.len() == 1 && fc[0] == 'q' && vo[0] == 'u' {
427 fc.push(vo.remove(0)); if vo.is_empty() && !lc.is_empty() {
429 return (fc, vo, lc); }
431 }
432
433 (fc, vo, lc)
434}
435
436pub fn is_valid_cvc(s: &str) -> bool {
441 if s.is_empty() || s.len() <= 1 {
442 return s.len() <= 1; }
444
445 let (fc, vo, lc) = split_cvc(s);
446
447 if vo.is_empty()
450 && !lc.is_empty()
451 && ((fc.len() == 2 && fc[0] == 'g' && fc[1] == 'i')
452 || (fc.len() == 2 && fc[0] == 'q' && fc[1] == 'u'))
453 {
454 return false;
455 }
456
457 if fc.len() == 1 && fc[0] == 'k' && !vo.is_empty() {
460 let first_vowel = vo[0];
461 if !matches!(first_vowel, 'e' | 'ê' | 'i' | 'y') {
462 return false;
463 }
464 }
465
466 let fc_mask = if !fc.is_empty() {
470 let m = lookup_mask(FC_ROWS, &fc, !vo.is_empty(), true);
471 if m == 0 {
472 return false;
473 }
474 m
475 } else {
476 0
477 };
478
479 let vo_mask = if !vo.is_empty() {
480 let m = lookup_mask(VO_ROWS, &vo, !lc.is_empty(), false);
481 if m == 0 {
482 return false;
483 }
484 m
485 } else {
486 0
487 };
488
489 let lc_mask = if !lc.is_empty() {
490 let m = lookup_mask(LC_ROWS, &lc, false, true);
491 if m == 0 {
492 return false;
493 }
494 m
495 } else {
496 0
497 };
498
499 if vo_mask == 0 {
500 return fc_mask != 0;
501 }
502 if fc_mask != 0 {
503 if !is_valid_cv(fc_mask, vo_mask) {
504 return false;
505 }
506 if lc_mask == 0 {
507 return true;
508 }
509 }
510 if lc_mask != 0 {
511 return is_valid_vc(vo_mask, lc_mask);
512 }
513 true
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn valid_syllables() {
522 assert!(is_valid_cvc("ba"));
523 assert!(is_valid_cvc("con"));
524 assert!(is_valid_cvc("không"));
525 assert!(is_valid_cvc("người"));
526 assert!(is_valid_cvc("được"));
527 assert!(is_valid_cvc("trường"));
528 assert!(is_valid_cvc("buồn"));
529 assert!(is_valid_cvc("đẹp"));
530 assert!(is_valid_cvc("bâ"));
531 assert!(is_valid_cvc("mê"));
532 assert!(is_valid_cvc("că"));
533 assert!(is_valid_cvc("mơ"));
534 assert!(is_valid_cvc("ngoăn"));
535 }
536
537 #[test]
538 fn invalid_english_words() {
539 assert!(!is_valid_cvc("wôd")); assert!(!is_valid_cvc("rebôt")); assert!(!is_valid_cvc("ôk")); assert!(!is_valid_cvc("vâi")); assert!(!is_valid_cvc("alô")); assert!(!is_valid_cvc("world")); assert!(!is_valid_cvc("wỏld")); }
547
548 #[test]
549 fn đ_abbreviations() {
550 assert!(!is_valid_cvc("vcđ"));
551 assert!(!is_valid_cvc("đc"));
552 assert!(!is_valid_cvc("nđm"));
553 assert!(!is_valid_cvc("avcđ"));
554 assert!(!is_valid_cvc("bcđ"));
555 }
556}
557
558#[test]
559fn debug_gi_split() {
560 let (fc, vo, lc) = split_cvc("giờ");
561 eprintln!("giờ: fc={fc:?} vo={vo:?} lc={lc:?}");
562 eprintln!("valid: {}", is_valid_cvc("giờ"));
563}