Panda Noir

JavaScript の限界を究めるブログです。

ひらがなからローマ字への変換可能パターンを列挙するプログラムをつくった

タイピングソフトを作っていたときに、かなり使えそうなローマ字変換プログラムをつくれたので単体で公開することにしました。たぶんほぼ入力可能な方法は網羅できていると思います。

プログラム概要

たとえば「ちゃっぷりん」を変換するとき、「tyappurinn」ともできますし、「chixyaxtsupurinn」とも入力できます。誰がこんな変態入力するんだ

こうなってくると全パターンを列挙したくなるのがプログラマの性です。しかし、本当の意味で全パターンを列挙するのはアホらしいですよね。「『ちゃ』はtya、tixya、cha、chixyaで入力できる」というように各文字の可能パターンを列挙すれば十分です。

このように各文字の可能パターンを列挙するgetRoman関数を作りました。コードは最後に載せてあります。

使い方

実際に「ちゃっぷりん」を変換してみます。

const str = 'ちゃっぷりん';
let i = 0;
let res = '', difficult;
while (i < str.length) {
    const _res = getRoman(str, i);
    res += _res[0][0]; // 入力可能な中で一番ポピュラーっぽいものを選ぶ
    difficult += _res[0].pop(); // 入力可能入力可能な中で一番めんどくさそうなものを選ぶ
    i += _res[1]; // 変換対象となった文字数の分だけ進める
}
console.log(res);
console.log(difficult);

実行すると

tyappurinn
chixyaxtsupurinn

と出力されます。もちろん全パターン列挙もできます。

const str = 'ちゃっぷりん';
let i = 0;
let res = [''];
while (i < str.length) {
    const [pattern, count] = getRoman(str, i);
    const _res = [];
    for (let j = 0; j < pattern.length; j++)
        _res.push(...res.map(item => item + pattern[j]));
    res = _res;
    i += count;
}
console.log(res);

実行結果はこんな感じです。

[ 'tyappurinn',
  'chappurinn',
  'tixyappurinn',
  'chixyappurinn',
  'tyaxtupurinn',
  'chaxtupurinn',
  'tixyaxtupurinn',
  'chixyaxtupurinn',
  'tyaxtsupurinn',
  'chaxtsupurinn',
  'tixyaxtsupurinn',
  'chixyaxtsupurinn' ]

仕様について

簡単な仕様説明をします。

  • 「っ」の後ろに文字がない、あるいは「っ」の後ろがアルファベットや数字、記号の場合、「「xtu、xtsu」で入力できる」という結果を返す
  • 「ん」を入力する際、直後に「ナ行」「ア行」「ヤ行」が続く、または後ろに文字がない場合、「nnで入力できる」という結果を返す
  • 「ん」を入力する際、上記以外の場合、「nで入力できる」という結果を返す
  • 「ん」の直後で、「ナ行」「ア行」「ヤ行」でない、たとえば「か」の場合、「「ka、nka」で入力できる」という結果を返す

あとはふつうに返します。4番についてのみ解説したいと思います。

たとえば「あんかー」と入力する場合、「anka-」「annka-」のどちらでも入力できます。しかし、「ん」が「n、nnで入力できる」としてしまうとタイピングソフトをつくるときに不都合が生じてしまいます。そこで、「ん」はnのみで入力できるとして、次の「か」が「ka、nkaで入力できる」というようにしました。

コード

const isSmallChar = next => [...'ぁぃぅぇぉゃゅょ'].includes(next);
const add = n => item => n + item;
const romanTable = {'を': 'wo',
    'しゃ': 'sya,sha,sixya,shixya', 'しゅ': 'syu,shu,sixyu,shixyu',
    'しぇ': 'sye,she,sixye,shixye', 'しょ': 'syo,sho,sixyo,shixyo',
    'ちゃ': 'tya,cha,tixya,chixya', 'ちゅ': 'tyu,chu,tixyu,chixyu',
    'ちぇ': 'tye,che,tixye,chixye', 'ちょ': 'tyo,cho,tixyo,chixyo',
    'じゃ': 'ja,zya,jixya,zixya', 'じゅ': 'ju,zyu,jixyu,zixyu',
    'じぇ': 'je,zye,jixe,zixe', 'じょ': 'jo,zyo,jixyo,zixyo',
    'てぃ': 'thi,texi', 'てぇ': 'the,texe', 'ふぁ': 'fa,fuxa,huxa',
    'ふぃ': 'fi,fuxi,huxi', 'ふぇ': 'fe,fuxe,huxe',
    'ふぉ': 'fo,fuxo,huxo', 'ゔぁ': 'va,vuxa',
    'ゔぃ': 'vi,vuxi', 'ゔ': 'vu', 'ゔぇ': 've,vuxe', 'ゔぉ': 'vo,vuxo',
    'うぁ': 'uxa,wha', 'うぃ': 'wi,uxi,whi', 'うぇ': 'we,uxe,whe',
    'うぉ': 'uxo,who',
    'くぁ': 'kwa,kuxa',
    'ぐぁ': 'gwa,guxa', 'ぐぃ': 'gwi,guxi',
    'ぐぅ': 'gwu,guxu', 'ぐぇ': 'gwe,guxe',
    'ぐぉ': 'gwo,guxo',
    'つぁ': 'tsa,tuxa,tsuxa', 'つぃ': 'tsi,tuxi,tsuxi',
    'つぇ': 'tse,tuxe,tsuxe', 'つぉ': 'tso,tuxo,tsuxo',
    'とぁ': 'twa,toxa', 'とぃ': 'twi,toxi',
    'とぅ': 'twu,toxu', 'とぇ': 'twe,toxe',
    'とぉ': 'two,toxo',
    'でぃ': 'dhi,dexi', 'どぅ': 'dwu,doxu',
    'ゐ': 'wyi', 'ゑ': 'wye',
    'ー': '-', '。': '.'
};

for (const key of Object.keys(romanTable)) romanTable[key] = romanTable[key].split(',');
for (const val of 'abcdefghijklmnopqrstuvwxyz0123456789- ,:(){}.・!&%') {
    romanTable[val] = [val];
    romanTable[val.toUpperCase()] = [val.toUpperCase()];
}
romanTable['ヴぁ'] = romanTable['ゔぁ']; romanTable['ヴぃ'] = romanTable['ゔぃ'];
romanTable['ヴ'] = romanTable['ゔ']; romanTable['ヴぇ'] = romanTable['ゔぇ'];
romanTable['ヴぉ'] = romanTable['ゔぉ'];

const consonant = {
    'し': 's,sh', 'ち': 't,ch',
    'つ': 't,ts', 'ふ': 'h,f',
    'じ': 'z,j',
};
// 基本的なローマ字表を構築する
for (const [hiraganas, cons] of [
    ['あいうえお', ''], ['かきくけこ', 'k'],
    ['さしすせそ', 's'], ['たちつてと', 't'],
    ['なにぬねの', 'n'], ['はひふへほ', 'h'],
    ['まみむめも', 'm'], ['やゆよ', 'y'],
    ['らりるれろ', 'r'], ['わ', 'w'],
    ['がぎぐげご', 'g'], ['ざじずぜぞ', 'z'],
    ['だぢづでど', 'd'], ['ばびぶべぼ', 'b'],
    ['ぱぴぷぺぽ', 'p']]) {
    for (let i = 0, _i = hiraganas.length; i < _i; i++) {
        const hiragana = hiraganas[i];
        if (!consonant[hiragana]) consonant[hiragana] = cons;
        romanTable[hiragana] = consonant[hiragana].split(',').map(c => c + 'aiueo'[i]);
    }
}
romanTable['ゆ'] = ['yu'];
romanTable['よ'] = ['yo'];

const getRoman = (furigana, targetPos) => {
    // ローマ字の取得
    // furiganaのtargetPosの位置を取得
    // 結果は配列の形式で返す
    // [[ローマ字], 変換対象となる文字数]
    furigana = [...furigana];
    let result = new Array();
    const nowChar = furigana[targetPos],
        nextChar = furigana[targetPos + 1];
    if (isSmallChar(nextChar) && romanTable[nowChar + nextChar]) result = [romanTable[nowChar + nextChar].concat(), 2]; // 「じゃ」 などromanTableに登録されている場合
    else if (isSmallChar(nowChar)) {
        // 拗音単独の場合
        result = [[[...'aiueo', 'ya', 'yu', 'yo'].map(s => 'x' + s)[[...'ぁぃぅぇぉゃゅょ'].includes(nowChar)]], 1];
    } else if ([...'ぃぇゃゅょ'].includes(nextChar)){
        // 次が拗音の場合
        const smallChar = {
            'ぃ': ['yi', 'ixi'],
            'ぇ': ['ye', 'ixe'],
            'ゃ': ['ya', 'ixya'],
            'ゅ': ['yu', 'ixyu'],
            'ょ': ['yo', 'ixyo']
        }[nextChar];
        for (const cons of consonant[nowChar].split(',')) result.push(cons + smallChar[0], cons + smallChar[1]);
        romanTable[nowChar + nextChar] = result.concat();
        result = [result, 2];
    } else if (nowChar === 'ん') {
        // 今の文字が「ん」の場合
        // 必要最低限のnで返す
        result = ['nn'];
        if (nextChar !== '' && (consonant[nextChar] === undefined || !['n', '', 'y'].includes(consonant[nextChar])))
            result = ['n']; // 「んな」「んや」「んあ」でない、または後ろが記号のケース
        result = [result, 1];
    } else if (nowChar === 'っ') {
        // いまの文字が「っ」の場合
        result = [['xtu', 'xtsu'], 1]; // 「女神さまっ」や「女神さまっ2」のように、後ろが存在しないか記号のケース
        if (nextChar !== '' && consonant[nextChar] !== undefined) {
            const [_res, count] = getRoman(furigana, targetPos + 1);
            result = [[..._res.map(item => item[0] + item), ..._res.map(add('xtu')), ..._res.map(add('xtsu'))], count + 1];
        }
    } else result = [romanTable[nowChar].concat(), 1]; // 普通のとき
    if (furigana[targetPos - 1] === 'ん' && !['n', '', 'y'].includes(consonant[nowChar])) {
        // ここはnを足す処理
        // たとえば「しんくろにしてぃーん」で「shin」と入力したとき、次はnでもよいし、sでもよい。
        // ただ、「ん」のほうにnを付け加えるより、うしろの「く」を便宜上「'ku'でも'nku'でもよい」としたほうが便利。
        // このnを足す処理を行う
        result[0] = result[0].concat(result[0].map(add('n')));
    }
    return [result[0], result[1]];
};