bunpo 0.1.1

Lightweight Japanese conjugation reversal (deinflection) system
Documentation
use std::collections::HashMap;

pub fn to_hiragana(input: &str) -> String {
    let mut result = String::new();
    let mut is_katakana = false;
    for c in input.chars() {
        if ('\u{30A1}'..='\u{30F6}').contains(&c) {
            let hira = std::char::from_u32(c as u32 - 0x60).unwrap_or(c);
            result.push(hira);
            is_katakana = true;
        } else {
            result.push(c);
        }
    }
    if is_katakana {
        return result;
    }

    romaji_to_hiragana(input)
}

fn romaji_to_hiragana(input: &str) -> String {
    let table = romaji_hiragana_table();
    let mut result = String::new();
    let mut i = 0;
    let chars: Vec<char> = input.to_lowercase().chars().collect();

    while i < chars.len() {
        let mut matched = false;
        for len in (1..=3).rev() {
            if i + len <= chars.len() {
                let slice: String = chars[i..i + len].iter().collect();
                if let Some(hira) = table.get(slice.as_str()) {
                    result.push_str(hira);
                    i += len;
                    matched = true;
                    break;
                }
            }
        }
        if !matched {
            if i + 1 < chars.len()
                && chars[i] == chars[i + 1]
                && is_consonant(chars[i])
                && chars[i] != 'n'
            {
                result.push('');
                i += 1;
            } else if chars[i] == 'n' {
                if i + 1 == chars.len() || !is_vowel(chars[i + 1]) {
                    result.push('');
                    i += 1;
                } else {
                    i += 1;
                }
            } else {
                result.push(chars[i]);
                i += 1;
            }
        }
    }
    result
}

fn is_consonant(c: char) -> bool {
    matches!(
        c,
        'b' | 'c'
            | 'd'
            | 'f'
            | 'g'
            | 'h'
            | 'j'
            | 'k'
            | 'l'
            | 'm'
            | 'p'
            | 'q'
            | 'r'
            | 's'
            | 't'
            | 'v'
            | 'w'
            | 'x'
            | 'z'
    )
}

fn is_vowel(c: char) -> bool {
    matches!(c, 'a' | 'i' | 'u' | 'e' | 'o')
}

fn romaji_hiragana_table() -> HashMap<&'static str, &'static str> {
    // non-exhaustive table, but covers most common syllables and digraphs
    let mut m = HashMap::new();
    // Vowels
    m.insert("a", "");
    m.insert("i", "");
    m.insert("u", "");
    m.insert("e", "");
    m.insert("o", "");
    // K
    m.insert("ka", "");
    m.insert("ki", "");
    m.insert("ku", "");
    m.insert("ke", "");
    m.insert("ko", "");
    m.insert("kya", "きゃ");
    m.insert("kyu", "きゅ");
    m.insert("kyo", "きょ");
    // S
    m.insert("sa", "");
    m.insert("shi", "");
    m.insert("su", "");
    m.insert("se", "");
    m.insert("so", "");
    m.insert("sha", "しゃ");
    m.insert("shu", "しゅ");
    m.insert("sho", "しょ");
    // T
    m.insert("ta", "");
    m.insert("chi", "");
    m.insert("tsu", "");
    m.insert("te", "");
    m.insert("to", "");
    m.insert("cha", "ちゃ");
    m.insert("chu", "ちゅ");
    m.insert("cho", "ちょ");
    // N
    m.insert("na", "");
    m.insert("ni", "");
    m.insert("nu", "");
    m.insert("ne", "");
    m.insert("no", "");
    m.insert("nya", "にゃ");
    m.insert("nyu", "にゅ");
    m.insert("nyo", "にょ");
    // H
    m.insert("ha", "");
    m.insert("hi", "");
    m.insert("fu", "");
    m.insert("he", "");
    m.insert("ho", "");
    m.insert("hya", "ひゃ");
    m.insert("hyu", "ひゅ");
    m.insert("hyo", "ひょ");
    // M
    m.insert("ma", "");
    m.insert("mi", "");
    m.insert("mu", "");
    m.insert("me", "");
    m.insert("mo", "");
    m.insert("mya", "みゃ");
    m.insert("myu", "みゅ");
    m.insert("myo", "みょ");
    // Y
    m.insert("ya", "");
    m.insert("yu", "");
    m.insert("yo", "");
    // R
    m.insert("ra", "");
    m.insert("ri", "");
    m.insert("ru", "");
    m.insert("re", "");
    m.insert("ro", "");
    m.insert("rya", "りゃ");
    m.insert("ryu", "りゅ");
    m.insert("ryo", "りょ");
    // W
    m.insert("wa", "");
    m.insert("wo", "");
    // G
    m.insert("ga", "");
    m.insert("gi", "");
    m.insert("gu", "");
    m.insert("ge", "");
    m.insert("go", "");
    m.insert("gya", "ぎゃ");
    m.insert("gyu", "ぎゅ");
    m.insert("gyo", "ぎょ");
    // Z
    m.insert("za", "");
    m.insert("ji", "");
    m.insert("zu", "");
    m.insert("ze", "");
    m.insert("zo", "");
    m.insert("ja", "じゃ");
    m.insert("ju", "じゅ");
    m.insert("jo", "じょ");
    // D
    m.insert("da", "");
    m.insert("di", "");
    m.insert("du", "");
    m.insert("de", "");
    m.insert("do", "");
    // B
    m.insert("ba", "");
    m.insert("bi", "");
    m.insert("bu", "");
    m.insert("be", "");
    m.insert("bo", "");
    m.insert("bya", "びゃ");
    m.insert("byu", "びゅ");
    m.insert("byo", "びょ");
    // P
    m.insert("pa", "");
    m.insert("pi", "");
    m.insert("pu", "");
    m.insert("pe", "");
    m.insert("po", "");
    m.insert("pya", "ぴゃ");
    m.insert("pyu", "ぴゅ");
    m.insert("pyo", "ぴょ");
    // Small tsu
    m.insert("xtsu", "");
    m.insert("ltsu", "");
    // N
    m.insert("n", "");
    // Special
    m.insert("vu", "");
    m
}

/// Returns true if the character is a hiragana character.
pub fn is_hiragana(c: char) -> bool {
    ('\u{3040}'..='\u{309F}').contains(&c)
}

pub fn is_kana_word(word: &str) -> bool {
    word.chars().all(|c| is_hiragana(c))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_hiragana() {
        assert_eq!(is_hiragana(''), true);
        assert_eq!(is_hiragana('a'), false);
    }

    #[test]
    fn test_is_kana_word() {
        assert_eq!(is_kana_word(""), true);
        assert_eq!(is_kana_word("漢字"), false);
    }

    #[test]
    fn test_romaji_to_hiragana_basic() {
        assert_eq!(to_hiragana("ike"), "いけ");
        assert_eq!(to_hiragana("イケ"), "いけ");
        assert_eq!(to_hiragana("いけ"), "いけ");
        assert_eq!(to_hiragana("zubon"), "ずぼん");
        assert_eq!(to_hiragana("durai"), "づらい");
    }

    #[test]
    fn test_romaji_to_hiragana_small_tsu() {
        assert_eq!(to_hiragana("itt"), "いっt");
        assert_eq!(to_hiragana("itte"), "いって");
        assert_eq!(to_hiragana("ittt"), "いっっt");
        assert_eq!(to_hiragana("ittte"), "いっって");
        assert_eq!(to_hiragana("itttte"), "いっっって");
    }
}