use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use crate::config::ChineseConvertMode;
static SC_TO_TC: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static TC_TO_SC: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static SC_FIRST_CHARS: OnceLock<HashSet<char>> = OnceLock::new();
static TC_FIRST_CHARS: OnceLock<HashSet<char>> = OnceLock::new();
const MAX_WORD_LEN_CHARS: usize = 6;
fn build_sc_to_tc() -> HashMap<&'static str, &'static str> {
[
("头发", "頭髮"), ("理发", "理髮"), ("发型", "髮型"), ("发际", "髮際"), ("白发", "白髮"), ("出发", "出發"), ("出发点", "出發點"), ("发展", "發展"), ("发现", "發現"), ("发生", "發生"), ("发布", "發布"), ("发射", "發射"), ("发言", "發言"), ("发表", "發表"), ("面条", "麵條"), ("拉面", "拉麵"), ("面粉", "麵粉"), ("方便面", "方便麵"), ("挂面", "掛麵"), ("面包", "麵包"), ("里面", "裡面"), ("这里", "這裡"), ("那里", "那裡"), ("心里", "心裡"), ("哪里", "哪裡"), ("以后", "以後"), ("然后", "然後"), ("后来", "後來"), ("之后", "之後"), ("此后", "此後"), ("今后", "今後"), ("前后", "前後"), ("后面", "後面"), ("后边", "後邊"), ("干燥", "乾燥"), ("饼干", "餅乾"), ("干净", "乾淨"), ("晒干", "曬乾"), ("干部", "幹部"), ("骨干", "骨幹"), ("树干", "樹幹"), ("关系", "關係"), ("联系", "聯繫"), ("系统", "系統"), ("当时", "當時"), ("当然", "當然"), ("当地", "當地"), ("适当", "適當"), ("征求", "徵求"), ("象征", "象徵"), ("特征", "特徵"), ("征兆", "徵兆"), ]
.into_iter()
.collect()
}
fn build_tc_to_sc() -> HashMap<&'static str, &'static str> {
[
("頭髮", "头发"),
("理髮", "理发"),
("髮型", "发型"),
("白髮", "白发"),
("出發", "出发"),
("發展", "发展"),
("發現", "发现"),
("麵條", "面条"),
("拉麵", "拉面"),
("麵粉", "面粉"),
("麵包", "面包"),
("裡面", "里面"),
("心裡", "心里"),
("聯繫", "联系"),
("骨幹", "骨干"),
("幹部", "干部"),
("徵求", "征求"),
("象徵", "象征"),
("特徵", "特征"),
("徵兆", "征兆"),
]
.into_iter()
.collect()
}
fn build_first_chars(map: &HashMap<&'static str, &'static str>) -> HashSet<char> {
map.keys().filter_map(|k| k.chars().next()).collect()
}
fn sc_to_tc_map() -> &'static HashMap<&'static str, &'static str> {
SC_TO_TC.get_or_init(build_sc_to_tc)
}
fn tc_to_sc_map() -> &'static HashMap<&'static str, &'static str> {
TC_TO_SC.get_or_init(build_tc_to_sc)
}
fn sc_first_chars() -> &'static HashSet<char> {
SC_FIRST_CHARS.get_or_init(|| build_first_chars(sc_to_tc_map()))
}
fn tc_first_chars() -> &'static HashSet<char> {
TC_FIRST_CHARS.get_or_init(|| build_first_chars(tc_to_sc_map()))
}
pub fn convert_words<'a>(
input: &'a str,
mode: ChineseConvertMode,
extra: Option<&HashMap<String, String>>,
) -> Cow<'a, str> {
if mode == ChineseConvertMode::Off {
return Cow::Borrowed(input);
}
let (map, gate): (&HashMap<&str, &str>, &HashSet<char>) = match mode {
ChineseConvertMode::ToTraditional => (sc_to_tc_map(), sc_first_chars()),
ChineseConvertMode::ToSimplified => (tc_to_sc_map(), tc_first_chars()),
ChineseConvertMode::Off => unreachable!(),
};
let extra_gate: HashSet<char> = extra
.map(|e| e.keys().filter_map(|k| k.chars().next()).collect())
.unwrap_or_default();
let chars: Vec<(usize, char)> = input.char_indices().collect();
let n = chars.len();
let mut owned: Option<String> = None;
let mut i = 0usize;
while i < n {
let (byte_pos, ch) = chars[i];
if !gate.contains(&ch) && !extra_gate.contains(&ch) {
if let Some(ref mut buf) = owned {
buf.push(ch);
}
i += 1;
continue;
}
let max_try = MAX_WORD_LEN_CHARS.min(n - i);
let mut matched = false;
for try_len in (1..=max_try).rev() {
let end_byte = if i + try_len < n {
chars[i + try_len].0
} else {
input.len()
};
let candidate = &input[byte_pos..end_byte];
let replacement: Option<&str> = map
.get(candidate)
.copied()
.or_else(|| extra.and_then(|e| e.get(candidate).map(String::as_str)));
if let Some(repl) = replacement {
if owned.is_none() {
owned = Some(input[..byte_pos].to_owned());
}
owned.as_mut().unwrap().push_str(repl);
i += try_len;
matched = true;
break;
}
}
if !matched {
if let Some(ref mut buf) = owned {
buf.push(ch);
}
i += 1;
}
}
match owned {
Some(s) => Cow::Owned(s),
None => Cow::Borrowed(input),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sc_to_tc(s: &str) -> Cow<'_, str> {
convert_words(s, ChineseConvertMode::ToTraditional, None)
}
fn tc_to_sc(s: &str) -> Cow<'_, str> {
convert_words(s, ChineseConvertMode::ToSimplified, None)
}
#[test]
fn mode_off_returns_borrowed() {
let input = "头发发展";
let result = convert_words(input, ChineseConvertMode::Off, None);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, input);
}
#[test]
fn clean_ascii_returns_borrowed() {
let result = sc_to_tc("hello world");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn japanese_text_returns_borrowed() {
let result = sc_to_tc("斎藤一郎");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn no_match_returns_borrowed() {
let result = sc_to_tc("皇后太后");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn hair_vs_depart_disambiguation() {
assert_eq!(sc_to_tc("头发"), "頭髮");
assert_eq!(sc_to_tc("出发"), "出發");
}
#[test]
fn noodle_vs_face_disambiguation() {
assert_eq!(sc_to_tc("面条"), "麵條");
let result = sc_to_tc("面孔");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn inside_vs_unit_disambiguation() {
assert_eq!(sc_to_tc("里面"), "裡面");
assert_eq!(sc_to_tc("这里"), "這裡");
let result = sc_to_tc("公里");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn after_vs_empress_disambiguation() {
assert_eq!(sc_to_tc("以后"), "以後");
assert_eq!(sc_to_tc("然后"), "然後");
let result = sc_to_tc("皇后");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn dry_vs_cadre_disambiguation() {
assert_eq!(sc_to_tc("干燥"), "乾燥");
assert_eq!(sc_to_tc("干部"), "幹部");
}
#[test]
fn longest_match_wins() {
assert_eq!(sc_to_tc("出发点"), "出發點");
}
#[test]
fn mixed_sentence() {
assert_eq!(sc_to_tc("以后发展很好"), "以後發展很好");
}
#[test]
fn tc_to_sc_basic() {
assert_eq!(tc_to_sc("頭髮"), "头发");
assert_eq!(tc_to_sc("骨幹"), "骨干");
}
#[test]
fn custom_dict_extra() {
let mut extra: HashMap<String, String> = HashMap::new();
extra.insert("測試".to_owned(), "测试".to_owned());
let result = convert_words("測試", ChineseConvertMode::ToSimplified, Some(&extra));
assert_eq!(result, "测试");
}
#[test]
fn custom_dict_does_not_override_builtin() {
let mut extra: HashMap<String, String> = HashMap::new();
extra.insert("头发".to_owned(), "WRONG".to_owned());
let result = convert_words("头发", ChineseConvertMode::ToTraditional, Some(&extra));
assert_eq!(result, "頭髮"); }
}