「カタカナのフリガナをひらがなに統一したい」「半角カナで届いたデータを読みやすく直したい」——日本語データを扱うとかな変換は地味に頻出する。実装してみると、ひらがなとカタカナの相互変換は拍子抜けするほど簡単で、面倒なのは半角カナが絡んだときだけ、という構造が見えてくる。
ひらがな・カタカナ・半角カナを相互変換するツールをぱんだツールズの1機能として作った。ライブラリなし・ブラウザ完結。
この記事では、ひらがな⇔カタカナが Unicode 上でなぜ 0x60 の足し引きだけで変換できるのか、そして「半角カナ→ひらがな」のような多段変換を関数合成でどう素直に組むかを解説する。
ひらがな ⇔ カタカナ:0x60 を足し引きするだけ
Unicode では、ひらがな(U+3041〜U+3096)とカタカナ(U+30A1〜U+30F6)がまったく同じ並び順で配置されている。ぁ と ァ、あ と ア、ん と ン がそれぞれ対応する位置にいて、その差はちょうど 0x60(10進で96)。
ひらがな 'あ' = U+3042 カタカナ 'ア' = U+30A2 差 = 0x60
ひらがな 'ん' = U+3093 カタカナ 'ン' = U+30F3 差 = 0x60
だから、対象の文字コード範囲に正規表現でヒットさせて、コードポイントを 0x60 足し引きするだけで変換できる。マッピングテーブルは要らない。
// ひらがな → カタカナ(U+3041-U+3096)
function hiraganaToKatakana(text: string): string {
return text.replace(/[ぁ-ゖ]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + 0x60))
}
// カタカナ → ひらがな(U+30A1-U+30F6)
function katakanaToHiragana(text: string): string {
return text.replace(/[ァ-ヶ]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) - 0x60))
}
replace のコールバックで1文字ずつコードをずらすだけ。範囲外の文字(漢字・英数字・記号)は正規表現にマッチしないのでそのまま残る。「文章の一部のカナだけ変換」が自然にできる。
濁音・半濁音も気にしなくていい。が(U+304C)と ガ(U+30AC)も同じ 0x60 差で並んでいるので、合成済みの濁音文字がそのまま1対1で変換される。範囲を U+3096(ゖ)までにしているのは、それ以降の ゛゜ゝゞ(繰り返し記号など)はカタカナ側に対応がなく、ずらすと変な文字になるため。
ここまでが「拍子抜けするほど簡単」な部分。
半角カナだけは別世界(マッピングテーブルが要る)
簡単なのはここまで。半角カタカナ(ア = U+FF71 など)は、全角カナとは並び順もオフセットも揃っておらず、0x60 のような一律の足し引きが効かない。素直にマッピングテーブルを持つ。
const HANKAKU_TO_ZENKAKU: Record<string, string> = {
'ア': 'ア', 'イ': 'イ', 'ウ': 'ウ', /* ... */
'ワ': 'ワ', 'ン': 'ン', '゙': '゛', '゚': '゜',
}
さらに半角カナは濁音を持たない。ガ は半角だと カ(カ)+ ゙(濁点)の2文字で表現される。なので半角→全角では、次の文字が濁点・半濁点かを先読みして、濁音テーブルで1文字に合成する。
const DAKUTEN_MAP: Record<string, string> = {
'カ': 'ガ', 'キ': 'ギ', /* ... */ 'ウ': 'ヴ', // ヴ → ヴ も拾う
}
const HANDAKUTEN_MAP: Record<string, string> = {
'ハ': 'パ', 'ヒ': 'ピ', 'フ': 'プ', 'ヘ': 'ペ', 'ホ': 'ポ',
}
function hankakuToZenkakuKana(text: string): string {
let result = ''
for (let i = 0; i < text.length; i++) {
const c = text[i]
const next = text[i + 1]
const zenkaku = HANKAKU_TO_ZENKAKU[c]
if (!zenkaku) { result += c; continue }
if (next === '゙') {
result += DAKUTEN_MAP[zenkaku] ?? zenkaku + '゛' // 濁音化、無ければ濁点を添える
i++
} else if (next === '゚') {
result += HANDAKUTEN_MAP[zenkaku] ?? zenkaku + '゜'
i++
} else {
result += zenkaku
}
}
return result
}
DAKUTEN_MAP[zenkaku] ?? zenkaku + '゛' のフォールバックがポイント。ア゙(濁点の付かないアに濁点)のような無効な組み合わせが来ても、合成済み文字がなければ「ア+濁点記号」として落とす。マッチしたら i++ で濁点ぶんを読み飛ばす。
4モードを「関数合成」で組む
このツールには4つの変換モードがある。ひらがな→カタカナ、カタカナ→ひらがな、半角カナ→カタカナ、そして半角カナ→ひらがな。
最後の「半角カナ→ひらがな」が面白い。これ専用の変換テーブルを新たに作る必要はない。すでにある2つの関数をつなげるだけで済む。半角カナ→ひらがな は「半角カナ→全角カタカナ」してから「カタカナ→ひらがな」すれば実現できる。
function convert(text: string, mode: ConvertMode): string {
switch (mode) {
case 'hiragana-to-katakana': return hiraganaToKatakana(text)
case 'katakana-to-hiragana': return katakanaToHiragana(text)
case 'hankaku-to-katakana': return hankakuToZenkakuKana(text)
// 半角カナ → 全角カタカナ → ひらがな の2段(関数合成)
case 'hankaku-to-hiragana': return katakanaToHiragana(hankakuToZenkakuKana(text))
}
}
katakanaToHiragana(hankakuToZenkakuKana(text)) の入れ子1行。半角カナを一度「全角カタカナ」という中間表現に正規化してしまえば、あとは既存の 0x60 変換に乗れる。新しいロジックはゼロ。
これは変換系を組むときの定石で、全ての変換を直接実装するのではなく、中間表現(ここでは全角カタカナ)にハブを作り、そこ経由で繋ぐと組み合わせが一気に減る。4モードあっても、実体の変換関数は3つ(ひら→カタ、カタ→ひら、半角→全角カタ)で足りている。半角→ひらがな は合成で導出した4つめ、という構図。
まとめ
かな変換は「ひらがな⇔カタカナは一瞬、半角カナだけ手間」という非対称な題材だった。
- ひらがな⇔カタカナは Unicode 上で同じ並び。
0x60を足し引きするだけ。テーブル不要・濁音も自動 - 変換範囲は
U+3041-3096/U+30A1-30F6。末尾の繰り返し記号などを巻き込まないよう範囲を区切る - 半角カナはオフセットが効かないのでマッピングテーブル。濁音は
カ+゙の2文字を先読みして合成、無効な組み合わせはフォールバック - 多段変換(半角カナ→ひらがな)は専用実装せず、全角カタカナを中間表現にした関数合成で導出する
0x60 の足し引きで済むひらがな・カタカナの素直さと、半角カナの泥臭さの落差が、日本語テキスト処理の縮図のようで面白い題材だった。
ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理など、開発者向けのツールを多数公開している。全部無料・登録不要・ブラウザ完結で使える。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。