use crate::tables;
#[derive(Clone, Copy, Default)]
struct Letter {
c: char,
is_vowel: bool,
variant: u8,
tone: u8,
is_dstroke: bool,
upper: bool,
from_w: bool,
}
impl Letter {
fn new(c: char, upper: bool) -> Self {
let lc = c.to_ascii_lowercase();
Letter {
c: lc,
is_vowel: tables::is_vowel(lc),
variant: 0,
tone: 0,
is_dstroke: false,
upper,
from_w: false,
}
}
}
fn emit(ls: &[Letter]) -> String {
let mut out = String::with_capacity(ls.len() * 3);
for lt in ls {
if lt.is_dstroke {
out.push_str(if lt.upper {
tables::D_STROKE_UP
} else {
tables::D_STROKE_LO
});
} else if lt.is_vowel {
out.push_str(tables::vowel_utf8(lt.c, lt.variant, lt.tone, lt.upper));
} else {
out.push(if lt.upper {
lt.c.to_ascii_uppercase()
} else {
lt.c
});
}
}
out
}
fn last_vowel_index(ls: &[Letter]) -> Option<usize> {
ls.iter().rposition(|lt| lt.is_vowel)
}
fn last_d_index(ls: &[Letter]) -> Option<usize> {
ls.iter()
.rposition(|lt| lt.c == 'd' && !lt.is_vowel && !lt.is_dstroke)
}
fn last_w_target_index(ls: &[Letter]) -> Option<usize> {
ls.iter()
.rposition(|lt| lt.is_vowel && matches!(lt.c, 'a' | 'o' | 'u' | 'e'))
}
fn tone_vowel_index(ls: &[Letter], _modern: bool) -> Option<usize> {
let end = last_vowel_index(ls)?;
let mut start = end;
while start > 0 && ls[start - 1].is_vowel {
start -= 1;
}
if start < end {
if ls[start].c == 'u' && start > 0 && ls[start - 1].c == 'q' {
start += 1;
} else if ls[start].c == 'i' && start > 0 && ls[start - 1].c == 'g' {
start += 1;
}
}
if start > end {
start = end;
}
for i in (start..=end).rev() {
if ls[i].variant != 0 {
return Some(i);
}
}
let count = end - start + 1;
if count == 1 {
return Some(start);
}
if count >= 3 {
if ls[start].c == 'u'
&& start + 2 <= end
&& ls[start + 1].c == 'y'
&& ls[start + 2].c == 'e'
{
return Some(start + 2);
}
return Some(start + 1);
}
if end + 1 < ls.len() {
return Some(end);
}
let (c1, c2) = (ls[start].c, ls[end].c);
if (c1 == 'o' && c2 == 'a') || (c1 == 'o' && c2 == 'e') || (c1 == 'u' && c2 == 'y') {
return Some(end); }
Some(start) }
fn telex_tone(c: char) -> Option<u8> {
match c.to_ascii_lowercase() {
's' => Some(2),
'f' => Some(1),
'r' => Some(3),
'x' => Some(4),
'j' => Some(5),
_ => None,
}
}
fn vni_tone(c: char) -> Option<u8> {
match c {
'1' => Some(2),
'2' => Some(1),
'3' => Some(3),
'4' => Some(4),
'5' => Some(5),
_ => None,
}
}
pub fn convert_telex(input: &str, modern: bool, short_w: bool) -> String {
let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
for ch in input.chars() {
let lc = ch.to_ascii_lowercase();
let upper = ch.is_ascii_uppercase();
let n = ls.len();
if lc == 'd' && n > 0 && ls[n - 1].c == 'd' && !ls[n - 1].is_vowel && !ls[n - 1].is_dstroke
{
ls[n - 1].is_dstroke = true;
if upper {
ls[n - 1].upper = true;
}
continue;
}
if tables::is_vowel(lc) && matches!(lc, 'a' | 'e' | 'o') && n > 0 {
let mut consumed = false;
if let Some(vi) = last_vowel_index(&ls) {
for i in (0..=vi).rev() {
if ls[i].is_vowel {
if ls[i].c == lc {
if ls[i].variant == 0 {
ls[i].variant = 1;
if upper {
ls[i].upper = true;
}
consumed = true;
break;
}
if ls[i].variant == 1 {
ls[i].variant = 0;
break;
} }
} else {
break;
}
}
}
if consumed {
continue;
}
}
if lc == 'w' {
if let Some(vi) = last_w_target_index(&ls) {
match ls[vi].c {
'a' => {
ls[vi].variant = if ls[vi].variant == 2 { 0 } else { 2 };
continue;
}
'o' => {
if ls[vi].variant == 2 {
ls[vi].variant = 0;
if vi > 0 && ls[vi - 1].c == 'u' && ls[vi - 1].variant == 2 {
ls[vi - 1].variant = 0;
}
} else {
ls[vi].variant = 2;
if vi > 0 && ls[vi - 1].c == 'u' {
ls[vi - 1].variant = 2;
}
}
continue;
}
'u' => {
if ls[vi].variant == 2 {
if ls[vi].from_w {
ls[vi].c = 'w';
ls[vi].is_vowel = false;
ls[vi].from_w = false;
}
ls[vi].variant = 0;
} else {
ls[vi].variant = 2;
}
continue;
}
'e' => {
ls[vi].variant = if ls[vi].variant == 1 { 0 } else { 1 };
continue;
}
_ => {}
}
} else if short_w && !ls.last().map_or(false, |lt| lt.c == 'w' && !lt.is_vowel) {
ls.push(Letter {
c: 'u',
is_vowel: true,
variant: 2,
tone: 0,
is_dstroke: false,
upper,
from_w: true,
});
continue;
}
}
if let Some(t) = telex_tone(lc) {
if let Some(vi) = tone_vowel_index(&ls, modern) {
ls[vi].tone = t;
continue; }
}
if lc == 'z' {
if let Some(vi) = tone_vowel_index(&ls, modern) {
if ls[vi].tone != 0 {
ls[vi].tone = 0;
continue;
}
}
}
ls.push(Letter::new(ch, upper));
}
emit(&ls)
}
pub fn convert_teip_vni(input: &str, modern: bool, short_w: bool) -> String {
let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
for ch in input.chars() {
let lc = ch.to_ascii_lowercase();
let upper = ch.is_ascii_uppercase();
let n = ls.len();
if let Some(t) = vni_tone(lc) {
if let Some(vi) = tone_vowel_index(&ls, modern) {
ls[vi].tone = t;
continue;
}
}
if lc >= '0' && lc <= '9' {
match lc {
'6' => {
if let Some(vi) = last_vowel_index(&ls) {
if matches!(ls[vi].c, 'a' | 'e' | 'o') {
ls[vi].variant = 1;
continue;
}
}
}
'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;
}
}
}
'8' => {
if let Some(vi) = last_vowel_index(&ls) {
if ls[vi].c == 'a' {
ls[vi].variant = 2;
continue;
}
}
}
'9' => {
if let Some(di) = last_d_index(&ls) {
ls[di].is_dstroke = true;
if upper {
ls[di].upper = true;
}
}
continue;
}
'0' => {
continue;
}
_ => {} }
}
if lc == 'd' && n > 0 && ls[n - 1].c == 'd' && !ls[n - 1].is_vowel && !ls[n - 1].is_dstroke
{
ls[n - 1].is_dstroke = true;
if upper {
ls[n - 1].upper = true;
}
continue;
}
if tables::is_vowel(lc) && matches!(lc, 'a' | 'e' | 'o') && n > 0 {
let mut consumed = false;
if let Some(vi) = last_vowel_index(&ls) {
for i in (0..=vi).rev() {
if ls[i].is_vowel {
if ls[i].c == lc {
if ls[i].variant == 0 {
ls[i].variant = 1;
if upper {
ls[i].upper = true;
}
consumed = true;
break;
}
if ls[i].variant == 1 {
ls[i].variant = 0;
break;
} }
} else {
break;
}
}
}
if consumed {
continue;
}
}
if lc == 'w' {
if let Some(vi) = last_w_target_index(&ls) {
match ls[vi].c {
'a' => {
ls[vi].variant = if ls[vi].variant == 2 { 0 } else { 2 };
continue;
}
'o' => {
if ls[vi].variant == 2 {
ls[vi].variant = 0;
if vi > 0 && ls[vi - 1].c == 'u' && ls[vi - 1].variant == 2 {
ls[vi - 1].variant = 0;
}
} else {
ls[vi].variant = 2;
if vi > 0 && ls[vi - 1].c == 'u' {
ls[vi - 1].variant = 2;
}
}
continue;
}
'u' => {
if ls[vi].variant == 2 {
if ls[vi].from_w {
ls[vi].c = 'w';
ls[vi].is_vowel = false;
ls[vi].from_w = false;
}
ls[vi].variant = 0;
} else {
ls[vi].variant = 2;
}
continue;
}
'e' => {
ls[vi].variant = if ls[vi].variant == 1 { 0 } else { 1 };
continue;
}
_ => {}
}
} else if short_w && !ls.last().map_or(false, |lt| lt.c == 'w' && !lt.is_vowel) {
ls.push(Letter {
c: 'u',
is_vowel: true,
variant: 2,
tone: 0,
is_dstroke: false,
upper,
from_w: true,
});
continue;
}
}
if let Some(t) = telex_tone(lc) {
if let Some(vi) = tone_vowel_index(&ls, modern) {
ls[vi].tone = t;
continue; }
}
if lc == 'z' {
if let Some(vi) = tone_vowel_index(&ls, modern) {
if ls[vi].tone != 0 {
ls[vi].tone = 0;
continue;
}
}
}
ls.push(Letter::new(ch, upper));
}
emit(&ls)
}
pub fn convert_viqr(input: &str) -> String {
let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
for ch in input.chars() {
let lc = ch.to_ascii_lowercase();
let n = ls.len();
match ch {
'^' | '(' | '+' => {
if let Some(vi) = last_vowel_index(&ls) {
match (ch, ls[vi].c) {
('^', 'a' | 'e' | 'o') => {
ls[vi].variant = 1;
continue;
}
('(', 'a') => {
ls[vi].variant = 2;
continue;
}
('+', 'o' | 'u') => {
ls[vi].variant = 2;
continue;
}
_ => {} }
}
ls.push(Letter {
c: ch,
is_vowel: false,
variant: 0,
tone: 0,
is_dstroke: false,
upper: false,
from_w: false,
});
continue;
}
_ => {}
}
match ch {
'\'' => {
if let Some(vi) = last_vowel_index(&ls) {
ls[vi].tone = 2;
continue;
}
}
'`' => {
if let Some(vi) = last_vowel_index(&ls) {
ls[vi].tone = 1;
continue;
}
}
'?' => {
if let Some(vi) = last_vowel_index(&ls) {
ls[vi].tone = 3;
continue;
}
}
'~' => {
if let Some(vi) = last_vowel_index(&ls) {
ls[vi].tone = 4;
continue;
}
}
'.' => {
if let Some(vi) = last_vowel_index(&ls) {
ls[vi].tone = 5;
continue;
}
}
_ => {}
}
if lc == 'd' && n > 0 && ls[n - 1].c == 'd' && !ls[n - 1].is_vowel && !ls[n - 1].is_dstroke
{
ls[n - 1].is_dstroke = true;
if ch.is_ascii_uppercase() {
ls[n - 1].upper = true;
}
continue;
}
ls.push(Letter::new(ch, ch.is_ascii_uppercase()));
}
emit(&ls)
}
pub fn convert_vni(input: &str, modern: bool, _short_w: bool) -> String {
let mut ls: Vec<Letter> = Vec::with_capacity(input.len());
for ch in input.chars() {
let lc = ch.to_ascii_lowercase();
let upper = ch.is_ascii_uppercase();
if let Some(t) = vni_tone(lc) {
if let Some(vi) = tone_vowel_index(&ls, modern) {
ls[vi].tone = t;
continue;
}
}
match lc {
'6' => {
if let Some(vi) = last_vowel_index(&ls) {
if matches!(ls[vi].c, 'a' | 'e' | 'o') {
ls[vi].variant = 1;
continue;
}
}
}
'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;
}
}
}
'8' => {
if let Some(vi) = last_vowel_index(&ls) {
if ls[vi].c == 'a' {
ls[vi].variant = 2;
continue;
}
}
}
'9' => {
if let Some(di) = last_d_index(&ls) {
ls[di].is_dstroke = true;
if upper {
ls[di].upper = true;
}
}
continue;
}
'0' => {
continue;
}
_ => {}
}
if lc.is_ascii_alphabetic() || (lc.is_ascii_digit() && !ls.is_empty()) {
ls.push(Letter::new(ch, upper));
}
}
emit(&ls)
}
#[cfg(test)]
mod tests {
use super::*;
fn t(s: &str) -> String {
convert_telex(s, true, false)
}
fn tw(s: &str) -> String {
convert_telex(s, true, true)
}
fn tt(s: &str) -> String {
convert_telex(s, false, true)
}
fn vt(s: &str) -> String {
convert_vni(s, false, true)
}
#[test]
fn telex_basic() {
assert_eq!(t("as"), "á");
assert_eq!(t("af"), "à");
assert_eq!(t("ar"), "ả");
assert_eq!(t("ax"), "ã");
assert_eq!(t("aj"), "ạ");
assert_eq!(t("aw"), "ă");
assert_eq!(t("aa"), "â");
assert_eq!(t("ee"), "ê");
assert_eq!(t("oo"), "ô");
assert_eq!(t("ow"), "ơ");
assert_eq!(t("uw"), "ư");
assert_eq!(t("dd"), "đ");
}
#[test]
fn telex_compound() {
assert_eq!(t("aws"), "ắ");
assert_eq!(t("aas"), "ấ");
assert_eq!(t("ees"), "ế");
assert_eq!(t("oos"), "ố");
assert_eq!(t("ows"), "ớ");
assert_eq!(t("uws"), "ứ");
}
#[test]
fn telex_words() {
assert_eq!(t("xin"), "xin");
assert_eq!(t("tooi"), "tôi");
assert_eq!(t("laf"), "là");
assert_eq!(t("cos"), "có");
assert_eq!(t("khoong"), "không");
assert_eq!(t("tieengs"), "tiếng");
assert_eq!(t("Vieetj"), "Việt");
}
#[test]
fn telex_uow() {
assert_eq!(t("uow"), "ươ");
assert_eq!(t("cuowcs"), "cước");
assert_eq!(t("dduwowcj"), "được");
assert_eq!(t("nguwowif"), "người");
}
#[test]
fn telex_gi_qu() {
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á");
}
#[test]
fn telex_tone_placement() {
assert_eq!(t("chafo"), "chào");
assert_eq!(t("toans"), "toán");
assert_eq!(t("hoaf"), "hoà");
assert_eq!(t("thowif"), "thời");
assert_eq!(t("cuar"), "của");
assert_eq!(t("thoix"), "thõi");
assert_eq!(t("ofa"), "òa"); assert_eq!(t("oaf"), "oà"); }
#[test]
fn telex_english() {
assert_eq!(t("hello"), "hello");
assert_eq!(t("wood"), "wôd");
assert_eq!(t("reboot"), "rebôt");
}
#[test]
fn telex_z() {
assert_eq!(t("asz"), "a");
}
#[test]
fn telex_w_standalone() {
assert_eq!(t("w"), "w"); assert_eq!(tw("w"), "ư"); assert_eq!(tw("ws"), "ứ");
assert_eq!(tw("wf"), "ừ");
assert_eq!(tw("wn"), "ưn");
}
#[test]
fn telex_dd_traditional() {
assert_eq!(tt("dd"), "đ");
assert_eq!(tt("DD"), "Đ");
assert_eq!(tt("Dd"), "Đ");
assert_eq!(tt("dD"), "Đ");
}
#[test]
fn telex_marks_traditional() {
assert_eq!(tt("aa"), "â");
assert_eq!(tt("aw"), "ă");
assert_eq!(tt("ee"), "ê");
assert_eq!(tt("ew"), "ê");
assert_eq!(tt("oo"), "ô");
assert_eq!(tt("ow"), "ơ");
assert_eq!(tt("uw"), "ư");
assert_eq!(tt("uow"), "ươ");
}
#[test]
fn telex_w_traditional() {
assert_eq!(tt("w"), "ư");
assert_eq!(tt("W"), "Ư");
assert_eq!(tt("wn"), "ưn");
assert_eq!(tt("ws"), "ứ");
assert_eq!(tt("wf"), "ừ");
}
#[test]
fn telex_compound_traditional() {
assert_eq!(tt("aws"), "ắ");
assert_eq!(tt("aas"), "ấ");
assert_eq!(tt("ees"), "ế");
assert_eq!(tt("oos"), "ố");
assert_eq!(tt("ows"), "ớ");
assert_eq!(tt("uws"), "ứ");
}
#[test]
fn telex_words_traditional() {
assert_eq!(tt("chaof"), "chào");
assert_eq!(tt("vieejt"), "việt");
assert_eq!(tt("tieengs"), "tiếng");
assert_eq!(tt("quas"), "quá");
assert_eq!(tt("gias"), "giá");
assert_eq!(tt("xin"), "xin");
assert_eq!(tt("tooi"), "tôi");
assert_eq!(tt("laf"), "là");
assert_eq!(tt("cos"), "có");
assert_eq!(tt("khoong"), "không");
assert_eq!(tt("Vieetj"), "Việt");
}
#[test]
fn telex_tone_traditional() {
assert_eq!(tt("toans"), "toán");
assert_eq!(tt("hoaf"), "hoà");
assert_eq!(tt("thowif"), "thời");
assert_eq!(tt("cuar"), "của");
}
#[test]
fn telex_gi_qu_traditional() {
assert_eq!(tt("gif"), "gì");
assert_eq!(tt("giowf"), "giờ");
assert_eq!(tt("gioir"), "giỏi");
}
#[test]
fn telex_uow_traditional() {
assert_eq!(tt("cuowcs"), "cước");
assert_eq!(tt("dduwowcj"), "được");
assert_eq!(tt("nguwowif"), "người");
}
#[test]
fn telex_z_traditional() {
assert_eq!(tt("asz"), "a");
}
#[test]
fn telex_english_traditional() {
assert_eq!(tt("hello"), "hello");
}
#[test]
fn telex_more_traditional() {
assert_eq!(tt("hoanf"), "hoàn");
assert_eq!(tt("huyeenf"), "huyền");
assert_eq!(tt("nguyeexn"), "nguyễn");
assert_eq!(tt("ngoaif"), "ngoài");
assert_eq!(tt("thuowngf"), "thường");
}
#[test]
fn vni_marks_traditional() {
assert_eq!(vt("d9"), "đ");
assert_eq!(vt("a6"), "â");
assert_eq!(vt("a8"), "ă");
assert_eq!(vt("e6"), "ê");
assert_eq!(vt("o6"), "ô");
assert_eq!(vt("o7"), "ơ");
assert_eq!(vt("u7"), "ư");
assert_eq!(vt("uo7"), "ươ");
}
#[test]
fn vni_words_traditional() {
assert_eq!(vt("vie65t"), "việt");
assert_eq!(vt("tie61ng"), "tiếng");
assert_eq!(vt("qua1"), "quá");
assert_eq!(vt("gia1"), "giá");
}
#[test]
fn telex_tone_replace() {
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"); }
#[test]
fn teipvni_tone_replace() {
assert_eq!(tv("saosf"), "sào"); assert_eq!(tv("sao1f"), "sào"); }
#[test]
fn telex_triple_toggle() {
assert_eq!(tt("rooot"), "root"); assert_eq!(tt("roo"), "rô");
assert_eq!(tt("aa"), "â"); assert_eq!(tt("aaa"), "aa"); assert_eq!(tt("aaaa"), "aâ"); assert_eq!(tt("ee"), "ê");
assert_eq!(tt("eee"), "ee");
assert_eq!(tt("oooo"), "oô"); }
#[test]
fn telex_double_vowel_across_y() {
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");
assert_eq!(tt("sauas"), "sấu");
assert_eq!(tt("dauaf"), "dầu");
assert_eq!(tt("ee"), "ê");
assert_eq!(tt("aay"), "ây");
}
#[test]
fn telex_w_toggle() {
assert_eq!(tt("w"), "ư");
assert_eq!(tt("ww"), "w");
assert_eq!(tt("www"), "ww");
assert_eq!(tt("wwww"), "www");
assert_eq!(tt("wws"), "ws");
}
#[test]
fn telex_w_toggle_regular() {
assert_eq!(tt("aw"), "ă"); assert_eq!(tt("aww"), "a"); assert_eq!(tt("ow"), "ơ"); assert_eq!(tt("oww"), "o"); assert_eq!(tt("uw"), "ư"); assert_eq!(tt("uww"), "u"); assert_eq!(tt("ew"), "ê"); assert_eq!(tt("eww"), "e"); assert_eq!(tt("uow"), "ươ"); assert_eq!(tt("uoww"), "uo"); }
#[test]
fn telex_w_target() {
assert_eq!(tt("toiw"), "tơi");
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");
assert_eq!(tt("nguoiws"), "ngưới");
assert_eq!(tt("muoiwf"), "mười");
assert_eq!(tt("tuoiws"), "tưới");
assert_eq!(tt("tuooir"), "tuổi");
assert_eq!(tt("tuooif"), "tuồi");
}
#[test]
fn telex_toan() {
assert_eq!(tt("toanf"), "toàn");
assert_eq!(t("toanf"), "toàn");
assert_eq!(tt("hoanf"), "hoàn");
assert_eq!(t("hoanf"), "hoàn");
assert_eq!(t("oaf"), "oà"); assert_eq!(t("ofa"), "òa"); assert_eq!(tt("oaf"), "oà");
assert_eq!(tt("ofa"), "òa"); }
fn tv(s: &str) -> String {
convert_teip_vni(s, false, true)
}
#[test]
fn teipvni_telex_words() {
assert_eq!(tv("vieejt"), "việt");
assert_eq!(tv("dd"), "đ");
assert_eq!(tv("aa"), "â");
assert_eq!(tv("aw"), "ă");
assert_eq!(tv("ee"), "ê");
assert_eq!(tv("oo"), "ô");
assert_eq!(tv("ow"), "ơ");
assert_eq!(tv("uw"), "ư");
assert_eq!(tv("uow"), "ươ");
assert_eq!(tv("w"), "ư");
}
#[test]
fn teipvni_vni_marks() {
assert_eq!(tv("a6"), "â");
assert_eq!(tv("a8"), "ă");
assert_eq!(tv("e6"), "ê");
assert_eq!(tv("o6"), "ô");
assert_eq!(tv("o7"), "ơ");
assert_eq!(tv("u7"), "ư");
assert_eq!(tv("uo7"), "ươ");
assert_eq!(tv("d9"), "đ");
}
#[test]
fn teipvni_numbers() {
assert_eq!(tv("qua1"), "quá");
assert_eq!(tv("gia1"), "giá");
assert_eq!(tv("vie65t"), "việt");
assert_eq!(tv("tie61ng"), "tiếng");
}
#[test]
fn teipvni_mixed() {
assert_eq!(tv("vie6t"), "viêt"); assert_eq!(tv("vie6ts"), "viết"); assert_eq!(tv("tie6ngs"), "tiếng");
}
fn viqr(s: &str) -> String {
convert_viqr(s)
}
#[test]
fn viqr_marks() {
assert_eq!(viqr("a^"), "â");
assert_eq!(viqr("a("), "ă");
assert_eq!(viqr("e^"), "ê");
assert_eq!(viqr("o^"), "ô");
assert_eq!(viqr("o+"), "ơ");
assert_eq!(viqr("u+"), "ư");
assert_eq!(viqr("dd"), "đ");
}
#[test]
fn viqr_tones() {
assert_eq!(viqr("a'"), "á");
assert_eq!(viqr("a`"), "à");
assert_eq!(viqr("a?"), "ả");
assert_eq!(viqr("a~"), "ã");
assert_eq!(viqr("a."), "ạ");
}
#[test]
fn viqr_words() {
assert_eq!(viqr("vie^'t"), "viết");
assert_eq!(viqr("nu+o+'c"), "nước");
assert_eq!(viqr("ngu+o+`i"), "người");
}
#[test]
fn viqr_dd() {
assert_eq!(viqr("dd"), "đ");
assert_eq!(viqr("DD"), "Đ");
}
#[test]
fn viqr_english() {
assert_eq!(viqr("hello"), "hello");
}
}
#[test]
fn full_parity() {
struct Case {
input: &'static str,
expected: &'static str,
method: u8,
}
let cases = [
Case {
input: "dd",
expected: "đ",
method: 1,
},
Case {
input: "DD",
expected: "Đ",
method: 1,
},
Case {
input: "Dd",
expected: "Đ",
method: 1,
},
Case {
input: "dD",
expected: "Đ",
method: 1,
},
Case {
input: "aa",
expected: "â",
method: 1,
},
Case {
input: "aw",
expected: "ă",
method: 1,
},
Case {
input: "ee",
expected: "ê",
method: 1,
},
Case {
input: "ew",
expected: "ê",
method: 1,
},
Case {
input: "oo",
expected: "ô",
method: 1,
},
Case {
input: "ow",
expected: "ơ",
method: 1,
},
Case {
input: "uw",
expected: "ư",
method: 1,
},
Case {
input: "uow",
expected: "ươ",
method: 1,
},
Case {
input: "w",
expected: "ư",
method: 1,
},
Case {
input: "W",
expected: "Ư",
method: 1,
},
Case {
input: "wn",
expected: "ưn",
method: 1,
},
Case {
input: "ws",
expected: "ứ",
method: 1,
},
Case {
input: "wf",
expected: "ừ",
method: 1,
},
Case {
input: "aws",
expected: "ắ",
method: 1,
},
Case {
input: "aas",
expected: "ấ",
method: 1,
},
Case {
input: "ees",
expected: "ế",
method: 1,
},
Case {
input: "oos",
expected: "ố",
method: 1,
},
Case {
input: "ows",
expected: "ớ",
method: 1,
},
Case {
input: "uws",
expected: "ứ",
method: 1,
},
Case {
input: "chaof",
expected: "chào",
method: 1,
},
Case {
input: "vieejt",
expected: "việt",
method: 1,
},
Case {
input: "tieengs",
expected: "tiếng",
method: 1,
},
Case {
input: "quas",
expected: "quá",
method: 1,
},
Case {
input: "gias",
expected: "giá",
method: 1,
},
Case {
input: "xin",
expected: "xin",
method: 1,
},
Case {
input: "tooi",
expected: "tôi",
method: 1,
},
Case {
input: "laf",
expected: "là",
method: 1,
},
Case {
input: "cos",
expected: "có",
method: 1,
},
Case {
input: "khoong",
expected: "không",
method: 1,
},
Case {
input: "Vieetj",
expected: "Việt",
method: 1,
},
Case {
input: "toans",
expected: "toán",
method: 1,
},
Case {
input: "hoaf",
expected: "hoà",
method: 1,
},
Case {
input: "thowif",
expected: "thời",
method: 1,
},
Case {
input: "cuar",
expected: "của",
method: 1,
},
Case {
input: "gif",
expected: "gì",
method: 1,
},
Case {
input: "giowf",
expected: "giờ",
method: 1,
},
Case {
input: "gioir",
expected: "giỏi",
method: 1,
},
Case {
input: "cuowcs",
expected: "cước",
method: 1,
},
Case {
input: "dduwowcj",
expected: "được",
method: 1,
},
Case {
input: "nguwowif",
expected: "người",
method: 1,
},
Case {
input: "asz",
expected: "a",
method: 1,
},
Case {
input: "hello",
expected: "hello",
method: 1,
},
Case {
input: "hoanf",
expected: "hoàn",
method: 1,
},
Case {
input: "huyeenf",
expected: "huyền",
method: 1,
},
Case {
input: "nguyeexn",
expected: "nguyễn",
method: 1,
},
Case {
input: "ngoaif",
expected: "ngoài",
method: 1,
},
Case {
input: "thuowngf",
expected: "thường",
method: 1,
},
Case {
input: "d9",
expected: "đ",
method: 2,
},
Case {
input: "a6",
expected: "â",
method: 2,
},
Case {
input: "a8",
expected: "ă",
method: 2,
},
Case {
input: "e6",
expected: "ê",
method: 2,
},
Case {
input: "o6",
expected: "ô",
method: 2,
},
Case {
input: "o7",
expected: "ơ",
method: 2,
},
Case {
input: "u7",
expected: "ư",
method: 2,
},
Case {
input: "uo7",
expected: "ươ",
method: 2,
},
Case {
input: "vie65t",
expected: "việt",
method: 2,
},
Case {
input: "tie61ng",
expected: "tiếng",
method: 2,
},
Case {
input: "qua1",
expected: "quá",
method: 2,
},
Case {
input: "gia1",
expected: "giá",
method: 2,
},
Case {
input: "vie61t",
expected: "viết",
method: 4,
},
Case {
input: "vieejt",
expected: "việt",
method: 4,
},
Case {
input: "tie61ng",
expected: "tiếng",
method: 4,
},
Case {
input: "a6",
expected: "â",
method: 4,
},
Case {
input: "dd",
expected: "đ",
method: 4,
},
Case {
input: "dd",
expected: "đ",
method: 3,
},
Case {
input: "DD",
expected: "Đ",
method: 3,
},
Case {
input: "vie^'t",
expected: "viết",
method: 3,
},
Case {
input: "nu+o+'c",
expected: "nước",
method: 3,
},
Case {
input: "ngu+o+`i",
expected: "người",
method: 3,
},
];
let mut failures = Vec::new();
for c in &cases {
let result = match c.method {
1 => convert_telex(c.input, false, true),
2 => convert_vni(c.input, false, true),
3 => convert_viqr(c.input),
4 => convert_teip_vni(c.input, false, true),
_ => unreachable!(),
};
if result != c.expected {
failures.push(format!(
"[m{}] '{}' -> '{}' (expected '{}')",
c.method, c.input, result, c.expected
));
}
}
assert!(
failures.is_empty(),
"{} full parity failures:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn comprehensive_vietnamese() {
let cases: &[(&str, &str, u8)] = &[
("as", "á", 1),
("af", "à", 1),
("ar", "ả", 1),
("ax", "ã", 1),
("aj", "ạ", 1),
("es", "é", 1),
("ef", "è", 1),
("er", "ẻ", 1),
("ex", "ẽ", 1),
("ej", "ẹ", 1),
("is", "í", 1),
("if", "ì", 1),
("ir", "ỉ", 1),
("ix", "ĩ", 1),
("ij", "ị", 1),
("os", "ó", 1),
("of", "ò", 1),
("or", "ỏ", 1),
("ox", "õ", 1),
("oj", "ọ", 1),
("us", "ú", 1),
("uf", "ù", 1),
("ur", "ủ", 1),
("ux", "ũ", 1),
("uj", "ụ", 1),
("ys", "ý", 1),
("yf", "ỳ", 1),
("yr", "ỷ", 1),
("yx", "ỹ", 1),
("yj", "ỵ", 1),
("aw", "ă", 1),
("aws", "ắ", 1),
("awf", "ằ", 1),
("awr", "ẳ", 1),
("awx", "ẵ", 1),
("awj", "ặ", 1),
("aa", "â", 1),
("aas", "ấ", 1),
("aaf", "ầ", 1),
("aar", "ẩ", 1),
("aax", "ẫ", 1),
("aaj", "ậ", 1),
("ee", "ê", 1),
("ees", "ế", 1),
("eef", "ề", 1),
("eer", "ể", 1),
("eex", "ễ", 1),
("eej", "ệ", 1),
("ew", "ê", 1),
("ews", "ế", 1),
("ewf", "ề", 1),
("ewr", "ể", 1),
("oo", "ô", 1),
("oos", "ố", 1),
("oof", "ồ", 1),
("oor", "ổ", 1),
("oox", "ỗ", 1),
("ooj", "ộ", 1),
("ow", "ơ", 1),
("ows", "ớ", 1),
("owf", "ờ", 1),
("owr", "ở", 1),
("owx", "ỡ", 1),
("owj", "ợ", 1),
("uw", "ư", 1),
("uws", "ứ", 1),
("uwf", "ừ", 1),
("uwr", "ử", 1),
("uwx", "ữ", 1),
("uwj", "ự", 1),
("dd", "đ", 1),
("w", "ư", 1),
("ws", "ứ", 1),
("wf", "ừ", 1),
("wr", "ử", 1),
("wx", "ữ", 1),
("wj", "ự", 1),
("wn", "ưn", 1),
("wt", "ưt", 1),
("ais", "ái", 1),
("aif", "ài", 1),
("air", "ải", 1),
("aix", "ãi", 1),
("aij", "ại", 1),
("aos", "áo", 1),
("aof", "ào", 1),
("aor", "ảo", 1),
("aox", "ão", 1),
("aoj", "ạo", 1),
("aus", "áu", 1),
("auf", "àu", 1),
("aur", "ảu", 1),
("aux", "ãu", 1),
("auj", "ạu", 1),
("aays", "ấy", 1),
("aayf", "ầy", 1),
("aayr", "ẩy", 1),
("aayx", "ẫy", 1),
("aayj", "ậy", 1),
("ays", "áy", 1),
("ayf", "ày", 1),
("ayr", "ảy", 1),
("ayx", "ãy", 1),
("ayj", "ạy", 1),
("eos", "éo", 1),
("eof", "èo", 1),
("eor", "ẻo", 1),
("eox", "ẽo", 1),
("eoj", "ẹo", 1),
("eeus", "ếu", 1),
("eeuf", "ều", 1),
("eeur", "ểu", 1),
("eeux", "ễu", 1),
("eeuj", "ệu", 1),
("ois", "ói", 1),
("oif", "òi", 1),
("oir", "ỏi", 1),
("oix", "õi", 1),
("oij", "ọi", 1),
("oois", "ối", 1),
("ooif", "ồi", 1),
("ooir", "ổi", 1),
("ooix", "ỗi", 1),
("ooij", "ội", 1),
("owis", "ới", 1),
("owif", "ời", 1),
("owir", "ởi", 1),
("owix", "ỡi", 1),
("owij", "ợi", 1),
("uis", "úi", 1),
("uif", "ùi", 1),
("uir", "ủi", 1),
("uix", "ũi", 1),
("uij", "ụi", 1),
("uwis", "ứi", 1),
("uwif", "ừi", 1),
("uwir", "ửi", 1),
("uwix", "ữi", 1),
("uwij", "ựi", 1),
("uwus", "ứu", 1),
("uwuf", "ừu", 1),
("uwur", "ửu", 1),
("uwux", "ữu", 1),
("uwuj", "ựu", 1),
("oans", "oán", 1),
("oanf", "oàn", 1),
("oanr", "oản", 1),
("oanx", "oãn", 1),
("oanj", "oạn", 1),
("oats", "oát", 1),
("oatf", "oàt", 1),
("oatr", "oảt", 1),
("oens", "oén", 1),
("oenf", "oèn", 1),
("uyns", "uýn", 1),
("uynf", "uỳn", 1),
("uynr", "uỷn", 1),
("oas", "oá", 1),
("oaf", "oà", 1),
("oar", "oả", 1),
("oax", "oã", 1),
("oaj", "oạ", 1),
("oes", "oé", 1),
("oef", "oè", 1),
("uys", "uý", 1),
("uyf", "uỳ", 1),
("uyr", "uỷ", 1),
("uyx", "uỹ", 1),
("uyj", "uỵ", 1),
("oais", "oái", 1),
("oaif", "oài", 1),
("oair", "oải", 1),
("oaix", "oãi", 1),
("oaij", "oại", 1),
("oays", "oáy", 1),
("oayf", "oày", 1),
("oayr", "oảy", 1),
("oayx", "oãy", 1),
("oayj", "oạy", 1),
("uaays", "uấy", 1),
("uaayf", "uầy", 1),
("uaayr", "uẩy", 1),
("uaayx", "uẫy", 1),
("uaayj", "uậy", 1),
("uyeef", "uyề", 1),
("uyees", "uyế", 1),
("uyeer", "uyể", 1),
("uyeex", "uyễ", 1),
("uyeej", "uyệ", 1),
("ieef", "iề", 1),
("iees", "iế", 1),
("ieer", "iể", 1),
("ieex", "iễ", 1),
("ieej", "iệ", 1),
("yeef", "yề", 1),
("yees", "yế", 1),
("yeer", "yể", 1),
("yeex", "yễ", 1),
("yeej", "yệ", 1),
("uoois", "uối", 1),
("uooif", "uồi", 1),
("uooir", "uổi", 1),
("uooix", "uỗi", 1),
("uooij", "uội", 1),
("uoiws", "ưới", 1),
("uoiwf", "ười", 1),
("uoiwr", "ưởi", 1),
("uoiwx", "ưỡi", 1),
("uoiwj", "ượi", 1),
("gif", "gì", 1),
("gis", "gí", 1),
("gir", "gỉ", 1),
("gix", "gĩ", 1),
("gij", "gị", 1),
("gias", "giá", 1),
("giaf", "già", 1),
("giar", "giả", 1),
("giax", "giã", 1),
("giaj", "giạ", 1),
("gioir", "giỏi", 1),
("gioif", "giòi", 1),
("giois", "giói", 1),
("quas", "quá", 1),
("quaf", "quà", 1),
("quar", "quả", 1),
("quax", "quã", 1),
("quaj", "quạ", 1),
("quys", "quý", 1),
("quyf", "quỳ", 1),
("quyr", "quỷ", 1),
("quyx", "quỹ", 1),
("quyj", "quỵ", 1),
("chaof", "chào", 1),
("vieejt", "việt", 1),
("tieengs", "tiếng", 1),
("khoong", "không", 1),
("cuar", "của", 1),
("cuowcs", "cước", 1),
("dduwowcj", "được", 1),
("nguwowif", "người", 1),
("thuowngf", "thường", 1),
("hoanf", "hoàn", 1),
("huyeenf", "huyền", 1),
("nguyeexn", "nguyễn", 1),
("ngoaif", "ngoài", 1),
("ngoafi", "ngoài", 1),
("vaya", "vây", 1),
("vayaj", "vậy", 1),
("vayas", "vấy", 1),
("mayas", "mấy", 1),
("nhana", "nhân", 1),
("khanas", "khấn", 1),
("nhanaa", "nhana", 1),
("toans", "toán", 1),
("thowif", "thời", 1),
("xin", "xin", 1),
("tooi", "tôi", 1),
("laf", "là", 1),
("cos", "có", 1),
("banj", "bạn", 1),
("minhf", "mình", 1),
("nawm", "năm", 1),
("thangs", "tháng", 1),
("truwowngf", "trường", 1),
("luaatj", "luật", 1),
("nhas", "nhá", 1),
("nghir", "nghỉ", 1),
("gheps", "ghép", 1),
("truws", "trứ", 1),
("thas", "thá", 1),
("phos", "phó", 1),
("khas", "khá", 1),
("chays", "cháy", 1),
("ngays", "ngáy", 1),
("ddejp", "đẹp", 1),
("maats", "mất", 1),
("bacs", "bác", 1),
("ngachs", "ngách", 1),
("banhf", "bành", 1),
("tins", "tín", 1),
("metj", "mẹt", 1),
("asz", "a", 1),
("awsjz", "ă", 1),
("aasz", "â", 1),
("hello", "hello", 1),
("bacon", "bacon", 1),
("thing", "thing", 1),
("climb", "climb", 1),
("As", "Á", 1),
("Af", "À", 1),
("Aas", "Ấ", 1),
("Aws", "Ắ", 1),
("Dd", "Đ", 1),
("DD", "Đ", 1),
("W", "Ư", 1),
("Ws", "Ứ", 1),
("Vieetj", "Việt", 1),
("Tooi", "Tôi", 1),
("d9", "đ", 2),
("a6", "â", 2),
("a8", "ă", 2),
("e6", "ê", 2),
("o6", "ô", 2),
("o7", "ơ", 2),
("u7", "ư", 2),
("uo7", "ươ", 2),
("vie65t", "việt", 2),
("tie61ng", "tiếng", 2),
("qua1", "quá", 2),
("gia1", "giá", 2),
("vie61t", "viết", 4),
("vieejt", "việt", 4),
("tie61ng", "tiếng", 4),
("a6", "â", 4),
("dd", "đ", 4),
("dd", "đ", 3),
("DD", "Đ", 3),
("vie^'t", "viết", 3),
("nu+o+'c", "nước", 3),
("ngu+o+`i", "người", 3),
];
let mut failures = Vec::new();
for &(input, expected, method) in cases {
let result = match method {
1 => convert_telex(input, false, true),
2 => convert_vni(input, false, true),
3 => convert_viqr(input),
4 => convert_teip_vni(input, false, true),
_ => unreachable!(),
};
if result != expected {
failures.push(format!(
"[m{}] '{}' -> '{}' (expected '{}')",
method, input, result, expected
));
}
}
assert!(
failures.is_empty(),
"{} comprehensive failures (traditional mode):\n{}",
failures.len(),
failures.join("\n")
);
}