Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

ヘボン式のローマ字を変換するプログラム

(この記事はQiitaで僕が書いたものを移行した記事です。記事中のコメントはQiitaの該当記事を参照ください)

ググっても出てこなかったのでドラマウロボロス聞きながら作りました。そのせいでif入り乱れのスパゲッティです。あしからず。

※ヘボン式自体が不可逆圧縮のため一部の文字列はひらがな->ローマ字->ひらがなと変換しても元の文字列に完全には戻せません。

//SHORYAKU=falseの場合
hiraganaToRome("とうきょう");//tokyo

hiraganaToRome("おおさか");//osaka

hiraganaToRome("おおの");//ono

romeToHiragana(hiraganaToRome("とうきょう"));//ときょ
romeToHiragana(hiraganaToRome("おおさか"));//おさか
romeToHiragana(hiraganaToRome("おおの"));//おの
//SHORYAKU=trueの場合
hiraganaToRome("とうきょう");//to^kyo^

hiraganaToRome("おおさか");//o^saka

hiraganaToRome("おおの");//o^no

romeToHiragana(hiraganaToRome("とうきょう"));//とおきょお
romeToHiragana(hiraganaToRome("おおさか"));//おおさか
romeToHiragana(hiraganaToRome("おおの"));//おおの

使用方法

romeToHiragana(str) 関数がローマ字をひらがなに、 hiraganaToRome(str) 関数がひらがなをローマ字に変換します。

注意点

ここで注意点がひとつあります。このヘボン式変換機はひらがな->ローマ字->ひらがなと変換すると、元どおりにならないことがあります。

具体的には「おおの」、「きょうと」「おの」です。これらをローマ字に変換すると「ONO」、「KYOTO」、「ONO」となります。 つまり、OO、OUがOに変換されてしまいます。このせいでローマ字からひらがなに戻すときに「ONO」は「おの」に、「KYOTO」は「きょと」になります。

SHORYAKU変数をtrueにすると「おおの」が「O^NO」と変換されるようになり、多少マシになるようにしています。しかし、これも「とうきょう」が「TO^KYO^」、「おおさか」が「O^SAKA」になり、ひらがなに戻すと「とおきょお」都もしくは「おうさか」府が誕生してしまいます。どちらも正しく戻せません。OUもOOもOに変換するヘボン式が悪いのです。私のせいではないのです。

というわけでOU、OOがOになるという不可逆圧縮がかかっていることに注意して使用してください。

コード

var SHORYAKU = true;
var table = {
    'none': 'あいうえお', 'k': 'かきくけこ', 'g': 'がぎぐげご', 's': 'さxすせそ', 'z': 'ざじずぜぞ', 't': 'たxxてと',
    'd': 'だぢづでど', 'n': 'なにぬねの', 'h': 'はひxへほ', 'b': 'ばびぶべぼ', 'p': 'ぱぴぷぺぽ', 'm': 'まみむめも',
    'y': 'やxゆxよ', 'r': 'らりるれろ', 'w': 'わゐxゑを'
};
var consonant = {};
for (var key in table) {
    var arr = table[key].split('');
    for (var i = 0, _i = arr.length; i < _i; i++) {
        if (arr[i] === 'x') continue;
        consonant[arr[i]] = key;
    }
}
consonant['し'] = 'sh';
consonant['ち'] = 'ch';
var smallChar = /[ゃゅょ]/;
function isConsonant(s) { return /[kgsztdnhbpmyrw]/.test(s); }
function isVowel(s) { return /[aiueo]/.test(s); }
function YAYUYO(str) { return 'ゃゅょ'.charAt('auo'.indexOf(str)); }
function romeToHiragana(str) {
    var res = '';
    if (SHORYAKU) str = str.replace(/(.)\^/g, function(_, a) {
        //ここを変えることで「おうさか」府、「とおきょお」都問題をいじれる
        return a + a;//おおさか
        //return a+'u';//とうきょう
    });
    for (var i = 0, _i = str.length; i < _i; i++) {
        if (isConsonant(str.charAt(i))) {
            if (str.charAt(i) === 'm' && (str.charAt(i + 1) === 'b' || str.charAt(i + 1) === 'm' || str.charAt(i + 1) === 'p') || str.charAt(i) === 'n' && isConsonant(str.charAt(i + 1))) {
                //namba or kanno
                return res + 'ん' + romeToHiragana(str.slice(i + 1));
            }else if (str.charAt(i + 1) === str.charAt(i)) {
                //促音
                return res + 'っ' + romeToHiragana(str.slice(i + 1));
            }else if (str.charAt(i) === 't' && str.charAt(i + 1) === 'c' && str.charAt(i + 2) === 'h') {
                //特殊な促音
                res += 'っち';
                res += YAYUYO(str.charAt(i + 3));
                return res + romeToHiragana(str.slice(i + 4));
            }else if (str.charAt(i + 1) === 'y') {
                //kyotoのkyo部分
                res += table[str.charAt(i)].charAt(1);
                res += YAYUYO(str.charAt(i + 2));
                i += 2;
            }else {
                //普通の子音+母音
                if (str.slice(i, i + 2) === 'sh' || str.slice(i, i + 2) === 'ch') {
                    if ('auo'.indexOf(str.charAt(i + 2)) !== -1) {
                        res += str.slice(i, i + 2) === 'sh' ? 'し' : 'ち';
                        res += YAYUYO(str.charAt(i + 2));
                    }else {
                        //sheは考慮しない
                        res += str.slice(i, i + 2) === 'sh' ? 'し' : 'ち';
                    }
                    i += 2;
                }else {
                    if (isVowel(str.charAt(i + 1))) {
                        res += table[str.charAt(i)].charAt('aiueo'.indexOf(str.charAt(i + 1)));
                        i += 1;
                    }else {
                        throw 'unexpected ' + str.slice(i);
                    }
                }
            }
        }else if (isVowel(str.charAt(i))) {
            res += 'あいうえお'.charAt('aiueo'.indexOf(str.charAt(i)));
        }
        //撥音
    }
    return res;
}
function hiraganaToRome(str) {
    var res = '';
    for (var i = 0, _i = str.length; i < _i; i++) {
        if (str.charAt(i) === 'ん') {
            if (consonant[str.charAt(i + 1)] === 'b' || consonant[str.charAt(i + 1)] === 'm' || consonant[str.charAt(i + 1)] === 'p') {
                res += 'm';
            }else {
                res += 'n';
            }
        }else if (str.charAt(i) === 'っ') {
            //直後がCH(=ち)?T:直後の子音
            if (str.charAt(i + 1)) {
                if (str.charAt(i + 1) === 'ち') {
                    res += 't';
                }else {
                    res += consonant[str.charAt(i + 1)].charAt(0);
                }
            }else throw Error('っ');
        }else if (str.charAt(i + 1) && smallChar.test(str.charAt(i + 1))) {
            if (str.charAt(i) === 'し' || str.charAt(i) === 'ち') {
                res += consonant[str.charAt(i)];
            }else if (str.charAt(i) === 'じ') {
                res += 'j';
            }else {
                res += consonant[str.charAt(i)] + 'y';
            }
            res += ['a', 'u', 'o']['ゃゅょ'.indexOf(str.charAt(i + 1))];
            i += 1;
        }else if (str.charAt(i) === 'し') {
            res += 'shi';
        }else if (str.charAt(i) === 'ち') {
            res += 'chi';
        }else if (str.charAt(i) === 'つ') {
            res += 'tsu';
        }else if (str.charAt(i) === 'ふ') {
            res += 'fu';
        }else {
            if (consonant[str.charAt(i)] !== 'none') res += consonant[str.charAt(i)];
            res += 'aiueo'.charAt(table[consonant[str.charAt(i)]].indexOf(str.charAt(i)));
        }
    }
    res = res.replace(/oo/g, 'o' + (SHORYAKU ? '^' : ''))
    .replace(/uu/g, 'u' + (SHORYAKU ? '^' : ''))
    .replace(/ou/g, 'o' + (SHORYAKU ? '^' : ''));
    return res;
}