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 {
86 let mut consumed = false;
87 for i in (0..n).rev() {
88 if ls[i].is_vowel {
89 if ls[i].c == lc {
90 if ls[i].variant == 0 { ls[i].variant = 1; if upper { ls[i].upper = true; } consumed = true; break; }
91 if ls[i].variant == 1 { ls[i].variant = 0; break; } }
93 } else { break; }
94 }
95 if consumed { continue; }
96 }
97 if lc == 'w' {
98 if let Some(vi) = last_w_target_index(&ls) {
99 match ls[vi].c {
100 'a' => { ls[vi].variant = 2; continue; }
101 'o' => { ls[vi].variant = 2; if vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; }
102 'u' => { ls[vi].variant = 2; continue; }
103 'e' => { ls[vi].variant = 1; continue; }
104 _ => {}
105 }
106 } else if short_w {
107 ls.push(Letter { c: 'u', is_vowel: true, variant: 2, tone: 0, is_dstroke: false, upper });
109 continue;
110 }
111 }
112 if let Some(t) = telex_tone(lc) {
113 if let Some(vi) = tone_vowel_index(&ls, modern) {
114 ls[vi].tone = t; continue; }
116 }
117 if lc == 'z' {
118 if let Some(vi) = tone_vowel_index(&ls, modern) {
119 if ls[vi].tone != 0 { ls[vi].tone = 0; continue; }
120 }
121 }
122 ls.push(Letter::new(ch, upper));
123 }
124 emit(&ls)
125}
126
127pub fn convert_teip_vni(input: &str, modern: bool, short_w: bool) -> String {
133 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
134 for ch in input.chars() {
135 let lc = ch.to_ascii_lowercase(); let upper = ch.is_ascii_uppercase(); let n = ls.len();
136
137 if let Some(t) = vni_tone(lc) {
139 if let Some(vi) = tone_vowel_index(&ls, modern) { ls[vi].tone = t; continue; }
140 }
141 if lc >= '0' && lc <= '9' {
142 match lc {
143 '6' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'a'|'e'|'o') { ls[vi].variant = 1; continue; } } }
144 '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; } } }
145 '8' => { if let Some(vi) = last_vowel_index(&ls) { if ls[vi].c == 'a' { ls[vi].variant = 2; continue; } } }
146 '9' => { if let Some(di) = last_d_index(&ls) { ls[di].is_dstroke = true; if upper { ls[di].upper = true; } } continue; }
147 '0' => { continue; }
148 _ => {} }
150 }
151
152 if lc == 'd' && n > 0 && ls[n-1].c == 'd' && !ls[n-1].is_vowel && !ls[n-1].is_dstroke
154 { ls[n-1].is_dstroke = true; if upper { ls[n-1].upper = true; } continue; }
155
156 if tables::is_vowel(lc) && matches!(lc, 'a'|'e'|'o') && n > 0 {
160 let mut consumed = false;
161 for i in (0..n).rev() {
162 if ls[i].is_vowel {
163 if ls[i].c == lc {
164 if ls[i].variant == 0 { ls[i].variant = 1; if upper { ls[i].upper = true; } consumed = true; break; }
165 if ls[i].variant == 1 { ls[i].variant = 0; break; } }
167 } else { break; }
168 }
169 if consumed { continue; }
170 }
171
172 if lc == 'w' {
174 if let Some(vi) = last_w_target_index(&ls) {
175 match ls[vi].c {
176 'a' => { ls[vi].variant = 2; continue; }
177 'o' => { ls[vi].variant = 2; if vi > 0 && ls[vi-1].c == 'u' { ls[vi-1].variant = 2; } continue; }
178 'u' => { ls[vi].variant = 2; continue; }
179 'e' => { ls[vi].variant = 1; continue; }
180 _ => {}
181 }
182 } else if short_w {
183 ls.push(Letter { c: 'u', is_vowel: true, variant: 2, tone: 0, is_dstroke: false, upper });
184 continue;
185 }
186 }
187
188 if let Some(t) = telex_tone(lc) {
190 if let Some(vi) = tone_vowel_index(&ls, modern) {
191 ls[vi].tone = t; continue; }
193 }
194 if lc == 'z' {
195 if let Some(vi) = tone_vowel_index(&ls, modern) {
196 if ls[vi].tone != 0 { ls[vi].tone = 0; continue; }
197 }
198 }
199
200 ls.push(Letter::new(ch, upper));
201 }
202 emit(&ls)
203}
204
205pub fn convert_viqr(input: &str) -> String {
212 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
213 for ch in input.chars() {
214 let lc = ch.to_ascii_lowercase(); let n = ls.len();
215
216 match ch {
218 '^' | '(' | '+' => {
219 if let Some(vi) = last_vowel_index(&ls) {
220 match (ch, ls[vi].c) {
221 ('^', 'a' | 'e' | 'o') => { ls[vi].variant = 1; continue; }
222 ('(', 'a') => { ls[vi].variant = 2; continue; }
223 ('+', 'o' | 'u') => { ls[vi].variant = 2; continue; }
224 _ => {} }
226 }
227 ls.push(Letter { c: ch, is_vowel: false, variant: 0, tone: 0, is_dstroke: false, upper: false });
229 continue;
230 }
231 _ => {}
232 }
233
234 match ch {
236 '\'' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 2; continue; } }
237 '`' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 1; continue; } }
238 '?' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 3; continue; } }
239 '~' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 4; continue; } }
240 '.' => { if let Some(vi) = last_vowel_index(&ls) { ls[vi].tone = 5; continue; } }
241 _ => {}
242 }
243
244 if lc == 'd' && n > 0 && ls[n-1].c == 'd' && !ls[n-1].is_vowel && !ls[n-1].is_dstroke {
246 ls[n-1].is_dstroke = true;
247 if ch.is_ascii_uppercase() { ls[n-1].upper = true; }
248 continue;
249 }
250
251 ls.push(Letter::new(ch, ch.is_ascii_uppercase()));
252 }
253 emit(&ls)
254}
255
256pub fn convert_vni(input: &str, modern: bool, _short_w: bool) -> String {
257 let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
258 for ch in input.chars() {
259 let lc = ch.to_ascii_lowercase(); let upper = ch.is_ascii_uppercase();
260 if let Some(t) = vni_tone(lc) {
261 if let Some(vi) = tone_vowel_index(&ls, modern) { ls[vi].tone = t; continue; }
262 }
263 match lc {
264 '6' => { if let Some(vi) = last_vowel_index(&ls) { if matches!(ls[vi].c, 'a'|'e'|'o') { ls[vi].variant = 1; continue; } } }
265 '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; } } }
266 '8' => { if let Some(vi) = last_vowel_index(&ls) { if ls[vi].c == 'a' { ls[vi].variant = 2; continue; } } }
267 '9' => { if let Some(di) = last_d_index(&ls) { ls[di].is_dstroke = true; if upper { ls[di].upper = true; } } continue; }
268 '0' => { continue; }
269 _ => {}
270 }
271 if lc.is_ascii_alphabetic() || (lc.is_ascii_digit() && !ls.is_empty()) { ls.push(Letter::new(ch, upper)); }
272 }
273 emit(&ls)
274}
275
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 fn t(s: &str) -> String { convert_telex(s, true, false) }
282 fn tw(s: &str) -> String { convert_telex(s, true, true) }
283 fn tt(s: &str) -> String { convert_telex(s, false, true) }
285 fn vt(s: &str) -> String { convert_vni(s, false, true) }
286
287 #[test] fn telex_basic() {
288 assert_eq!(t("as"),"á"); assert_eq!(t("af"),"à"); assert_eq!(t("ar"),"ả"); assert_eq!(t("ax"),"ã"); assert_eq!(t("aj"),"ạ");
289 assert_eq!(t("aw"),"ă"); assert_eq!(t("aa"),"â"); assert_eq!(t("ee"),"ê"); assert_eq!(t("oo"),"ô"); assert_eq!(t("ow"),"ơ");
290 assert_eq!(t("uw"),"ư"); assert_eq!(t("dd"),"đ");
291 }
292 #[test] fn telex_compound() {
293 assert_eq!(t("aws"),"ắ"); assert_eq!(t("aas"),"ấ"); assert_eq!(t("ees"),"ế"); assert_eq!(t("oos"),"ố"); assert_eq!(t("ows"),"ớ"); assert_eq!(t("uws"),"ứ");
294 }
295 #[test] fn telex_words() {
296 assert_eq!(t("xin"),"xin"); assert_eq!(t("tooi"),"tôi"); assert_eq!(t("laf"),"là"); assert_eq!(t("cos"),"có");
297 assert_eq!(t("khoong"),"không"); assert_eq!(t("tieengs"),"tiếng"); assert_eq!(t("Vieetj"),"Việt");
298 }
299 #[test] fn telex_uow() {
300 assert_eq!(t("uow"),"ươ"); assert_eq!(t("cuowcs"),"cước"); assert_eq!(t("dduwowcj"),"được"); assert_eq!(t("nguwowif"),"người");
301 }
302 #[test] fn telex_gi_qu() {
303 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á");
304 }
305 #[test] fn telex_tone_placement() {
306 assert_eq!(t("chafo"),"chào"); assert_eq!(t("toans"),"toán"); assert_eq!(t("hoaf"),"hoà"); assert_eq!(t("thowif"),"thời");
307 assert_eq!(t("cuar"),"của"); assert_eq!(t("thoix"),"thõi");
308 assert_eq!(t("ofa"),"òa"); assert_eq!(t("oaf"),"oà"); }
312 #[test] fn telex_english() {
313 assert_eq!(t("hello"),"hello");
314 assert_eq!(t("wood"),"wôd");
315 assert_eq!(t("reboot"),"rebôt");
316 }
317 #[test] fn telex_z() { assert_eq!(t("asz"),"a"); }
318 #[test] fn telex_w_standalone() {
319 assert_eq!(t("w"),"w"); assert_eq!(tw("w"),"ư"); assert_eq!(tw("ws"),"ứ");
322 assert_eq!(tw("wf"),"ừ");
323 assert_eq!(tw("wn"),"ưn");
324 }
325
326 #[test] fn telex_dd_traditional() {
329 assert_eq!(tt("dd"),"đ"); assert_eq!(tt("DD"),"Đ");
330 assert_eq!(tt("Dd"),"Đ"); assert_eq!(tt("dD"),"Đ");
331 }
332 #[test] fn telex_marks_traditional() {
333 assert_eq!(tt("aa"),"â"); assert_eq!(tt("aw"),"ă");
334 assert_eq!(tt("ee"),"ê"); assert_eq!(tt("ew"),"ê");
335 assert_eq!(tt("oo"),"ô"); assert_eq!(tt("ow"),"ơ");
336 assert_eq!(tt("uw"),"ư"); assert_eq!(tt("uow"),"ươ");
337 }
338 #[test] fn telex_w_traditional() {
339 assert_eq!(tt("w"),"ư"); assert_eq!(tt("W"),"Ư");
340 assert_eq!(tt("wn"),"ưn"); assert_eq!(tt("ws"),"ứ"); assert_eq!(tt("wf"),"ừ");
341 }
342 #[test] fn telex_compound_traditional() {
343 assert_eq!(tt("aws"),"ắ"); assert_eq!(tt("aas"),"ấ"); assert_eq!(tt("ees"),"ế");
344 assert_eq!(tt("oos"),"ố"); assert_eq!(tt("ows"),"ớ"); assert_eq!(tt("uws"),"ứ");
345 }
346 #[test] fn telex_words_traditional() {
347 assert_eq!(tt("chaof"),"chào"); assert_eq!(tt("vieejt"),"việt");
348 assert_eq!(tt("tieengs"),"tiếng"); assert_eq!(tt("quas"),"quá");
349 assert_eq!(tt("gias"),"giá"); assert_eq!(tt("xin"),"xin");
350 assert_eq!(tt("tooi"),"tôi"); assert_eq!(tt("laf"),"là"); assert_eq!(tt("cos"),"có");
351 assert_eq!(tt("khoong"),"không"); assert_eq!(tt("Vieetj"),"Việt");
352 }
353 #[test] fn telex_tone_traditional() {
354 assert_eq!(tt("toans"),"toán"); assert_eq!(tt("hoaf"),"hoà");
356 assert_eq!(tt("thowif"),"thời"); assert_eq!(tt("cuar"),"của");
357 }
358 #[test] fn telex_gi_qu_traditional() {
359 assert_eq!(tt("gif"),"gì"); assert_eq!(tt("giowf"),"giờ");
360 assert_eq!(tt("gioir"),"giỏi");
361 }
362 #[test] fn telex_uow_traditional() {
363 assert_eq!(tt("cuowcs"),"cước"); assert_eq!(tt("dduwowcj"),"được");
364 assert_eq!(tt("nguwowif"),"người");
365 }
366 #[test] fn telex_z_traditional() { assert_eq!(tt("asz"),"a"); }
367 #[test] fn telex_english_traditional() { assert_eq!(tt("hello"),"hello"); }
368 #[test] fn telex_more_traditional() {
369 assert_eq!(tt("hoanf"),"hoàn"); assert_eq!(tt("huyeenf"),"huyền");
370 assert_eq!(tt("nguyeexn"),"nguyễn"); assert_eq!(tt("ngoaif"),"ngoài");
371 assert_eq!(tt("thuowngf"),"thường");
372 }
373 #[test] fn vni_marks_traditional() {
374 assert_eq!(vt("d9"),"đ"); assert_eq!(vt("a6"),"â"); assert_eq!(vt("a8"),"ă");
375 assert_eq!(vt("e6"),"ê"); assert_eq!(vt("o6"),"ô"); assert_eq!(vt("o7"),"ơ");
376 assert_eq!(vt("u7"),"ư"); assert_eq!(vt("uo7"),"ươ");
377 }
378 #[test] fn vni_words_traditional() {
379 assert_eq!(vt("vie65t"),"việt"); assert_eq!(vt("tie61ng"),"tiếng");
380 assert_eq!(vt("qua1"),"quá"); assert_eq!(vt("gia1"),"giá");
381 }
382
383 #[test] fn telex_tone_replace() {
385 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");}
391 #[test] fn teipvni_tone_replace() {
392 assert_eq!(tv("saosf"),"sào"); assert_eq!(tv("sao1f"),"sào"); }
395
396 #[test] fn telex_triple_toggle() {
398 assert_eq!(tt("rooot"),"root"); assert_eq!(tt("roo"),"rô");
400 assert_eq!(tt("aa"),"â"); assert_eq!(tt("aaa"),"aa"); assert_eq!(tt("aaaa"),"aâ"); assert_eq!(tt("ee"),"ê");
404 assert_eq!(tt("eee"),"ee");
405 assert_eq!(tt("oooo"),"oô"); }
407
408 #[test] fn telex_double_vowel_across_y() {
410 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");
420 assert_eq!(tt("sauas"),"sấu");
421 assert_eq!(tt("dauaf"),"dầu");
422 assert_eq!(tt("ee"),"ê");
424 assert_eq!(tt("aay"),"ây");
426 }
427
428 #[test] fn telex_w_target() {
430 assert_eq!(tt("toiw"),"tơi");
432 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");
438 assert_eq!(tt("nguoiws"),"ngưới");
439 assert_eq!(tt("muoiwf"),"mười");
441 assert_eq!(tt("tuoiws"),"tưới");
443 assert_eq!(tt("tuooir"),"tuổi");
445 assert_eq!(tt("tuooif"),"tuồi");
446 }
447 #[test] fn telex_toan() {
448 assert_eq!(tt("toanf"),"toàn");
450 assert_eq!(t("toanf"),"toàn");
451 assert_eq!(tt("hoanf"),"hoàn");
452 assert_eq!(t("hoanf"),"hoàn");
453 assert_eq!(t("oaf"),"oà"); assert_eq!(t("ofa"),"òa"); assert_eq!(tt("oaf"),"oà");
458 assert_eq!(tt("ofa"),"òa"); }
460
461 fn tv(s: &str) -> String { convert_teip_vni(s, false, true) }
463
464 #[test] fn teipvni_telex_words() {
465 assert_eq!(tv("vieejt"),"việt"); assert_eq!(tv("dd"),"đ");
466 assert_eq!(tv("aa"),"â"); assert_eq!(tv("aw"),"ă");
467 assert_eq!(tv("ee"),"ê"); assert_eq!(tv("oo"),"ô");
468 assert_eq!(tv("ow"),"ơ"); assert_eq!(tv("uw"),"ư");
469 assert_eq!(tv("uow"),"ươ"); assert_eq!(tv("w"),"ư");
470 }
471 #[test] fn teipvni_vni_marks() {
472 assert_eq!(tv("a6"),"â"); assert_eq!(tv("a8"),"ă");
473 assert_eq!(tv("e6"),"ê"); assert_eq!(tv("o6"),"ô");
474 assert_eq!(tv("o7"),"ơ"); assert_eq!(tv("u7"),"ư");
475 assert_eq!(tv("uo7"),"ươ"); assert_eq!(tv("d9"),"đ");
476 }
477 #[test] fn teipvni_numbers() {
478 assert_eq!(tv("qua1"),"quá"); assert_eq!(tv("gia1"),"giá");
480 assert_eq!(tv("vie65t"),"việt"); assert_eq!(tv("tie61ng"),"tiếng");
481 }
482 #[test] fn teipvni_mixed() {
483 assert_eq!(tv("vie6t"),"viêt"); assert_eq!(tv("vie6ts"),"viết"); assert_eq!(tv("tie6ngs"),"tiếng");
487 }
488
489 fn viqr(s: &str) -> String { convert_viqr(s) }
491
492 #[test] fn viqr_marks() {
493 assert_eq!(viqr("a^"),"â"); assert_eq!(viqr("a("),"ă");
494 assert_eq!(viqr("e^"),"ê"); assert_eq!(viqr("o^"),"ô");
495 assert_eq!(viqr("o+"),"ơ"); assert_eq!(viqr("u+"),"ư");
496 assert_eq!(viqr("dd"),"đ");
497 }
498 #[test] fn viqr_tones() {
499 assert_eq!(viqr("a'"),"á"); assert_eq!(viqr("a`"),"à");
500 assert_eq!(viqr("a?"),"ả"); assert_eq!(viqr("a~"),"ã");
501 assert_eq!(viqr("a."),"ạ");
502 }
503 #[test] fn viqr_words() {
504 assert_eq!(viqr("vie^'t"),"viết");
505 assert_eq!(viqr("nu+o+'c"),"nước");
506 assert_eq!(viqr("ngu+o+`i"),"người");
507 }
508 #[test] fn viqr_dd() {
509 assert_eq!(viqr("dd"),"đ"); assert_eq!(viqr("DD"),"Đ");
510 }
511 #[test] fn viqr_english() { assert_eq!(viqr("hello"),"hello"); }
512}
513
514#[test]
515fn full_parity() {
516 struct Case { input: &'static str, expected: &'static str, method: u8 }
520 let cases = [
522 Case{input:"dd",expected:"đ",method:1},Case{input:"DD",expected:"Đ",method:1},
524 Case{input:"Dd",expected:"Đ",method:1},Case{input:"dD",expected:"Đ",method:1},
525 Case{input:"aa",expected:"â",method:1},Case{input:"aw",expected:"ă",method:1},
526 Case{input:"ee",expected:"ê",method:1},Case{input:"ew",expected:"ê",method:1},
527 Case{input:"oo",expected:"ô",method:1},Case{input:"ow",expected:"ơ",method:1},
528 Case{input:"uw",expected:"ư",method:1},Case{input:"uow",expected:"ươ",method:1},
529 Case{input:"w",expected:"ư",method:1},Case{input:"W",expected:"Ư",method:1},
530 Case{input:"wn",expected:"ưn",method:1},Case{input:"ws",expected:"ứ",method:1},
531 Case{input:"wf",expected:"ừ",method:1},
532 Case{input:"aws",expected:"ắ",method:1},Case{input:"aas",expected:"ấ",method:1},
533 Case{input:"ees",expected:"ế",method:1},Case{input:"oos",expected:"ố",method:1},
534 Case{input:"ows",expected:"ớ",method:1},Case{input:"uws",expected:"ứ",method:1},
535 Case{input:"chaof",expected:"chào",method:1},Case{input:"vieejt",expected:"việt",method:1},
536 Case{input:"tieengs",expected:"tiếng",method:1},Case{input:"quas",expected:"quá",method:1},
537 Case{input:"gias",expected:"giá",method:1},Case{input:"xin",expected:"xin",method:1},
538 Case{input:"tooi",expected:"tôi",method:1},Case{input:"laf",expected:"là",method:1},
539 Case{input:"cos",expected:"có",method:1},Case{input:"khoong",expected:"không",method:1},
540 Case{input:"Vieetj",expected:"Việt",method:1},
541 Case{input:"toans",expected:"toán",method:1},Case{input:"hoaf",expected:"hoà",method:1},
542 Case{input:"thowif",expected:"thời",method:1},Case{input:"cuar",expected:"của",method:1},
543 Case{input:"gif",expected:"gì",method:1},Case{input:"giowf",expected:"giờ",method:1},
544 Case{input:"gioir",expected:"giỏi",method:1},
545 Case{input:"cuowcs",expected:"cước",method:1},Case{input:"dduwowcj",expected:"được",method:1},
546 Case{input:"nguwowif",expected:"người",method:1},
547 Case{input:"asz",expected:"a",method:1},Case{input:"hello",expected:"hello",method:1},
548 Case{input:"hoanf",expected:"hoàn",method:1},Case{input:"huyeenf",expected:"huyền",method:1},
549 Case{input:"nguyeexn",expected:"nguyễn",method:1},Case{input:"ngoaif",expected:"ngoài",method:1},
550 Case{input:"thuowngf",expected:"thường",method:1},
551 Case{input:"d9",expected:"đ",method:2},Case{input:"a6",expected:"â",method:2},
553 Case{input:"a8",expected:"ă",method:2},Case{input:"e6",expected:"ê",method:2},
554 Case{input:"o6",expected:"ô",method:2},Case{input:"o7",expected:"ơ",method:2},
555 Case{input:"u7",expected:"ư",method:2},Case{input:"uo7",expected:"ươ",method:2},
556 Case{input:"vie65t",expected:"việt",method:2},Case{input:"tie61ng",expected:"tiếng",method:2},
557 Case{input:"qua1",expected:"quá",method:2},Case{input:"gia1",expected:"giá",method:2},
558 Case{input:"vie61t",expected:"viết",method:4},Case{input:"vieejt",expected:"việt",method:4},
560 Case{input:"tie61ng",expected:"tiếng",method:4},
561 Case{input:"a6",expected:"â",method:4},Case{input:"dd",expected:"đ",method:4},
562 Case{input:"dd",expected:"đ",method:3},Case{input:"DD",expected:"Đ",method:3},
564 Case{input:"vie^'t",expected:"viết",method:3},Case{input:"nu+o+'c",expected:"nước",method:3},
565 Case{input:"ngu+o+`i",expected:"người",method:3},
566 ];
567
568 let mut failures = Vec::new();
569 for c in &cases {
570 let result = match c.method {
571 1 => convert_telex(c.input, false, true),
572 2 => convert_vni(c.input, false, true),
573 3 => convert_viqr(c.input),
574 4 => convert_teip_vni(c.input, false, true),
575 _ => unreachable!(),
576 };
577 if result != c.expected {
578 failures.push(format!("[m{}] '{}' -> '{}' (expected '{}')", c.method, c.input, result, c.expected));
579 }
580 }
581 assert!(failures.is_empty(), "{} full parity failures:\n{}", failures.len(), failures.join("\n"));
582}
583
584#[test]
585fn comprehensive_vietnamese() {
586 let cases: &[(&str, &str, u8)] = &[
592 ("as","á",1),("af","à",1),("ar","ả",1),("ax","ã",1),("aj","ạ",1),
594 ("es","é",1),("ef","è",1),("er","ẻ",1),("ex","ẽ",1),("ej","ẹ",1),
595 ("is","í",1),("if","ì",1),("ir","ỉ",1),("ix","ĩ",1),("ij","ị",1),
596 ("os","ó",1),("of","ò",1),("or","ỏ",1),("ox","õ",1),("oj","ọ",1),
597 ("us","ú",1),("uf","ù",1),("ur","ủ",1),("ux","ũ",1),("uj","ụ",1),
598 ("ys","ý",1),("yf","ỳ",1),("yr","ỷ",1),("yx","ỹ",1),("yj","ỵ",1),
599
600 ("aw","ă",1),("aws","ắ",1),("awf","ằ",1),("awr","ẳ",1),("awx","ẵ",1),("awj","ặ",1),
603 ("aa","â",1),("aas","ấ",1),("aaf","ầ",1),("aar","ẩ",1),("aax","ẫ",1),("aaj","ậ",1),
605 ("ee","ê",1),("ees","ế",1),("eef","ề",1),("eer","ể",1),("eex","ễ",1),("eej","ệ",1),
607 ("ew","ê",1),("ews","ế",1),("ewf","ề",1),("ewr","ể",1),
608 ("oo","ô",1),("oos","ố",1),("oof","ồ",1),("oor","ổ",1),("oox","ỗ",1),("ooj","ộ",1),
610 ("ow","ơ",1),("ows","ớ",1),("owf","ờ",1),("owr","ở",1),("owx","ỡ",1),("owj","ợ",1),
612 ("uw","ư",1),("uws","ứ",1),("uwf","ừ",1),("uwr","ử",1),("uwx","ữ",1),("uwj","ự",1),
614
615 ("dd","đ",1),
617
618 ("w","ư",1),("ws","ứ",1),("wf","ừ",1),("wr","ử",1),("wx","ữ",1),("wj","ự",1),
620 ("wn","ưn",1),("wt","ưt",1),
621
622 ("ais","ái",1),("aif","ài",1),("air","ải",1),("aix","ãi",1),("aij","ại",1),
625 ("aos","áo",1),("aof","ào",1),("aor","ảo",1),("aox","ão",1),("aoj","ạo",1),
626 ("aus","áu",1),("auf","àu",1),("aur","ảu",1),("aux","ãu",1),("auj","ạu",1),
627 ("aays","ấy",1),("aayf","ầy",1),("aayr","ẩy",1),("aayx","ẫy",1),("aayj","ậy",1),
629 ("ays","áy",1),("ayf","ày",1),("ayr","ảy",1),("ayx","ãy",1),("ayj","ạy",1),
630 ("eos","éo",1),("eof","èo",1),("eor","ẻo",1),("eox","ẽo",1),("eoj","ẹo",1),
632 ("eeus","ếu",1),("eeuf","ều",1),("eeur","ểu",1),("eeux","ễu",1),("eeuj","ệu",1),
633 ("ois","ói",1),("oif","òi",1),("oir","ỏi",1),("oix","õi",1),("oij","ọi",1),
635 ("oois","ối",1),("ooif","ồi",1),("ooir","ổi",1),("ooix","ỗi",1),("ooij","ội",1),
636 ("owis","ới",1),("owif","ời",1),("owir","ởi",1),("owix","ỡi",1),("owij","ợi",1),
637 ("uis","úi",1),("uif","ùi",1),("uir","ủi",1),("uix","ũi",1),("uij","ụi",1),
638 ("uwis","ứi",1),("uwif","ừi",1),("uwir","ửi",1),("uwix","ữi",1),("uwij","ựi",1),
639 ("uwus","ứu",1),("uwuf","ừu",1),("uwur","ửu",1),("uwux","ữu",1),("uwuj","ựu",1),
640
641 ("oans","oán",1),("oanf","oàn",1),("oanr","oản",1),("oanx","oãn",1),("oanj","oạn",1),
643 ("oats","oát",1),("oatf","oàt",1),("oatr","oảt",1),
644 ("oens","oén",1),("oenf","oèn",1),
645 ("uyns","uýn",1),("uynf","uỳn",1),("uynr","uỷn",1),
646
647 ("oas","oá",1),("oaf","oà",1),("oar","oả",1),("oax","oã",1),("oaj","oạ",1),
649 ("oes","oé",1),("oef","oè",1),
650 ("uys","uý",1),("uyf","uỳ",1),("uyr","uỷ",1),("uyx","uỹ",1),("uyj","uỵ",1),
651
652 ("oais","oái",1),("oaif","oài",1),("oair","oải",1),("oaix","oãi",1),("oaij","oại",1),
655 ("oays","oáy",1),("oayf","oày",1),("oayr","oảy",1),("oayx","oãy",1),("oayj","oạy",1),
656 ("uaays","uấy",1),("uaayf","uầy",1),("uaayr","uẩy",1),("uaayx","uẫy",1),("uaayj","uậy",1),
658 ("uyeef","uyề",1),("uyees","uyế",1),("uyeer","uyể",1),("uyeex","uyễ",1),("uyeej","uyệ",1),
660 ("ieef","iề",1),("iees","iế",1),("ieer","iể",1),("ieex","iễ",1),("ieej","iệ",1),
662 ("yeef","yề",1),("yees","yế",1),("yeer","yể",1),("yeex","yễ",1),("yeej","yệ",1),
663 ("uoois","uối",1),("uooif","uồi",1),("uooir","uổi",1),("uooix","uỗi",1),("uooij","uội",1),
665 ("uoiws","ưới",1),("uoiwf","ười",1),("uoiwr","ưởi",1),("uoiwx","ưỡi",1),("uoiwj","ượi",1),
667
668 ("gif","gì",1),("gis","gí",1),("gir","gỉ",1),("gix","gĩ",1),("gij","gị",1),
670 ("gias","giá",1),("giaf","già",1),("giar","giả",1),("giax","giã",1),("giaj","giạ",1),
671 ("gioir","giỏi",1),("gioif","giòi",1),("giois","giói",1),
672 ("quas","quá",1),("quaf","quà",1),("quar","quả",1),("quax","quã",1),("quaj","quạ",1),
673 ("quys","quý",1),("quyf","quỳ",1),("quyr","quỷ",1),("quyx","quỹ",1),("quyj","quỵ",1),
674
675 ("chaof","chào",1),("vieejt","việt",1),("tieengs","tiếng",1),
677 ("khoong","không",1),("cuar","của",1),("cuowcs","cước",1),
678 ("dduwowcj","được",1),("nguwowif","người",1),("thuowngf","thường",1),
679 ("hoanf","hoàn",1),("huyeenf","huyền",1),("nguyeexn","nguyễn",1),
680 ("ngoaif","ngoài",1),("ngoafi","ngoài",1),
681 ("vaya","vây",1),("vayaj","vậy",1),("vayas","vấy",1),("mayas","mấy",1),
682 ("toans","toán",1),("thowif","thời",1),
683 ("xin","xin",1),("tooi","tôi",1),("laf","là",1),("cos","có",1),
684 ("banj","bạn",1),("minhf","mình",1),("nawm","năm",1),("thangs","tháng",1),
685 ("truwowngf","trường",1),("luaatj","luật",1),
686
687 ("nhas","nhá",1),("nghir","nghỉ",1),("gheps","ghép",1),
689 ("truws","trứ",1),("thas","thá",1),("phos","phó",1),
690 ("khas","khá",1),("chays","cháy",1),("ngays","ngáy",1),
691
692 ("ddejp","đẹp",1),("maats","mất",1),("bacs","bác",1),
694 ("ngachs","ngách",1),("banhf","bành",1),
695 ("tins","tín",1),("metj","mẹt",1),
696
697 ("asz","a",1),("awsjz","ă",1),("aasz","â",1),
699
700 ("hello","hello",1),("bacon","bacon",1),("thing","thing",1),("climb","climb",1),
702
703 ("As","Á",1),("Af","À",1),("Aas","Ấ",1),("Aws","Ắ",1),
705 ("Dd","Đ",1),("DD","Đ",1),
706 ("W","Ư",1),("Ws","Ứ",1),
707 ("Vieetj","Việt",1),("Tooi","Tôi",1),
708
709 ("d9","đ",2),("a6","â",2),("a8","ă",2),("e6","ê",2),
711 ("o6","ô",2),("o7","ơ",2),("u7","ư",2),("uo7","ươ",2),
712 ("vie65t","việt",2),("tie61ng","tiếng",2),
713 ("qua1","quá",2),("gia1","giá",2),
714
715 ("vie61t","viết",4),("vieejt","việt",4),
717 ("tie61ng","tiếng",4),("a6","â",4),("dd","đ",4),
718
719 ("dd","đ",3),("DD","Đ",3),
721 ("vie^'t","viết",3),("nu+o+'c","nước",3),("ngu+o+`i","người",3),
722 ];
723
724 let mut failures = Vec::new();
725 for &(input, expected, method) in cases {
726 let result = match method {
727 1 => convert_telex(input, false, true),
728 2 => convert_vni(input, false, true),
729 3 => convert_viqr(input),
730 4 => convert_teip_vni(input, false, true),
731 _ => unreachable!(),
732 };
733 if result != expected {
734 failures.push(format!("[m{}] '{}' -> '{}' (expected '{}')", method, input, result, expected));
735 }
736 }
737 assert!(failures.is_empty(),
738 "{} comprehensive failures (traditional mode):\n{}",
739 failures.len(), failures.join("\n"));
740}