diff --git a/codegen/dict/kakasidict.utf8 b/codegen/dict/kakasidict.utf8 index 78294ff..58b166c 100644 --- a/codegen/dict/kakasidict.utf8 +++ b/codegen/dict/kakasidict.utf8 @@ -8317,7 +8317,6 @@ おうようじょう 応用上 おうようじょうほう 応用情報 おうようじょうほうがく 応用情報学 -おうじょうけん 応用情報学研究センター おうようすいしんか 応用推進課 おうようすうがく 応用数学 おうようすうがっか 応用数学科 @@ -14908,7 +14907,6 @@ かんいしんぱん 簡易新版 かんいてき 簡易的 かんいほけん 簡易保険 -かんいほけんほーる 簡易保険ホール かんいほうしき 簡易方式 かんいむせん 簡易無線 かんけつ 簡潔 @@ -42588,7 +42586,6 @@ しょせきるい 書籍類 しょせん 書泉 しょせんぐらんで 書泉グランデ -しょせんぶっくまーと 書泉ブックマート しょたい 書体 しょだな 書棚 かきおき 書置 @@ -46961,7 +46958,6 @@ しんにっぽん 新日本 しんにほんしょうけん 新日本証券 しんにほんせいてつ 新日本製鉄 -しんにってつ 新日本製鉄株式会社 しんにほんせいてつ 新日本製鐵 しんにっぽんせいてつ 新日本製鐵 しんにってつ 新日鐵 @@ -54141,7 +54137,6 @@ せんこうちゅう 選考中 せんこうび 選考日 せんこう 選鉱 -せんけん 選鉱精錬研究所 せんじゃ 選者 せんしゅ 選手 せんしゅいちらん 選手一覧 @@ -62107,7 +62102,6 @@ ちゅうがくせいばん 中学生版 ちゅうがくにゅうしもんだい 中学入試問題 ちゅうかっこ 中括弧 -なかま 中間 ちゅうかんれべるがくしゅう 中間レベル学習 ちゅうかんえき 中間駅 ちゅうかんえきしはつ 中間駅始発 @@ -66471,7 +66465,6 @@ でんしききぶ 電子機器部 でんしぎじゅつ 電子技術 でんしぎじゅつしゃ 電子技術者 -でんそうけん 電子技術総合研究所 でんしきょう 電子協 でんしけいじばん 電子掲示板 でんしけい 電子系 @@ -66486,7 +66479,6 @@ でんしこうがくきょうしつ 電子工学教室 でんしこうがくせんこう 電子工学専攻 でんしこうぎょう 電子工業 -でんしきょう 電子工業振興協会 でんしこうさく 電子工作 でんしざいりょう 電子材料 でんししき 電子式 @@ -67641,7 +67633,6 @@ とうきょうがす 東京ガス とうきょうすたいる 東京スタイル とうきょうてあとる 東京テアトル -とうきょうべいえぬけーほーる 東京ベイNKホール とうきょういがい 東京以外 とうきょういち 東京一 とうきょうえき 東京駅 @@ -68467,7 +68458,6 @@ とうけいしょり 統計処理 とうけいじょうほう 統計情報 とうけいすうがく 統計数学 -とうすうけん 統計数理研究所 とうけいち 統計値 とうけいてき 統計的 とうけいてきぱたあん 統計的パターン @@ -71469,12 +71459,10 @@ にっぽんとむそん 日本トムソン にっぽんはむ 日本ハム にほんはむ 日本ハム -にっぽんひゅーむかん 日本ヒューム管 にっぽんびくたー 日本ビクター にっぽんぺいんと 日本ペイント にっぽんゆにばっく 日本ユニバック にっぽんれーす 日本レース -にほんろぼっとがっかい 日本ロボット学会 にほんいがい 日本以外 にほんいじょう 日本以上 にほんいち 日本一 @@ -72032,8 +72020,6 @@ にゅうしゅつりょく 入出力 にゅうしゅつりょくh 入出力 にゅうしゅつりょくせっと 入出力セット -にゅうしゅつりょくぱ 入出力パターン -にゅうしゅつりょくぱたーん 入出力パターン にゅうしゅつりょくかんけい 入出力関係 にゅうしゅつりょくけい 入出力系 にゅうしゅつりょくそうち 入出力装置 @@ -79676,7 +79662,6 @@ ぶんいちそうごうしゅっぱん 文一総合出版 ぶんえんどう 文苑堂 ぶんか 文化 -ぶんかしゃったー 文化シャッター ぶんかかい 文化会 ぶんかかいかん 文化会館 ぶんかかいかん 文化会舘 @@ -80078,7 +80063,6 @@ へいおん 平温 へいおん 平穏 ひらかな 平仮名 -ひらかなかくていにゅうりょく 平仮名確定入力 へいか 平価 へいけ 平家 へいけものがたり 平家物語 @@ -110778,7 +110762,6 @@ せんせいよう 先生用 せんせんげつ 先先月 せんたんいりょう 先端医療 -せんたんかがくぎじゅつけんきゅうせんたー 先端科学技術研究センター せんたんぎじゅつけんきゅう 先端技術研究 せんたんきょうふしょう 先端恐怖症 せんたんけん 先端研 @@ -114513,7 +114496,6 @@ でんしききるい 電子機器類 でんしきどう 電子軌道 でんしぎじゅつかんけい 電子技術関係 -でんしぎじゅつそうごうけんきゅうしょ 電子技術総合研究所 でんしけいじばん 電子掲示版 でんしけいさんきしつ 電子計算機室 でんしこうさくよう 電子工作用 @@ -116022,7 +116004,6 @@ にほんちんぼつ 日本沈没 にほんていえん 日本庭園 にほんてつどう 日本鉄道 -にほんにんちかがくかい 日本認知科学会 にほんねこ 日本猫 にほんばんじまく 日本版字幕 にほんぶっきょう 日本仏教 @@ -116115,7 +116096,6 @@ にゅうよくざい 入浴剤 にゅうよくはっぽうき 入浴発泡器 にゅうらい 入来 -にゅうりょくぱらめーた 入力パラメータ にゅうりょくかんじ 入力漢字 にゅうりょくかんきょう 入力環境 にゅうりょくじたい 入力自体 diff --git a/codegen/src/main.rs b/codegen/src/main.rs index 1b07f8b..acf9774 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -1,4 +1,5 @@ mod phfbin_gen; +mod testconv; use std::{borrow::Cow, collections::HashMap, path::Path}; @@ -66,12 +67,13 @@ fn parse_dict_ln(records: &mut Records, line: &str, ln: usize) { .or_else(|| context.map(str::to_owned)) .unwrap_or_default(), ) { - std::collections::hash_map::Entry::Occupied(mut e) => { - // Replace reading if the new one is longer + std::collections::hash_map::Entry::Occupied(_) => { + /* + // Replace reading if the new one is shorter let val = e.get_mut(); - if val.len() < reading.len() { + if val.len() > reading.len() { *val = reading.to_owned(); - } + }*/ } std::collections::hash_map::Entry::Vacant(e) => { e.insert(reading.to_owned()); @@ -198,9 +200,63 @@ impl Encodable for Readings { } } +fn find_redundant_compounds(dict: &Records) -> Records { + let mut wdict = dict.clone(); + + for (kanji, readings) in dict { + if kanji.chars().count() <= 3 { + continue; + } + if readings.len() != 1 { + continue; + } + + if let Some(reading) = readings.get("") { + // Try to convert the entry without it being present + let entry = wdict.remove_entry(kanji).unwrap(); + let res = testconv::convert(kanji, &wdict); + + if &res == reading || to_romaji_nodc(&res) == to_romaji_nodc(reading) { + println!("Redundant: {} - {}", kanji, reading); + } else { + // Put the entry back if it is necessary + wdict.insert(entry.0, entry.1); + } + } + } + wdict +} + +/// Romanize and remove double consonants +fn to_romaji_nodc(text: &str) -> String { + let rom = wana_kana::to_romaji::to_romaji(text); + + let mut buf = String::new(); + let mut citer = rom.chars().peekable(); + + while let Some(c) = citer.next() { + if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') { + match citer.peek() { + Some(nc) => { + if &c != nc { + buf.push(c); + } + } + None => buf.push(c), + } + } else { + buf.push(c); + } + } + buf +} + fn generate_kanji_dict() -> Vec { let mut records = Records::default(); parse_dict(&mut records, Path::new("dict/kakasidict.utf8")); + records = find_redundant_compounds(&records); + + println!("kanji_dict: {} entries", records.len()); let mut phfmap = phfbin_gen::Map::::default(); for (kanji, readings) in records { diff --git a/codegen/src/testconv.rs b/codegen/src/testconv.rs new file mode 100644 index 0000000..850d27d --- /dev/null +++ b/codegen/src/testconv.rs @@ -0,0 +1,196 @@ +use crate::{Records, CLETTERS}; + +const ENDMARK: [char; 11] = [ + ')', ']', '!', '.', ',', '\u{3001}', '\u{3002}', '\u{ff1f}', '\u{ff10}', '\u{ff1e}', '\u{ff1c}', +]; +const DASH_SYMBOLS: [char; 4] = ['\u{30FC}', '\u{2015}', '\u{2212}', '\u{FF70}']; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CharType { + Kanji, + Katakana, + Hiragana, + Symbol, + Alpha, +} + +pub fn convert(text: &str, dict: &Records) -> String { + // TODO: char conversion should be done with iterators + let mut char_indices = text.char_indices(); + let mut kana_text = String::new(); + let mut hiragana = String::new(); + let mut prev_type = CharType::Kanji; + + // output_flag + // means (output buffer?, output text[i]?, copy to buffer and increment i?) + // possible (False, True, True), (True, False, False), (True, True, True) + // (False, False, True) + + while let Some((i, c)) = char_indices.next() { + let output_flag = if ENDMARK.contains(&c) { + (CharType::Symbol, true, true, true) + } else if DASH_SYMBOLS.contains(&c) { + (prev_type, false, false, true) + } else if is_sym(c) { + if prev_type != CharType::Symbol { + (CharType::Symbol, true, false, true) + } else { + (CharType::Symbol, false, true, true) + } + } else if wana_kana::utils::is_char_katakana(c) { + ( + CharType::Katakana, + prev_type != CharType::Katakana, + false, + true, + ) + } else if wana_kana::utils::is_char_hiragana(c) { + ( + CharType::Hiragana, + prev_type != CharType::Hiragana, + false, + true, + ) + } else if c.is_ascii() { + (CharType::Alpha, prev_type != CharType::Alpha, false, true) + } else if wana_kana::utils::is_char_kanji(c) { + if !kana_text.is_empty() { + hiragana.push_str(&convert_kana(&kana_text)); + } + let (t, n) = convert_kanji(&text[i..], &kana_text, &dict); + + if n > 0 { + kana_text = t; + for _ in 1..n { + char_indices.next(); + } + (CharType::Kanji, false, false, false) + } else { + // Unknown kanji + kana_text.clear(); + // TODO: FOR TESTING + hiragana.push_str("🯄"); + (CharType::Kanji, true, false, false) + } + } else if matches!(c as u32, 0xf000..=0xfffd | 0x10000..=0x10ffd) { + // PUA: ignore and drop + if !kana_text.is_empty() { + hiragana.push_str(&convert_kana(&kana_text)); + } + (prev_type, false, false, false) + } else { + (prev_type, true, true, true) + }; + + prev_type = output_flag.0; + + if output_flag.1 && output_flag.2 { + kana_text.push(c); + hiragana.push_str(&convert_kana(&kana_text)); + kana_text.clear() + } else if output_flag.1 && output_flag.3 { + if !kana_text.is_empty() { + hiragana.push_str(&convert_kana(&kana_text)); + } + kana_text = c.to_string(); + } else if output_flag.3 { + kana_text.push(c); + } + } + + // Convert last word + if !kana_text.is_empty() { + hiragana.push_str(&convert_kana(&kana_text)); + } + + hiragana +} + +fn is_sym(c: char) -> bool { + matches!(c as u32, + 0x3000..=0x3020 | + 0x3030..=0x303F | + 0x0391..=0x03A1 | + 0x03A3..=0x03A9 | + 0x03B1..=0x03C9 | + 0x0410..= 0x044F | + 0xFF01..=0xFF1A | + 0x00A1..=0x00FF | + 0xFF20..=0xFF5E | + 0x0451 | + 0x0401 + ) +} + +fn convert_kana(text: &str) -> String { + wana_kana::to_hiragana::to_hiragana_with_opt( + text, + wana_kana::Options { + use_obsolete_kana: false, + pass_romaji: true, + upcase_katakana: false, + imemode: false, + }, + ) +} + +/// Convert the leading kanji from the input string to hiragana +fn convert_kanji(text: &str, btext: &str, dict: &Records) -> (String, usize) { + let mut translation: Option = None; + let mut i_c = 0; + let mut n_c = 0; + let mut char_indices = text.char_indices().peekable(); + + while let Some((i, c)) = char_indices.next() { + let kanji = &text[0..i + c.len_utf8()]; + + let this_tl = dict.get(kanji).and_then(|readings| { + readings + .iter() + .find_map(|(k, reading)| { + if k.is_empty() { + None + } else if let Some(cltr) = CLETTERS.get(&k.chars().next().unwrap_or_default()) { + char_indices.peek().and_then(|(_, next_c)| { + // Shortcut if the next character is not hiragana + if wana_kana::utils::is_char_hiragana(*next_c) { + if cltr.contains(&&next_c.to_string().as_str()) { + // Add the next character to the char count + i_c += 1; + let mut hira = reading.to_owned(); + hira.push(*next_c); + return Some(hira); + } else { + None + } + } else { + None + } + }) + } else if wana_kana::is_hiragana::is_hiragana(&k) { + if btext.contains(reading) { + Some(reading.to_owned()) + } else { + None + } + } else { + panic!("invalid reading key") + } + }) + .or_else(|| readings.get("").cloned()) + }); + + i_c += 1; + if let Some(tl) = this_tl { + translation = Some(tl); + n_c = i_c; + } + if i_c >= 12 { + break; + } + } + + translation + .map(|tl| (tl.to_owned(), n_c)) + .unwrap_or_default() +} diff --git a/src/kanji_dict.bin b/src/kanji_dict.bin index 159f5a4..02af08a 100644 Binary files a/src/kanji_dict.bin and b/src/kanji_dict.bin differ diff --git a/src/lib.rs b/src/lib.rs index e5770ca..5ed25a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ use phfbin::PhfMap; use types::{KanjiString, Readings}; const KANJI_DICT: &[u8] = include_bytes!("./kanji_dict.bin"); +const MAX_KANJI_LEN: usize = 7; static CLETTERS: phf::Map = phf::phf_map!( b'a' => &['あ', 'ぁ', 'っ', 'わ', 'ゎ'], @@ -39,6 +40,20 @@ static CLETTERS: phf::Map = phf::phf_map!( b'v' => &['ゔ'], ); +const ENDMARK: [char; 11] = [ + ')', ']', '!', '.', ',', '\u{3001}', '\u{3002}', '\u{ff1f}', '\u{ff10}', '\u{ff1e}', '\u{ff1c}', +]; +const DASH_SYMBOLS: [char; 4] = ['\u{30FC}', '\u{2015}', '\u{2212}', '\u{FF70}']; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CharType { + Kanji, + Katakana, + Hiragana, + Symbol, + Alpha, +} + pub fn convert(text: &str) -> KakasiResult { let dict = PhfMap::new(KANJI_DICT); @@ -46,12 +61,131 @@ pub fn convert(text: &str) -> KakasiResult { let text = text.nfkc().collect::(); let text = convert_syn(&text); - let hiragana = convert_kanji(&text, "", &dict).0; - let romaji = wana_kana::to_romaji::to_romaji(&hiragana); + let mut char_indices = text.char_indices(); + let mut kana_text = String::new(); + let mut prev_type = CharType::Kanji; + + let mut hiragana = String::new(); + let mut romaji = String::new(); + + let conv_kana_txt = |kana_text: &mut String, hiragana: &mut String, romaji: &mut String| { + if !kana_text.is_empty() { + let h = convert_kana(&kana_text); + hiragana.push_str(&h); + romaji.push_str(&wana_kana::to_romaji::to_romaji(&h)); + romaji.push(' '); + } + }; + + // output_flag + // means (output buffer?, output text[i]?, copy to buffer and increment i?) + // possible (False, True, True), (True, False, False), (True, True, True) + // (False, False, True) + + while let Some((i, c)) = char_indices.next() { + let output_flag = if ENDMARK.contains(&c) { + (CharType::Symbol, true, true, true) + } else if DASH_SYMBOLS.contains(&c) { + (prev_type, false, false, true) + } else if is_sym(c) { + if prev_type != CharType::Symbol { + (CharType::Symbol, true, false, true) + } else { + (CharType::Symbol, false, true, true) + } + } else if wana_kana::utils::is_char_katakana(c) { + ( + CharType::Katakana, + prev_type != CharType::Katakana, + false, + true, + ) + } else if wana_kana::utils::is_char_hiragana(c) { + ( + CharType::Hiragana, + prev_type != CharType::Hiragana, + false, + true, + ) + } else if c.is_ascii() { + (CharType::Alpha, prev_type != CharType::Alpha, false, true) + } else if wana_kana::utils::is_char_kanji(c) { + conv_kana_txt(&mut kana_text, &mut hiragana, &mut romaji); + let (t, n) = convert_kanji(&text[i..], &kana_text, &dict); + + if n > 0 { + kana_text = t; + for _ in 1..n { + char_indices.next(); + } + (CharType::Kanji, false, false, false) + } else { + // Unknown kanji + kana_text.clear(); + // TODO: FOR TESTING + hiragana.push_str("🯄"); + romaji.push_str("🯄"); + (CharType::Kanji, true, false, false) + } + } else if matches!(c as u32, 0xf000..=0xfffd | 0x10000..=0x10ffd) { + // PUA: ignore and drop + conv_kana_txt(&mut kana_text, &mut hiragana, &mut romaji); + kana_text.clear(); + (prev_type, false, false, false) + } else { + (prev_type, true, true, true) + }; + + prev_type = output_flag.0; + + if output_flag.1 && output_flag.2 { + kana_text.push(c); + conv_kana_txt(&mut kana_text, &mut hiragana, &mut romaji); + kana_text.clear() + } else if output_flag.1 && output_flag.3 { + conv_kana_txt(&mut kana_text, &mut hiragana, &mut romaji); + kana_text = c.to_string(); + } else if output_flag.3 { + kana_text.push(c); + } + } + + // Convert last word + conv_kana_txt(&mut kana_text, &mut hiragana, &mut romaji); + // Remove trailing space + romaji.pop(); KakasiResult { hiragana, romaji } } +fn is_sym(c: char) -> bool { + matches!(c as u32, + 0x3000..=0x3020 | + 0x3030..=0x303F | + 0x0391..=0x03A1 | + 0x03A3..=0x03A9 | + 0x03B1..=0x03C9 | + 0x0410..= 0x044F | + 0xFF01..=0xFF1A | + 0x00A1..=0x00FF | + 0xFF20..=0xFF5E | + 0x0451 | + 0x0401 + ) +} + +fn convert_kana(text: &str) -> String { + wana_kana::to_hiragana::to_hiragana_with_opt( + text, + wana_kana::Options { + use_obsolete_kana: false, + pass_romaji: true, + upcase_katakana: false, + imemode: false, + }, + ) +} + /// Convert the leading kanji from the input string to hiragana /// /// # Arguments @@ -61,7 +195,7 @@ pub fn convert(text: &str) -> KakasiResult { /// The input needs to be NFKC-normalized and synonymous kanji need to be /// replaced using [`convert_syn`]. /// -/// * `btext` - +/// * `btext` - Buffer string (leading kana) /// /// # Return /// @@ -69,6 +203,7 @@ pub fn convert(text: &str) -> KakasiResult { /// * `1` - Number of converted chars from the input string fn convert_kanji(text: &str, btext: &str, dict: &PhfMap) -> (String, usize) { let mut translation = None; + let mut i_c = 0; let mut n_c = 0; let mut char_indices = text.char_indices().peekable(); @@ -87,7 +222,7 @@ fn convert_kanji(text: &str, btext: &str, dict: &PhfMap) -> (String, usize) { CLETTERS.get(&ch).and_then(|cltr| { if cltr.contains(next_c) { // Add the next character to the char count - n_c += 1; + i_c += 1; hira.push(*next_c); Some(hira) } else { @@ -109,11 +244,14 @@ fn convert_kanji(text: &str, btext: &str, dict: &PhfMap) -> (String, usize) { }) }); - match this_tl { - Some(this_tl) => translation = Some(this_tl), - None => break, + i_c += 1; + if let Some(tl) = this_tl { + translation = Some(tl); + n_c = i_c; + } + if i_c >= MAX_KANJI_LEN { + break; } - n_c += 1; } translation @@ -165,6 +303,9 @@ mod tests { #[rstest] #[case("会っAbc", "あっ", 2)] + #[case("渋谷", "しぶや", 2)] + // #[case("渋谷公会堂", "しぶやこうかいどう", 5)] + // #[case("家畜衛生試験場", "かちくえいせいしけんじょう", 7)] fn t_convert_kanji(#[case] text: &str, #[case] expect: &str, #[case] expect_n: usize) { let dict = PhfMap::new(KANJI_DICT); let (res, n) = convert_kanji(text, "", &dict); diff --git a/src/main.rs b/src/main.rs index 205cad9..482d32a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ fn main() { for line in std::io::stdin().lines() { let res = kakasi::convert(&line.unwrap()); - println!("{} - {}", res.hiragana, res.romaji); + println!("{}\n{}\n\n", res.hiragana, res.romaji); } }