リーダブルコードを読む前に書いたコードを見たら、ゴミすぎて全部書き直しました。
今回はこちらのコードを書き直しました。
ひらがなからローマ字への変換可能パターンを列挙するプログラムをつくった - Panda Noir
書き直し1: 一度に1つのことを
getRoman関数は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]]; };