1#[derive(Copy, Clone, Debug)]
16struct Entry {
17 rom: &'static str,
18 kana_h: &'static str,
19 kana_k: &'static str,
20}
21
22const fn e(rom: &'static str, h: &'static str, k: &'static str) -> Entry {
24 Entry { rom, kana_h: h, kana_k: k }
25}
26
27#[rustfmt::skip]
29const TABLE: &[Entry] = &[
30 e("xtsu", "っ", "ッ"),
32 e("ltsu", "っ", "ッ"),
33
34 e("kya", "きゃ", "キャ"), e("kyu", "きゅ", "キュ"), e("kyo", "きょ", "キョ"),
36 e("gya", "ぎゃ", "ギャ"), e("gyu", "ぎゅ", "ギュ"), e("gyo", "ぎょ", "ギョ"),
37 e("sha", "しゃ", "シャ"), e("shu", "しゅ", "シュ"), e("sho", "しょ", "ショ"),
38 e("sya", "しゃ", "シャ"), e("syu", "しゅ", "シュ"), e("syo", "しょ", "ショ"),
39 e("shi", "し", "シ"),
40 e("cha", "ちゃ", "チャ"), e("chu", "ちゅ", "チュ"), e("cho", "ちょ", "チョ"),
41 e("tya", "ちゃ", "チャ"), e("tyu", "ちゅ", "チュ"), e("tyo", "ちょ", "チョ"),
42 e("chi", "ち", "チ"),
43 e("tsu", "つ", "ツ"),
44 e("jya", "じゃ", "ジャ"), e("jyu", "じゅ", "ジュ"), e("jyo", "じょ", "ジョ"),
45 e("nya", "にゃ", "ニャ"), e("nyu", "にゅ", "ニュ"), e("nyo", "にょ", "ニョ"),
46 e("hya", "ひゃ", "ヒャ"), e("hyu", "ひゅ", "ヒュ"), e("hyo", "ひょ", "ヒョ"),
47 e("bya", "びゃ", "ビャ"), e("byu", "びゅ", "ビュ"), e("byo", "びょ", "ビョ"),
48 e("pya", "ぴゃ", "ピャ"), e("pyu", "ぴゅ", "ピュ"), e("pyo", "ぴょ", "ピョ"),
49 e("mya", "みゃ", "ミャ"), e("myu", "みゅ", "ミュ"), e("myo", "みょ", "ミョ"),
50 e("rya", "りゃ", "リャ"), e("ryu", "りゅ", "リュ"), e("ryo", "りょ", "リョ"),
51 e("xtu", "っ", "ッ"),
52 e("ltu", "っ", "ッ"),
53 e("xya", "ゃ", "ャ"), e("xyu", "ゅ", "ュ"), e("xyo", "ょ", "ョ"),
54
55 e("fya", "ふゃ", "フャ"), e("fyu", "ふゅ", "フュ"), e("fyo", "ふょ", "フョ"),
61 e("vya", "ゔゃ", "ヴャ"), e("vyu", "ゔゅ", "ヴュ"), e("vyo", "ゔょ", "ヴョ"),
62 e("tsa", "つぁ", "ツァ"), e("tsi", "つぃ", "ツィ"),
64 e("tse", "つぇ", "ツェ"), e("tso", "つぉ", "ツォ"),
65 e("che", "ちぇ", "チェ"),
66 e("she", "しぇ", "シェ"),
67 e("kwa", "くぁ", "クァ"), e("kwi", "くぃ", "クィ"),
69 e("kwe", "くぇ", "クェ"), e("kwo", "くぉ", "クォ"),
70 e("gwa", "ぐぁ", "グァ"), e("gwi", "ぐぃ", "グィ"),
71 e("gwe", "ぐぇ", "グェ"), e("gwo", "ぐぉ", "グォ"),
72 e("wha", "うぁ", "ウァ"), e("whi", "うぃ", "ウィ"),
75 e("whe", "うぇ", "ウェ"), e("who", "うぉ", "ウォ"),
76 e("tha", "てぁ", "テァ"), e("thi", "てぃ", "ティ"),
79 e("the", "てぇ", "テェ"), e("tho", "てょ", "テョ"),
80 e("dha", "でぁ", "デァ"), e("dhi", "でぃ", "ディ"),
81 e("dhe", "でぇ", "デェ"), e("dho", "でょ", "デョ"),
82 e("twu", "とぅ", "トゥ"),
84 e("dwu", "どぅ", "ドゥ"),
85
86 e("ka", "か", "カ"), e("ki", "き", "キ"), e("ku", "く", "ク"), e("ke", "け", "ケ"), e("ko", "こ", "コ"),
88 e("ga", "が", "ガ"), e("gi", "ぎ", "ギ"), e("gu", "ぐ", "グ"), e("ge", "げ", "ゲ"), e("go", "ご", "ゴ"),
89 e("sa", "さ", "サ"), e("su", "す", "ス"), e("se", "せ", "セ"), e("so", "そ", "ソ"),
90 e("si", "し", "シ"), e("za", "ざ", "ザ"), e("zi", "じ", "ジ"), e("zu", "ず", "ズ"), e("ze", "ぜ", "ゼ"), e("zo", "ぞ", "ゾ"),
92 e("ji", "じ", "ジ"), e("ta", "た", "タ"), e("tu", "つ", "ツ"), e("te", "て", "テ"), e("to", "と", "ト"),
94 e("ti", "ち", "チ"), e("da", "だ", "ダ"), e("di", "ぢ", "ヂ"), e("du", "づ", "ヅ"), e("de", "で", "デ"), e("do", "ど", "ド"),
96 e("na", "な", "ナ"), e("ni", "に", "ニ"), e("nu", "ぬ", "ヌ"), e("ne", "ね", "ネ"), e("no", "の", "ノ"),
97 e("ha", "は", "ハ"), e("hi", "ひ", "ヒ"), e("hu", "ふ", "フ"), e("he", "へ", "ヘ"), e("ho", "ほ", "ホ"),
98 e("fu", "ふ", "フ"), e("ba", "ば", "バ"), e("bi", "び", "ビ"), e("bu", "ぶ", "ブ"), e("be", "べ", "ベ"), e("bo", "ぼ", "ボ"),
100 e("pa", "ぱ", "パ"), e("pi", "ぴ", "ピ"), e("pu", "ぷ", "プ"), e("pe", "ぺ", "ペ"), e("po", "ぽ", "ポ"),
101 e("ma", "ま", "マ"), e("mi", "み", "ミ"), e("mu", "む", "ム"), e("me", "め", "メ"), e("mo", "も", "モ"),
102 e("ya", "や", "ヤ"), e("yu", "ゆ", "ユ"), e("yo", "よ", "ヨ"),
103 e("ra", "ら", "ラ"), e("ri", "り", "リ"), e("ru", "る", "ル"), e("re", "れ", "レ"), e("ro", "ろ", "ロ"),
104 e("wa", "わ", "ワ"), e("wo", "を", "ヲ"),
105 e("ja", "じゃ", "ジャ"), e("ju", "じゅ", "ジュ"), e("jo", "じょ", "ジョ"), e("fa", "ふぁ", "ファ"), e("fi", "ふぃ", "フィ"),
112 e("fe", "ふぇ", "フェ"), e("fo", "ふぉ", "フォ"),
113 e("va", "ゔぁ", "ヴァ"), e("vi", "ゔぃ", "ヴィ"),
116 e("vu", "ゔ", "ヴ"), e("ve", "ゔぇ", "ヴェ"),
117 e("vo", "ゔぉ", "ヴォ"),
118 e("wi", "うぃ", "ウィ"), e("we", "うぇ", "ウェ"),
120 e("je", "じぇ", "ジェ"),
122 e("xa", "ぁ", "ァ"), e("xi", "ぃ", "ィ"), e("xu", "ぅ", "ゥ"),
126 e("xe", "ぇ", "ェ"), e("xo", "ぉ", "ォ"),
127 e("la", "ぁ", "ァ"), e("li", "ぃ", "ィ"), e("lu", "ぅ", "ゥ"),
128 e("le", "ぇ", "ェ"), e("lo", "ぉ", "ォ"),
129 e("a", "あ", "ア"), e("i", "い", "イ"), e("u", "う", "ウ"), e("e", "え", "エ"), e("o", "お", "オ"),
137 e("-", "ー", "ー"), ];
139
140pub fn to_hiragana(s: &str) -> String {
145 render(s, false)
146}
147
148pub fn to_katakana(s: &str) -> String {
150 render(s, true)
151}
152
153fn render(s: &str, katakana: bool) -> String {
154 let lower = s.to_ascii_lowercase();
155 let bytes = lower.as_bytes();
156 let sokuon = if katakana { "ッ" } else { "っ" };
157 let n_str = if katakana { "ン" } else { "ん" };
158 let mut out = String::with_capacity(s.len() * 3);
159 let mut i = 0;
160 while i < bytes.len() {
161 if i + 1 < bytes.len()
167 && bytes[i] == bytes[i + 1]
168 && bytes[i] != b'n'
169 && is_gemini_consonant(bytes[i])
170 {
171 out.push_str(sokuon);
172 i += 1;
173 continue;
174 }
175 if bytes[i] == b'n' {
189 if i + 1 >= bytes.len() {
190 out.push_str(n_str);
191 i += 1;
192 continue;
193 }
194 let next = bytes[i + 1];
195 if next == b'n' && i + 2 >= bytes.len() {
196 out.push_str(n_str);
197 i += 2;
198 continue;
199 }
200 if !matches!(next, b'a' | b'i' | b'u' | b'e' | b'o' | b'y') {
201 out.push_str(n_str);
202 i += 1;
203 continue;
204 }
205 }
207 let mut matched_len = 0;
209 for entry in TABLE.iter() {
210 let pat = entry.rom.as_bytes();
211 if pat.len() <= bytes.len() - i && &bytes[i..i + pat.len()] == pat {
212 out.push_str(if katakana { entry.kana_k } else { entry.kana_h });
213 matched_len = pat.len();
214 break;
215 }
216 }
217 if matched_len == 0 {
218 out.push(bytes[i] as char);
222 i += 1;
223 } else {
224 i += matched_len;
225 }
226 }
227 out
228}
229
230const fn is_gemini_consonant(b: u8) -> bool {
231 matches!(
232 b,
233 b'k' | b'g' | b's' | b'z' | b'j' | b't' | b'd' | b'c'
234 | b'h' | b'f' | b'b' | b'p' | b'm' | b'r' | b'y' | b'w' | b'v'
235 )
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn basic_vowels() {
244 assert_eq!(to_hiragana("a"), "あ");
245 assert_eq!(to_hiragana("aiueo"), "あいうえお");
246 assert_eq!(to_katakana("aiueo"), "アイウエオ");
247 }
248
249 #[test]
250 fn basic_ka_row() {
251 assert_eq!(to_hiragana("ka"), "か");
252 assert_eq!(to_hiragana("kakikukeko"), "かきくけこ");
253 assert_eq!(to_katakana("ka"), "カ");
254 }
255
256 #[test]
257 fn voiced_and_semi_voiced() {
258 assert_eq!(to_hiragana("ga"), "が");
259 assert_eq!(to_hiragana("za"), "ざ");
260 assert_eq!(to_hiragana("ba"), "ば");
261 assert_eq!(to_hiragana("pa"), "ぱ");
262 }
263
264 #[test]
265 fn hepburn_and_kunrei_aliases() {
266 assert_eq!(to_hiragana("shi"), "し");
268 assert_eq!(to_hiragana("si"), "し");
269 assert_eq!(to_hiragana("chi"), "ち");
270 assert_eq!(to_hiragana("ti"), "ち");
271 assert_eq!(to_hiragana("tsu"), "つ");
272 assert_eq!(to_hiragana("tu"), "つ");
273 assert_eq!(to_hiragana("fu"), "ふ");
274 assert_eq!(to_hiragana("hu"), "ふ");
275 assert_eq!(to_hiragana("ji"), "じ");
276 assert_eq!(to_hiragana("zi"), "じ");
277 }
278
279 #[test]
280 fn youon_digraphs() {
281 assert_eq!(to_hiragana("kya"), "きゃ");
282 assert_eq!(to_hiragana("kyu"), "きゅ");
283 assert_eq!(to_hiragana("kyo"), "きょ");
284 assert_eq!(to_hiragana("sha"), "しゃ");
285 assert_eq!(to_hiragana("cha"), "ちゃ");
286 assert_eq!(to_hiragana("ja"), "じゃ");
287 assert_eq!(to_hiragana("nyo"), "にょ");
288 assert_eq!(to_katakana("rya"), "リャ");
289 }
290
291 #[test]
292 fn sokuon_gemination() {
293 assert_eq!(to_hiragana("kka"), "っか");
295 assert_eq!(to_hiragana("ssa"), "っさ");
297 assert_eq!(to_hiragana("xtsu"), "っ");
299 assert_eq!(to_hiragana("xtu"), "っ");
300 assert_eq!(to_hiragana("nn"), "ん");
302 }
303
304 #[test]
305 fn moraic_n_basic() {
306 assert_eq!(to_hiragana("n"), "ん");
308 assert_eq!(to_hiragana("na"), "な");
310 assert_eq!(to_hiragana("nn"), "ん");
312 }
313
314 #[test]
315 fn moraic_n_annai_pattern() {
316 assert_eq!(to_hiragana("nna"), "んな");
320 assert_eq!(to_hiragana("annai"), "あんない");
321 assert_eq!(to_hiragana("ganbaru"), "がんばる");
322 assert_eq!(to_hiragana("konnichiwa"), "こんにちわ");
326 }
327
328 #[test]
329 fn chouonpu_long_mark() {
330 assert_eq!(to_hiragana("ka-"), "かー");
331 assert_eq!(to_katakana("ko-hi-"), "コーヒー");
332 }
333
334 #[test]
335 fn incomplete_romaji_passes_through() {
336 assert_eq!(to_hiragana("k"), "k");
339 assert_eq!(to_hiragana("sh"), "sh");
340 }
341
342 #[test]
343 fn multi_syllable_word() {
344 assert_eq!(to_hiragana("nihon"), "にほん");
345 assert_eq!(to_hiragana("tokyo"), "ときょ");
346 assert_eq!(to_hiragana("nippon"), "にっぽん");
347 assert_eq!(to_katakana("kohi-"), "コヒー");
348 }
349
350 #[test]
351 fn case_insensitive() {
352 assert_eq!(to_hiragana("KA"), "か");
353 assert_eq!(to_hiragana("Nihon"), "にほん");
354 }
355
356 #[test]
357 fn foreign_fa_row() {
358 assert_eq!(to_katakana("fa"), "ファ");
362 assert_eq!(to_katakana("famiriaare"), "ファミリアアレ");
363 assert_eq!(to_katakana("fi"), "フィ");
364 assert_eq!(to_katakana("fe"), "フェ");
365 assert_eq!(to_katakana("fo"), "フォ");
366 assert_eq!(to_hiragana("fa"), "ふぁ");
368 assert_eq!(to_hiragana("famiriaare"), "ふぁみりああれ");
369 }
370
371 #[test]
372 fn foreign_va_row() {
373 assert_eq!(to_katakana("va"), "ヴァ");
374 assert_eq!(to_katakana("vi"), "ヴィ");
375 assert_eq!(to_katakana("vu"), "ヴ");
376 assert_eq!(to_katakana("ve"), "ヴェ");
377 assert_eq!(to_katakana("vo"), "ヴォ");
378 assert_eq!(to_katakana("vaiorin"), "ヴァイオリン");
380 }
381
382 #[test]
383 fn foreign_w_row() {
384 assert_eq!(to_katakana("wi"), "ウィ");
386 assert_eq!(to_katakana("we"), "ウェ");
387 assert_eq!(to_katakana("whi"), "ウィ");
388 assert_eq!(to_katakana("whe"), "ウェ");
389 assert_eq!(to_katakana("wha"), "ウァ");
390 assert_eq!(to_katakana("who"), "ウォ");
391 assert_eq!(to_katakana("wedhingu"), "ウェディング");
393 assert_eq!(to_katakana("wedingu"), "ウェヂング");
396 }
397
398 #[test]
399 fn foreign_je_che_she() {
400 assert_eq!(to_katakana("je"), "ジェ");
401 assert_eq!(to_katakana("che"), "チェ");
402 assert_eq!(to_katakana("she"), "シェ");
403 assert_eq!(to_katakana("jetto"), "ジェット");
405 assert_eq!(to_katakana("sheiku"), "シェイク");
407 }
408
409 #[test]
410 fn foreign_ts_row() {
411 assert_eq!(to_katakana("tsa"), "ツァ");
412 assert_eq!(to_katakana("tsi"), "ツィ");
413 assert_eq!(to_katakana("tse"), "ツェ");
414 assert_eq!(to_katakana("tso"), "ツォ");
415 }
416
417 #[test]
418 fn foreign_thi_dhi_twu_dwu() {
419 assert_eq!(to_katakana("thi"), "ティ");
421 assert_eq!(to_katakana("dhi"), "ディ");
422 assert_eq!(to_katakana("twu"), "トゥ");
423 assert_eq!(to_katakana("dwu"), "ドゥ");
424 assert_eq!(to_katakana("pa-thi-"), "パーティー");
426 }
427
428 #[test]
429 fn foreign_kwa_gwa_rows() {
430 assert_eq!(to_katakana("kwa"), "クァ");
431 assert_eq!(to_katakana("kwi"), "クィ");
432 assert_eq!(to_katakana("kwe"), "クェ");
433 assert_eq!(to_katakana("kwo"), "クォ");
434 assert_eq!(to_katakana("gwa"), "グァ");
435 }
436
437 #[test]
438 fn foreign_fy_vy_yoon() {
439 assert_eq!(to_katakana("fya"), "フャ");
440 assert_eq!(to_katakana("fyu"), "フュ");
441 assert_eq!(to_katakana("vya"), "ヴャ");
442 assert_eq!(to_katakana("vyu"), "ヴュ");
443 }
444
445 #[test]
446 fn small_kana_explicit() {
447 assert_eq!(to_hiragana("xa"), "ぁ");
449 assert_eq!(to_hiragana("xi"), "ぃ");
450 assert_eq!(to_hiragana("xu"), "ぅ");
451 assert_eq!(to_hiragana("xe"), "ぇ");
452 assert_eq!(to_hiragana("xo"), "ぉ");
453 assert_eq!(to_hiragana("la"), "ぁ");
454 assert_eq!(to_katakana("xa"), "ァ");
455 assert_eq!(to_katakana("la"), "ァ");
456 }
457
458 #[test]
459 fn pre_existing_kunrei_di_du_unchanged() {
460 assert_eq!(to_hiragana("di"), "ぢ");
464 assert_eq!(to_hiragana("du"), "づ");
465 assert_eq!(to_hiragana("ti"), "ち");
466 assert_eq!(to_hiragana("tu"), "つ");
467 }
468}