1use crate::tables;
6
7#[derive(Clone, Copy, Default)]
8struct Letter {
9 c: char,
10 is_vowel: bool, variant: u8, tone: u8, is_dstroke: bool, upper: bool,
11}
12
13impl Letter {
14 fn new(c: char, upper: bool) -> Self {
15 let lc = c.to_ascii_lowercase();
16 Letter { c: lc, is_vowel: tables::is_vowel(lc), variant: 0, tone: 0, is_dstroke: false, upper }
17 }
18}
19
20fn emit(ls: &[Letter]) -> String {
21 let mut out = String::with_capacity(ls.len() * 3);
22 for lt in ls {
23 if lt.is_dstroke {
24 out.push_str(if lt.upper { tables::D_STROKE_UP } else { tables::D_STROKE_LO });
25 } else if lt.is_vowel {
26 out.push_str(tables::vowel_utf8(lt.c, lt.variant, lt.tone, lt.upper));
27 } else {
28 out.push(if lt.upper { lt.c.to_ascii_uppercase() } else { lt.c });
29 }
30 }
31 out
32}
33
34fn last_vowel_index(ls: &[Letter]) -> Option<usize> { ls.iter().rposition(|lt| lt.is_vowel) }
35fn last_d_index(ls: &[Letter]) -> Option<usize> {
36 ls.iter().rposition(|lt| lt.c == 'd' && !lt.is_vowel && !lt.is_dstroke)
37}
38
39fn last_w_target_index(ls: &[Letter]) -> Option<usize> {
42 ls.iter().rposition(|lt| lt.is_vowel && matches!(lt.c, 'a' | 'o' | 'u' | 'e'))
43}
44
45fn tone_vowel_index(ls: &[Letter], _modern: bool) -> Option<usize> {
46 let end = last_vowel_index(ls)?;
47 let mut start = end;
48 while start > 0 && ls[start - 1].is_vowel { start -= 1; }
49 if start < end {
50 if ls[start].c == 'u' && start > 0 && ls[start - 1].c == 'q' { start += 1; }
51 else if ls[start].c == 'i' && start > 0 && ls[start - 1].c == 'g' { start += 1; }
52 }
53 if start > end { start = end; }
54 for i in (start..=end).rev() { if ls[i].variant != 0 { return Some(i); } }
55 let count = end - start + 1;
56 if count == 1 { return Some(start); }
57 if count >= 3 {
58 if ls[start].c == 'u' && start + 2 <= end && ls[start + 1].c == 'y' && ls[start + 2].c == 'e'
59 { return Some(start + 2); }
60 return Some(start + 1);
61 }
62 if end + 1 < ls.len() { return Some(end); }
63 let (c1, c2) = (ls[start].c, ls[end].c);
64 if (c1 == 'o' && c2 == 'a') || (c1 == 'o' && c2 == 'e') || (c1 == 'u' && c2 == 'y') {
65 return Some(end); }
67 Some(start) }
69
70fn telex_tone(c: char) -> Option<u8> {
71 match c.to_ascii_lowercase() { 's'=>Some(2),'f'=>Some(1),'r'=>Some(3),'x'=>Some(4),'j'=>Some(5), _=>None }
72}
73fn vni_tone(c: char) -> Option<u8> {
74 match c { '1'=>Some(2),'2'=>Some(1),'3'=>Some(3),'4'=>Some(4),'5'=>Some(5), _=>None }
75}
76
77pub fn convert_telex(input: &str, modern: bool, short_w: bool) -> String {
78 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
79 for ch in input.chars() {
80 let lc = ch.to_ascii_lowercase(); let upper = ch.is_ascii_uppercase(); let n = ls.len();
81 if lc == 'd' && n > 0 && ls[n-1].c == 'd' && !ls[n-1].is_vowel && !ls[n-1].is_dstroke
82 { ls[n-1].is_dstroke = true; if upper { ls[n-1].upper = true; } continue; }
83 if tables::is_vowel(lc) && matches!(lc, 'a'|'e'|'o') && n > 0 {
88 let mut consumed = false;
89 if let Some(vi) = last_vowel_index(&ls) {
90 for i in (0..=vi).rev() {
91 if ls[i].is_vowel {
92 if ls[i].c == lc {
93 if ls[i].variant == 0 { ls[i].variant = 1; if upper { ls[i].upper = true; } consumed = true; break; }
94 if ls[i].variant == 1 { ls[i].variant = 0; break; } }
96 } else { break; }
97 }
98 }
99 if consumed { continue; }
100 }
101 if lc == 'w' {
102 if let Some(vi) = last_w_target_index(&ls) {
103 match ls[vi].c {
104 'a' => { ls[vi].variant = 2; continue; }
105 'o' => { ls[vi].variant = 2; if vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; }
106 'u' => { ls[vi].variant = 2; continue; }
107 'e' => { ls[vi].variant = 1; continue; }
108 _ => {}
109 }
110 } else if short_w {
111 ls.push(Letter { c: 'u', is_vowel: true, variant: 2, tone: 0, is_dstroke: false, upper });
113 continue;
114 }
115 }
116 if let Some(t) = telex_tone(lc) {
117 if let Some(vi) = tone_vowel_index(&ls, modern) {
118 ls[vi].tone = t; continue; }
120 }
121 if lc == 'z' {
122 if let Some(vi) = tone_vowel_index(&ls, modern) {
123 if ls[vi].tone != 0 { ls[vi].tone = 0; continue; }
124 }
125 }
126 ls.push(Letter::new(ch, upper));
127 }
128 emit(&ls)
129}
130
131pub fn convert_teip_vni(input: &str, modern: bool, short_w: bool) -> String {
137 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
138 for ch in input.chars() {
139 let lc = ch.to_ascii_lowercase(); let upper = ch.is_ascii_uppercase(); let n = ls.len();
140
141 if let Some(t) = vni_tone(lc) {
143 if let Some(vi) = tone_vowel_index(&ls, modern) { ls[vi].tone = t; continue; }
144 }
145 if lc >= '0' && lc <= '9' {
146 match lc {
147 '6' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'a'|'e'|'o') { ls[vi].variant = 1; continue; } } }
148 '7' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'o'|'u') { ls[vi].variant = 2; if ls[vi].c == 'o' && vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; } } }
149 '8' => { if let Some(vi) = last_vowel_index(&ls) { if ls[vi].c == 'a' { ls[vi].variant = 2; continue; } } }
150 '9' => { if let Some(di) = last_d_index(&ls) { ls[di].is_dstroke = true; if upper { ls[di].upper = true; } } continue; }
151 '0' => { continue; }
152 _ => {} }
154 }
155
156 if lc == 'd' && n > 0 && ls[n-1].c == 'd' && !ls[n-1].is_vowel && !ls[n-1].is_dstroke
158 { ls[n-1].is_dstroke = true; if upper { ls[n-1].upper = true; } continue; }
159
160 if tables::is_vowel(lc) && matches!(lc, 'a'|'e'|'o') && n > 0 {
164 let mut consumed = false;
165 if let Some(vi) = last_vowel_index(&ls) {
166 for i in (0..=vi).rev() {
167 if ls[i].is_vowel {
168 if ls[i].c == lc {
169 if ls[i].variant == 0 { ls[i].variant = 1; if upper { ls[i].upper = true; } consumed = true; break; }
170 if ls[i].variant == 1 { ls[i].variant = 0; break; } }
172 } else { break; }
173 }
174 }
175 if consumed { continue; }
176 }
177
178 if lc == 'w' {
180 if let Some(vi) = last_w_target_index(&ls) {
181 match ls[vi].c {
182 'a' => { ls[vi].variant = 2; continue; }
183 'o' => { ls[vi].variant = 2; if vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; }
184 'u' => { ls[vi].variant = 2; continue; }
185 'e' => { ls[vi].variant = 1; continue; }
186 _ => {}
187 }
188 } else if short_w {
189 ls.push(Letter { c: 'u', is_vowel: true, variant: 2, tone: 0, is_dstroke: false, upper });
190 continue;
191 }
192 }
193
194 if let Some(t) = telex_tone(lc) {
196 if let Some(vi) = tone_vowel_index(&ls, modern) {
197 ls[vi].tone = t; continue; }
199 }
200 if lc == 'z' {
201 if let Some(vi) = tone_vowel_index(&ls, modern) {
202 if ls[vi].tone != 0 { ls[vi].tone = 0; continue; }
203 }
204 }
205
206 ls.push(Letter::new(ch, upper));
207 }
208 emit(&ls)
209}
210
211pub fn convert_viqr(input: &str) -> String {
218 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
219 for ch in input.chars() {
220 let lc = ch.to_ascii_lowercase(); let n = ls.len();
221
222 match ch {
224 '^' | '(' | '+' => {
225 if let Some(vi) = last_vowel_index(&ls) {
226 match (ch, ls[vi].c) {
227 ('^', 'a' | 'e' | 'o') => { ls[vi].variant = 1; continue; }
228 ('(', 'a') => { ls[vi].variant = 2; continue; }
229 ('+', 'o' | 'u') => { ls[vi].variant = 2; continue; }
230 _ => {} }
232 }
233 ls.push(Letter { c: ch, is_vowel: false, variant: 0, tone: 0, is_dstroke: false, upper: false });
235 continue;
236 }
237 _ => {}
238 }
239
240 match ch {
242 '\'' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 2; continue; } }
243 '`' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 1; continue; } }
244 '?' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 3; continue; } }
245 '~' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 4; continue; } }
246 '.' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 5; continue; } }
247 _ => {}
248 }
249
250 if lc == 'd' && n > 0 && ls[n-1].c == 'd' && !ls[n-1].is_vowel && !ls[n-1].is_dstroke {
252 ls[n-1].is_dstroke = true;
253 if ch.is_ascii_uppercase() { ls[n-1].upper = true; }
254 continue;
255 }
256
257 ls.push(Letter::new(ch, ch.is_ascii_uppercase()));
258 }
259 emit(&ls)
260}
261
262pub fn convert_vni(input: &str, modern: bool, _short_w: bool) -> String {
263 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
264 for ch in input.chars() {
265 let lc = ch.to_ascii_lowercase(); let upper = ch.is_ascii_uppercase();
266 if let Some(t) = vni_tone(lc) {
267 if let Some(vi) = tone_vowel_index(&ls, modern) { ls[vi].tone = t; continue; }
268 }
269 match lc {
270 '6' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'a'|'e'|'o') { ls[vi].variant = 1; continue; } } }
271 '7' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'o'|'u') { ls[vi].variant = 2; if ls[vi].c == 'o' && vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; } } }
272 '8' => { if let Some(vi) = last_vowel_index(&ls) { if ls[vi].c == 'a' { ls[vi].variant = 2; continue; } } }
273 '9' => { if let Some(di) = last_d_index(&ls) { ls[di].is_dstroke = true; if upper { ls[di].upper = true; } } continue; }
274 '0' => { continue; }
275 _ => {}
276 }
277 if lc.is_ascii_alphabetic() || (lc.is_ascii_digit() && !ls.is_empty()) { ls.push(Letter::new(ch, upper)); }
278 }
279 emit(&ls)
280}
281
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 fn t(s: &str) -> String { convert_telex(s, true, false) }
288 fn tw(s: &str) -> String { convert_telex(s, true, true) }
289 fn tt(s: &str) -> String { convert_telex(s, false, true) }
291 fn vt(s: &str) -> String { convert_vni(s, false, true) }
292
293 #[test] fn telex_basic() {
294 assert_eq!(t("as"),"á"); assert_eq!(t("af"),"à"); assert_eq!(t("ar"),"ả"); assert_eq!(t("ax"),"ã"); assert_eq!(t("aj"),"ạ");
295 assert_eq!(t("aw"),"ă"); assert_eq!(t("aa"),"â"); assert_eq!(t("ee"),"ê"); assert_eq!(t("oo"),"ô"); assert_eq!(t("ow"),"ơ");
296 assert_eq!(t("uw"),"ư"); assert_eq!(t("dd"),"đ");
297 }
298 #[test] fn telex_compound() {
299 assert_eq!(t("aws"),"ắ"); assert_eq!(t("aas"),"ấ"); assert_eq!(t("ees"),"ế"); assert_eq!(t("oos"),"ố"); assert_eq!(t("ows"),"ớ"); assert_eq!(t("uws"),"ứ");
300 }
301 #[test] fn telex_words() {
302 assert_eq!(t("xin"),"xin"); assert_eq!(t("tooi"),"tôi"); assert_eq!(t("laf"),"là"); assert_eq!(t("cos"),"có");
303 assert_eq!(t("khoong"),"không"); assert_eq!(t("tieengs"),"tiếng"); assert_eq!(t("Vieetj"),"Việt");
304 }
305 #[test] fn telex_uow() {
306 assert_eq!(t("uow"),"ươ"); assert_eq!(t("cuowcs"),"cước"); assert_eq!(t("dduwowcj"),"được"); assert_eq!(t("nguwowif"),"người");
307 }
308 #[test] fn telex_gi_qu() {
309 assert_eq!(t("gif"),"gì"); assert_eq!(t("giowf"),"giờ"); assert_eq!(t("gioir"),"giỏi"); assert_eq!(t("gias"),"giá"); assert_eq!(t("quas"),"quá");
310 }
311 #[test] fn telex_tone_placement() {
312 assert_eq!(t("chafo"),"chào"); assert_eq!(t("toans"),"toán"); assert_eq!(t("hoaf"),"hoà"); assert_eq!(t("thowif"),"thời");
313 assert_eq!(t("cuar"),"của"); assert_eq!(t("thoix"),"thõi");
314 assert_eq!(t("ofa"),"òa"); assert_eq!(t("oaf"),"oà"); }
318 #[test] fn telex_english() {
319 assert_eq!(t("hello"),"hello");
320 assert_eq!(t("wood"),"wôd");
321 assert_eq!(t("reboot"),"rebôt");
322 }
323 #[test] fn telex_z() { assert_eq!(t("asz"),"a"); }
324 #[test] fn telex_w_standalone() {
325 assert_eq!(t("w"),"w"); assert_eq!(tw("w"),"ư"); assert_eq!(tw("ws"),"ứ");
328 assert_eq!(tw("wf"),"ừ");
329 assert_eq!(tw("wn"),"ưn");
330 }
331
332 #[test] fn telex_dd_traditional() {
335 assert_eq!(tt("dd"),"đ"); assert_eq!(tt("DD"),"Đ");
336 assert_eq!(tt("Dd"),"Đ"); assert_eq!(tt("dD"),"Đ");
337 }
338 #[test] fn telex_marks_traditional() {
339 assert_eq!(tt("aa"),"â"); assert_eq!(tt("aw"),"ă");
340 assert_eq!(tt("ee"),"ê"); assert_eq!(tt("ew"),"ê");
341 assert_eq!(tt("oo"),"ô"); assert_eq!(tt("ow"),"ơ");
342 assert_eq!(tt("uw"),"ư"); assert_eq!(tt("uow"),"ươ");
343 }
344 #[test] fn telex_w_traditional() {
345 assert_eq!(tt("w"),"ư"); assert_eq!(tt("W"),"Ư");
346 assert_eq!(tt("wn"),"ưn"); assert_eq!(tt("ws"),"ứ"); assert_eq!(tt("wf"),"ừ");
347 }
348 #[test] fn telex_compound_traditional() {
349 assert_eq!(tt("aws"),"ắ"); assert_eq!(tt("aas"),"ấ"); assert_eq!(tt("ees"),"ế");
350 assert_eq!(tt("oos"),"ố"); assert_eq!(tt("ows"),"ớ"); assert_eq!(tt("uws"),"ứ");
351 }
352 #[test] fn telex_words_traditional() {
353 assert_eq!(tt("chaof"),"chào"); assert_eq!(tt("vieejt"),"việt");
354 assert_eq!(tt("tieengs"),"tiếng"); assert_eq!(tt("quas"),"quá");
355 assert_eq!(tt("gias"),"giá"); assert_eq!(tt("xin"),"xin");
356 assert_eq!(tt("tooi"),"tôi"); assert_eq!(tt("laf"),"là"); assert_eq!(tt("cos"),"có");
357 assert_eq!(tt("khoong"),"không"); assert_eq!(tt("Vieetj"),"Việt");
358 }
359 #[test] fn telex_tone_traditional() {
360 assert_eq!(tt("toans"),"toán"); assert_eq!(tt("hoaf"),"hoà");
362 assert_eq!(tt("thowif"),"thời"); assert_eq!(tt("cuar"),"của");
363 }
364 #[test] fn telex_gi_qu_traditional() {
365 assert_eq!(tt("gif"),"gì"); assert_eq!(tt("giowf"),"giờ");
366 assert_eq!(tt("gioir"),"giỏi");
367 }
368 #[test] fn telex_uow_traditional() {
369 assert_eq!(tt("cuowcs"),"cước"); assert_eq!(tt("dduwowcj"),"được");
370 assert_eq!(tt("nguwowif"),"người");
371 }
372 #[test] fn telex_z_traditional() { assert_eq!(tt("asz"),"a"); }
373 #[test] fn telex_english_traditional() { assert_eq!(tt("hello"),"hello"); }
374 #[test] fn telex_more_traditional() {
375 assert_eq!(tt("hoanf"),"hoàn"); assert_eq!(tt("huyeenf"),"huyền");
376 assert_eq!(tt("nguyeexn"),"nguyễn"); assert_eq!(tt("ngoaif"),"ngoài");
377 assert_eq!(tt("thuowngf"),"thường");
378 }
379 #[test] fn vni_marks_traditional() {
380 assert_eq!(vt("d9"),"đ"); assert_eq!(vt("a6"),"â"); assert_eq!(vt("a8"),"ă");
381 assert_eq!(vt("e6"),"ê"); assert_eq!(vt("o6"),"ô"); assert_eq!(vt("o7"),"ơ");
382 assert_eq!(vt("u7"),"ư"); assert_eq!(vt("uo7"),"ươ");
383 }
384 #[test] fn vni_words_traditional() {
385 assert_eq!(vt("vie65t"),"việt"); assert_eq!(vt("tie61ng"),"tiếng");
386 assert_eq!(vt("qua1"),"quá"); assert_eq!(vt("gia1"),"giá");
387 }
388
389 #[test] fn telex_tone_replace() {
391 assert_eq!(tt("saosf"),"sào"); assert_eq!(tt("saojs"),"sáo"); assert_eq!(tt("saojr"),"sảo"); assert_eq!(tt("hoasf"),"hoà"); assert_eq!(tt("toansr"),"toản");}
397 #[test] fn teipvni_tone_replace() {
398 assert_eq!(tv("saosf"),"sào"); assert_eq!(tv("sao1f"),"sào"); }
401
402 #[test] fn telex_triple_toggle() {
404 assert_eq!(tt("rooot"),"root"); assert_eq!(tt("roo"),"rô");
406 assert_eq!(tt("aa"),"â"); assert_eq!(tt("aaa"),"aa"); assert_eq!(tt("aaaa"),"aâ"); assert_eq!(tt("ee"),"ê");
410 assert_eq!(tt("eee"),"ee");
411 assert_eq!(tt("oooo"),"oô"); }
413
414 #[test] fn telex_double_vowel_across_y() {
416 assert_eq!(tt("vaya"),"vây"); assert_eq!(tt("vayaj"),"vậy"); assert_eq!(tt("vayas"),"vấy"); assert_eq!(tt("vayaf"),"vầy"); assert_eq!(tt("mayas"),"mấy"); assert_eq!(tt("dayaj"),"dậy"); assert_eq!(tt("quaya"),"quây"); assert_eq!(tt("quayar"),"quẩy");assert_eq!(tt("saua"),"sâu");
426 assert_eq!(tt("sauas"),"sấu");
427 assert_eq!(tt("dauaf"),"dầu");
428 assert_eq!(tt("ee"),"ê");
430 assert_eq!(tt("aay"),"ây");
432 }
433
434 #[test] fn telex_w_target() {
436 assert_eq!(tt("toiw"),"tơi");
438 assert_eq!(tt("toiws"),"tới"); assert_eq!(tt("toiwf"),"tời"); assert_eq!(tt("toiwr"),"tởi"); assert_eq!(tt("toiwj"),"tợi"); assert_eq!(tt("nguoiwf"),"người");
444 assert_eq!(tt("nguoiws"),"ngưới");
445 assert_eq!(tt("muoiwf"),"mười");
447 assert_eq!(tt("tuoiws"),"tưới");
449 assert_eq!(tt("tuooir"),"tuổi");
451 assert_eq!(tt("tuooif"),"tuồi");
452 }
453 #[test] fn telex_toan() {
454 assert_eq!(tt("toanf"),"toàn");
456 assert_eq!(t("toanf"),"toàn");
457 assert_eq!(tt("hoanf"),"hoàn");
458 assert_eq!(t("hoanf"),"hoàn");
459 assert_eq!(t("oaf"),"oà"); assert_eq!(t("ofa"),"òa"); assert_eq!(tt("oaf"),"oà");
464 assert_eq!(tt("ofa"),"òa"); }
466
467 fn tv(s: &str) -> String { convert_teip_vni(s, false, true) }
469
470 #[test] fn teipvni_telex_words() {
471 assert_eq!(tv("vieejt"),"việt"); assert_eq!(tv("dd"),"đ");
472 assert_eq!(tv("aa"),"â"); assert_eq!(tv("aw"),"ă");
473 assert_eq!(tv("ee"),"ê"); assert_eq!(tv("oo"),"ô");
474 assert_eq!(tv("ow"),"ơ"); assert_eq!(tv("uw"),"ư");
475 assert_eq!(tv("uow"),"ươ"); assert_eq!(tv("w"),"ư");
476 }
477 #[test] fn teipvni_vni_marks() {
478 assert_eq!(tv("a6"),"â"); assert_eq!(tv("a8"),"ă");
479 assert_eq!(tv("e6"),"ê"); assert_eq!(tv("o6"),"ô");
480 assert_eq!(tv("o7"),"ơ"); assert_eq!(tv("u7"),"ư");
481 assert_eq!(tv("uo7"),"ươ"); assert_eq!(tv("d9"),"đ");
482 }
483 #[test] fn teipvni_numbers() {
484 assert_eq!(tv("qua1"),"quá"); assert_eq!(tv("gia1"),"giá");
486 assert_eq!(tv("vie65t"),"việt"); assert_eq!(tv("tie61ng"),"tiếng");
487 }
488 #[test] fn teipvni_mixed() {
489 assert_eq!(tv("vie6t"),"viêt"); assert_eq!(tv("vie6ts"),"viết"); assert_eq!(tv("tie6ngs"),"tiếng");
493 }
494
495 fn viqr(s: &str) -> String { convert_viqr(s) }
497
498 #[test] fn viqr_marks() {
499 assert_eq!(viqr("a^"),"â"); assert_eq!(viqr("a("),"ă");
500 assert_eq!(viqr("e^"),"ê"); assert_eq!(viqr("o^"),"ô");
501 assert_eq!(viqr("o+"),"ơ"); assert_eq!(viqr("u+"),"ư");
502 assert_eq!(viqr("dd"),"đ");
503 }
504 #[test] fn viqr_tones() {
505 assert_eq!(viqr("a'"),"á"); assert_eq!(viqr("a`"),"à");
506 assert_eq!(viqr("a?"),"ả"); assert_eq!(viqr("a~"),"ã");
507 assert_eq!(viqr("a."),"ạ");
508 }
509 #[test] fn viqr_words() {
510 assert_eq!(viqr("vie^'t"),"viết");
511 assert_eq!(viqr("nu+o+'c"),"nước");
512 assert_eq!(viqr("ngu+o+`i"),"người");
513 }
514 #[test] fn viqr_dd() {
515 assert_eq!(viqr("dd"),"đ"); assert_eq!(viqr("DD"),"Đ");
516 }
517 #[test] fn viqr_english() { assert_eq!(viqr("hello"),"hello"); }
518}
519
520#[test]
521fn full_parity() {
522 struct Case { input: &'static str, expected: &'static str, method: u8 }
526 let cases = [
528 Case{input:"dd",expected:"đ",method:1},Case{input:"DD",expected:"Đ",method:1},
530 Case{input:"Dd",expected:"Đ",method:1},Case{input:"dD",expected:"Đ",method:1},
531 Case{input:"aa",expected:"â",method:1},Case{input:"aw",expected:"ă",method:1},
532 Case{input:"ee",expected:"ê",method:1},Case{input:"ew",expected:"ê",method:1},
533 Case{input:"oo",expected:"ô",method:1},Case{input:"ow",expected:"ơ",method:1},
534 Case{input:"uw",expected:"ư",method:1},Case{input:"uow",expected:"ươ",method:1},
535 Case{input:"w",expected:"ư",method:1},Case{input:"W",expected:"Ư",method:1},
536 Case{input:"wn",expected:"ưn",method:1},Case{input:"ws",expected:"ứ",method:1},
537 Case{input:"wf",expected:"ừ",method:1},
538 Case{input:"aws",expected:"ắ",method:1},Case{input:"aas",expected:"ấ",method:1},
539 Case{input:"ees",expected:"ế",method:1},Case{input:"oos",expected:"ố",method:1},
540 Case{input:"ows",expected:"ớ",method:1},Case{input:"uws",expected:"ứ",method:1},
541 Case{input:"chaof",expected:"chào",method:1},Case{input:"vieejt",expected:"việt",method:1},
542 Case{input:"tieengs",expected:"tiếng",method:1},Case{input:"quas",expected:"quá",method:1},
543 Case{input:"gias",expected:"giá",method:1},Case{input:"xin",expected:"xin",method:1},
544 Case{input:"tooi",expected:"tôi",method:1},Case{input:"laf",expected:"là",method:1},
545 Case{input:"cos",expected:"có",method:1},Case{input:"khoong",expected:"không",method:1},
546 Case{input:"Vieetj",expected:"Việt",method:1},
547 Case{input:"toans",expected:"toán",method:1},Case{input:"hoaf",expected:"hoà",method:1},
548 Case{input:"thowif",expected:"thời",method:1},Case{input:"cuar",expected:"của",method:1},
549 Case{input:"gif",expected:"gì",method:1},Case{input:"giowf",expected:"giờ",method:1},
550 Case{input:"gioir",expected:"giỏi",method:1},
551 Case{input:"cuowcs",expected:"cước",method:1},Case{input:"dduwowcj",expected:"được",method:1},
552 Case{input:"nguwowif",expected:"người",method:1},
553 Case{input:"asz",expected:"a",method:1},Case{input:"hello",expected:"hello",method:1},
554 Case{input:"hoanf",expected:"hoàn",method:1},Case{input:"huyeenf",expected:"huyền",method:1},
555 Case{input:"nguyeexn",expected:"nguyễn",method:1},Case{input:"ngoaif",expected:"ngoài",method:1},
556 Case{input:"thuowngf",expected:"thường",method:1},
557 Case{input:"d9",expected:"đ",method:2},Case{input:"a6",expected:"â",method:2},
559 Case{input:"a8",expected:"ă",method:2},Case{input:"e6",expected:"ê",method:2},
560 Case{input:"o6",expected:"ô",method:2},Case{input:"o7",expected:"ơ",method:2},
561 Case{input:"u7",expected:"ư",method:2},Case{input:"uo7",expected:"ươ",method:2},
562 Case{input:"vie65t",expected:"việt",method:2},Case{input:"tie61ng",expected:"tiếng",method:2},
563 Case{input:"qua1",expected:"quá",method:2},Case{input:"gia1",expected:"giá",method:2},
564 Case{input:"vie61t",expected:"viết",method:4},Case{input:"vieejt",expected:"việt",method:4},
566 Case{input:"tie61ng",expected:"tiếng",method:4},
567 Case{input:"a6",expected:"â",method:4},Case{input:"dd",expected:"đ",method:4},
568 Case{input:"dd",expected:"đ",method:3},Case{input:"DD",expected:"Đ",method:3},
570 Case{input:"vie^'t",expected:"viết",method:3},Case{input:"nu+o+'c",expected:"nước",method:3},
571 Case{input:"ngu+o+`i",expected:"người",method:3},
572 ];
573
574 let mut failures = Vec::new();
575 for c in &cases {
576 let result = match c.method {
577 1 => convert_telex(c.input, false, true),
578 2 => convert_vni(c.input, false, true),
579 3 => convert_viqr(c.input),
580 4 => convert_teip_vni(c.input, false, true),
581 _ => unreachable!(),
582 };
583 if result != c.expected {
584 failures.push(format!("[m{}] '{}' -> '{}' (expected '{}')", c.method, c.input, result, c.expected));
585 }
586 }
587 assert!(failures.is_empty(), "{} full parity failures:\n{}", failures.len(), failures.join("\n"));
588}
589
590#[test]
591fn comprehensive_vietnamese() {
592 let cases: &[(&str, &str, u8)] = &[
598 ("as","á",1),("af","à",1),("ar","ả",1),("ax","ã",1),("aj","ạ",1),
600 ("es","é",1),("ef","è",1),("er","ẻ",1),("ex","ẽ",1),("ej","ẹ",1),
601 ("is","í",1),("if","ì",1),("ir","ỉ",1),("ix","ĩ",1),("ij","ị",1),
602 ("os","ó",1),("of","ò",1),("or","ỏ",1),("ox","õ",1),("oj","ọ",1),
603 ("us","ú",1),("uf","ù",1),("ur","ủ",1),("ux","ũ",1),("uj","ụ",1),
604 ("ys","ý",1),("yf","ỳ",1),("yr","ỷ",1),("yx","ỹ",1),("yj","ỵ",1),
605
606 ("aw","ă",1),("aws","ắ",1),("awf","ằ",1),("awr","ẳ",1),("awx","ẵ",1),("awj","ặ",1),
609 ("aa","â",1),("aas","ấ",1),("aaf","ầ",1),("aar","ẩ",1),("aax","ẫ",1),("aaj","ậ",1),
611 ("ee","ê",1),("ees","ế",1),("eef","ề",1),("eer","ể",1),("eex","ễ",1),("eej","ệ",1),
613 ("ew","ê",1),("ews","ế",1),("ewf","ề",1),("ewr","ể",1),
614 ("oo","ô",1),("oos","ố",1),("oof","ồ",1),("oor","ổ",1),("oox","ỗ",1),("ooj","ộ",1),
616 ("ow","ơ",1),("ows","ớ",1),("owf","ờ",1),("owr","ở",1),("owx","ỡ",1),("owj","ợ",1),
618 ("uw","ư",1),("uws","ứ",1),("uwf","ừ",1),("uwr","ử",1),("uwx","ữ",1),("uwj","ự",1),
620
621 ("dd","đ",1),
623
624 ("w","ư",1),("ws","ứ",1),("wf","ừ",1),("wr","ử",1),("wx","ữ",1),("wj","ự",1),
626 ("wn","ưn",1),("wt","ưt",1),
627
628 ("ais","ái",1),("aif","ài",1),("air","ải",1),("aix","ãi",1),("aij","ại",1),
631 ("aos","áo",1),("aof","ào",1),("aor","ảo",1),("aox","ão",1),("aoj","ạo",1),
632 ("aus","áu",1),("auf","àu",1),("aur","ảu",1),("aux","ãu",1),("auj","ạu",1),
633 ("aays","ấy",1),("aayf","ầy",1),("aayr","ẩy",1),("aayx","ẫy",1),("aayj","ậy",1),
635 ("ays","áy",1),("ayf","ày",1),("ayr","ảy",1),("ayx","ãy",1),("ayj","ạy",1),
636 ("eos","éo",1),("eof","èo",1),("eor","ẻo",1),("eox","ẽo",1),("eoj","ẹo",1),
638 ("eeus","ếu",1),("eeuf","ều",1),("eeur","ểu",1),("eeux","ễu",1),("eeuj","ệu",1),
639 ("ois","ói",1),("oif","òi",1),("oir","ỏi",1),("oix","õi",1),("oij","ọi",1),
641 ("oois","ối",1),("ooif","ồi",1),("ooir","ổi",1),("ooix","ỗi",1),("ooij","ội",1),
642 ("owis","ới",1),("owif","ời",1),("owir","ởi",1),("owix","ỡi",1),("owij","ợi",1),
643 ("uis","úi",1),("uif","ùi",1),("uir","ủi",1),("uix","ũi",1),("uij","ụi",1),
644 ("uwis","ứi",1),("uwif","ừi",1),("uwir","ửi",1),("uwix","ữi",1),("uwij","ựi",1),
645 ("uwus","ứu",1),("uwuf","ừu",1),("uwur","ửu",1),("uwux","ữu",1),("uwuj","ựu",1),
646
647 ("oans","oán",1),("oanf","oàn",1),("oanr","oản",1),("oanx","oãn",1),("oanj","oạn",1),
649 ("oats","oát",1),("oatf","oàt",1),("oatr","oảt",1),
650 ("oens","oén",1),("oenf","oèn",1),
651 ("uyns","uýn",1),("uynf","uỳn",1),("uynr","uỷn",1),
652
653 ("oas","oá",1),("oaf","oà",1),("oar","oả",1),("oax","oã",1),("oaj","oạ",1),
655 ("oes","oé",1),("oef","oè",1),
656 ("uys","uý",1),("uyf","uỳ",1),("uyr","uỷ",1),("uyx","uỹ",1),("uyj","uỵ",1),
657
658 ("oais","oái",1),("oaif","oài",1),("oair","oải",1),("oaix","oãi",1),("oaij","oại",1),
661 ("oays","oáy",1),("oayf","oày",1),("oayr","oảy",1),("oayx","oãy",1),("oayj","oạy",1),
662 ("uaays","uấy",1),("uaayf","uầy",1),("uaayr","uẩy",1),("uaayx","uẫy",1),("uaayj","uậy",1),
664 ("uyeef","uyề",1),("uyees","uyế",1),("uyeer","uyể",1),("uyeex","uyễ",1),("uyeej","uyệ",1),
666 ("ieef","iề",1),("iees","iế",1),("ieer","iể",1),("ieex","iễ",1),("ieej","iệ",1),
668 ("yeef","yề",1),("yees","yế",1),("yeer","yể",1),("yeex","yễ",1),("yeej","yệ",1),
669 ("uoois","uối",1),("uooif","uồi",1),("uooir","uổi",1),("uooix","uỗi",1),("uooij","uội",1),
671 ("uoiws","ưới",1),("uoiwf","ười",1),("uoiwr","ưởi",1),("uoiwx","ưỡi",1),("uoiwj","ượi",1),
673
674 ("gif","gì",1),("gis","gí",1),("gir","gỉ",1),("gix","gĩ",1),("gij","gị",1),
676 ("gias","giá",1),("giaf","già",1),("giar","giả",1),("giax","giã",1),("giaj","giạ",1),
677 ("gioir","giỏi",1),("gioif","giòi",1),("giois","giói",1),
678 ("quas","quá",1),("quaf","quà",1),("quar","quả",1),("quax","quã",1),("quaj","quạ",1),
679 ("quys","quý",1),("quyf","quỳ",1),("quyr","quỷ",1),("quyx","quỹ",1),("quyj","quỵ",1),
680
681 ("chaof","chào",1),("vieejt","việt",1),("tieengs","tiếng",1),
683 ("khoong","không",1),("cuar","của",1),("cuowcs","cước",1),
684 ("dduwowcj","được",1),("nguwowif","người",1),("thuowngf","thường",1),
685 ("hoanf","hoàn",1),("huyeenf","huyền",1),("nguyeexn","nguyễn",1),
686 ("ngoaif","ngoài",1),("ngoafi","ngoài",1),
687 ("vaya","vây",1),("vayaj","vậy",1),("vayas","vấy",1),("mayas","mấy",1),
688 ("nhana","nhân",1),("khanas","khấn",1),("nhanaa","nhana",1),
690 ("toans","toán",1),("thowif","thời",1),
691 ("xin","xin",1),("tooi","tôi",1),("laf","là",1),("cos","có",1),
692 ("banj","bạn",1),("minhf","mình",1),("nawm","năm",1),("thangs","tháng",1),
693 ("truwowngf","trường",1),("luaatj","luật",1),
694
695 ("nhas","nhá",1),("nghir","nghỉ",1),("gheps","ghép",1),
697 ("truws","trứ",1),("thas","thá",1),("phos","phó",1),
698 ("khas","khá",1),("chays","cháy",1),("ngays","ngáy",1),
699
700 ("ddejp","đẹp",1),("maats","mất",1),("bacs","bác",1),
702 ("ngachs","ngách",1),("banhf","bành",1),
703 ("tins","tín",1),("metj","mẹt",1),
704
705 ("asz","a",1),("awsjz","ă",1),("aasz","â",1),
707
708 ("hello","hello",1),("bacon","bacon",1),("thing","thing",1),("climb","climb",1),
710
711 ("As","Á",1),("Af","À",1),("Aas","Ấ",1),("Aws","Ắ",1),
713 ("Dd","Đ",1),("DD","Đ",1),
714 ("W","Ư",1),("Ws","Ứ",1),
715 ("Vieetj","Việt",1),("Tooi","Tôi",1),
716
717 ("d9","đ",2),("a6","â",2),("a8","ă",2),("e6","ê",2),
719 ("o6","ô",2),("o7","ơ",2),("u7","ư",2),("uo7","ươ",2),
720 ("vie65t","việt",2),("tie61ng","tiếng",2),
721 ("qua1","quá",2),("gia1","giá",2),
722
723 ("vie61t","viết",4),("vieejt","việt",4),
725 ("tie61ng","tiếng",4),("a6","â",4),("dd","đ",4),
726
727 ("dd","đ",3),("DD","Đ",3),
729 ("vie^'t","viết",3),("nu+o+'c","nước",3),("ngu+o+`i","người",3),
730 ];
731
732 let mut failures = Vec::new();
733 for &(input, expected, method) in cases {
734 let result = match method {
735 1 => convert_telex(input, false, true),
736 2 => convert_vni(input, false, true),
737 3 => convert_viqr(input),
738 4 => convert_teip_vni(input, false, true),
739 _ => unreachable!(),
740 };
741 if result != expected {
742 failures.push(format!("[m{}] '{}' -> '{}' (expected '{}')", method, input, result, expected));
743 }
744 }
745 assert!(failures.is_empty(),
746 "{} comprehensive failures (traditional mode):\n{}",
747 failures.len(), failures.join("\n"));
748}