Panda Noir

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

リーダブルコードを読んだらコードの質が格段に上がった

リーダブルコードを読む前に書いたコードを見たら、ゴミすぎて全部書き直しました。

今回はこちらのコードを書き直しました。

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

書き直し1: 一度に1つのことを

getRoman関数は2つの処理を行っています。

  1. ローマ字にする
  2. 直前の文字が「ん」の場合にnを足す

これが一緒くたになっていて、とても読みづらいです。そこで、ローマ字にする部分をhiraganaToRoman関数として抜き出しました。

書き直し2: 中間結果を削除する

書き直し前のコードはresultという中間結果を保持していました。しかし、関数として抜き出したため、returnすれば良くなりました。そのため、elseを使う必要もなくなり、読みやすさが格段にあがりました。

書き直し前

        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 === 'ん') {

書き直し後

        const romanOfSmallChar = {
            'ぃ': ['yi', 'ixi'],
            'ぇ': ['ye', 'ixe'],
            'ゃ': ['ya', 'ixya'],
            'ゅ': ['yu', 'ixyu'],
            'ょ': ['yo', 'ixyo']
        }[second];

        const romans = [];
        for (const cons of consonant[first].split(','))
            romans.push(...romanOfSmallChar.map(roman => `${cons}${roman}`));
        romanTable[first+second] = romans.concat(); // キャッシュする
        return [romans, 2];
    }
    if (nowChar === 'ん') {

書き直し3: より優雅な手法を見つける

「ん」のローマ字について以下のように設定しました。

  • nだけで入力できる場合はn
  • nnまで入れなければいけない場合はnn(つぎが「や」「な」「あ」などの場合)

書き直し前

書き直し前のコードは「子音がない(アルファベット単体など)か、子音がnまたはyまたはないわけではない」かつ「つぎの文字がから文字でない」ときにnを返すとなっていました。

    if (nextChar !== '' && (consonant[nextChar] === undefined || !['n', '', 'y'].includes(consonant[nextChar])))
        return [['n'], 1]; // 「んな」「んや」「んあ」でない、または後ろが記号のケース
    return [['nn'], 1];

書き直し後

このわけのわからないロジックを簡潔にしました。

  • つぎがアルファベットならnを返す
  • つぎが空文字ならnnを返す
  • つぎの文字が「な」や「や」ならnnを返す
  • それ以外ならnを返す
    if (isAlphabet(second)) return [['n'], 1];
    if (second === '') return [['nn'], 1];
    if ('ny'.includes(consonant[second])) // consonant[second] === ''も含む
        return [['nn'], 1]; // 「んな」「んや」「んあ」のとき
    return [['n'], 1];

見やすく、理解もしやすくなっています。

まとめ

書き直した結果、コード全体は長くなってしまいました。しかし、コードをざっと見るだけでも、どのような処理が行われているのか分かるようになりました。

そのほかにも、コードを読めばあきらかなところで変な関数分割が起きていたり、変数名がわかりづらかったりしたので直しました。

全体書き直し後

const isAlphabet = char => {
    if (char === '') return false;
    if ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789- ,:(){}.・!&%'.includes(char)) return true;
    return false;
}

const romanTable = {
    'を': ['wo'],
    'うぁ': ['uxa','wha'],'うぃ': ['wi','uxi','whi'],'うぇ': ['we','uxe','whe'],'うぉ': ['uxo','who'],
    // 長いので省略
};

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++) {
        if (!consonant[hiraganas[i]])
            consonant[hiraganas[i]] = cons;
        romanTable[hiraganas[i]] = consonant[hiraganas[i]].split(',').map(cons => cons + 'aiueo'[i]);
    }
}
romanTable['ゆ'] = ['yu']; romanTable['よ'] = ['yo'];

const hiraganaToRoman = hiragana => {
    // hiraganaToRoman('しゃ') == [['sya', 'sha', 'sixya', 'shixya'], 2]
    // hiraganaToRoman('っぷ') == [['ppu', 'xtupu', 'xtsupu'], 2]

    if (hiragana === '') return [[''], 0];

    const first = [...hiragana][0] || '',
          second =  [...hiragana][1] || '';

    if (second !== '' && 'ぁぃぅぇぉゃゅょ'.includes(second)) {
        if (romanTable[first+second])
            return [romanTable[first+second].concat(), 2];
        // キャッシュがない場合の処理

        const romanOfSmallChar = {
            'ぃ': ['yi', 'ixi'],
            'ぇ': ['ye', 'ixe'],
            'ゃ': ['ya', 'ixya'],
            'ゅ': ['yu', 'ixyu'],
            'ょ': ['yo', 'ixyo']
        }[second];

        const romans = [];
        for (const cons of consonant[first].split(','))
            romans.push(...romanOfSmallChar.map(roman => `${cons}${roman}`));
        romanTable[first+second] = romans.concat(); // キャッシュする
        return [romans, 2];
    }
    if (first !== '' && 'ぁぃぅぇぉゃゅょ'.includes(first))
        switch (first) {
        case 'ぁ': return [['xa'], 1];
        case 'ぃ': return [['xi'], 1];
        case 'ぅ': return [['xu'], 1];
        case 'ぇ': return [['xe'], 1];
        case 'ぉ': return [['xo'], 1];
        case 'ゃ': return [['xya'], 1];
        case 'ゅ': return [['xyu'], 1];
        case 'ょ': return [['xyo'], 1];
        case 'ヵ': case 'ゕ': return [['xka'], 1];
        case 'ヶ': case 'ゖ': return [['xke'], 1];
        case 'ゎ': case 'ヮ': return [['xwa'], 1];
        }
    if (first === 'ん') {
        // 今の文字が「ん」の場合
        // 最低限入力しなければならない文字数のみ返す
        // 余分なもの("あんこ"に対するan'n'ko)は後ろの文字にくっつける(nkoと解釈する)

        if (isAlphabet(second)) return [['n'], 1];
        if (second === '') return [['nn'], 1];
        if ('ny'.includes(consonant[second])) // consonant[second] === ''も含む
            return [['nn'], 1]; // 「んな」「んや」「んあ」のとき
        return [['n'], 1];
    }
    if (first === 'っ') {
        // 「女神さまっ」や「女神さまっ2」のように、後ろが存在しないか記号のケース
        if (second === '' || isAlphabet(second))
            return [['xtu', 'xtsu'], 1];
        const [romanOfSecond, count] = hiraganaToRoman(hiragana.slice(1));
        return [
            [ ...romanOfSecond.map(item => `${item.charAt(0)}${item}`), // 「マップ」の'ppu'に相当
              ...romanOfSecond.map(roman => `xtu${roman}`),
              ...romanOfSecond.map(roman => `xtsu${roman}`) ],
            count + 1];
    }

    if (romanTable[first] == null)
        throw new Error('unknown character was given');
    return [romanTable[first].concat(), 1]; // 普通のとき
};
const getRoman = (furigana, targetPos) => {
    // ローマ字の取得
    // furiganaのtargetPosの位置を取得
    // 結果は配列の形式で返す
    // [[ローマ字], 変換対象となる文字数]
    const nowChar = furigana.charAt(targetPos);

    if (furigana === '') return [[''], 0];
    if (isAlphabet(nowChar)) return [[nowChar], 1];
    if (targetPos < 0 || targetPos >= furigana.length)
        throw new Error('range out of the string selected')

    const [roman, targetHiraganaLength] = hiraganaToRoman(furigana.slice(targetPos));

    // 「あんこ」をankoでもannkoでも打てるようにする処理
    // 「こが'ko'でも'nko'でも良い」と解釈している
    if (furigana.charAt(targetPos - 1) === 'ん' && !'ny'.includes(consonant[nowChar]))
        return [roman.concat(roman.map(roman => `n${roman}`)), targetHiraganaLength];

    return [roman, targetHiraganaLength];
};

全体書き直し前

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',
    // 長いので省略
};

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 = [];
    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 = [['x' + [...'aiueo', 'ya', 'yu', 'yo'][[...'ぁぃぅぇぉゃゅょ'].indexOf(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]];
};