Web便利ツール集

半角カナを全角に変換するJavaScript|濁点が2文字になる落とし穴

半角カナ全角JavaScriptUnicode正規化濁点NFKC

半角カナを全角に直そうとして、カ を カ に置換する表を作ったところ、ガ が ガ になってしまった。あるいは normalize('NFKC') で一発変換したら、関係のない ① や ㈱ まで変わってしまった。半角カナの変換では、こうした事故がよく起きます。

結論から言うと、半角カナ→全角カナは「半角カナの連続部分だけに normalize('NFKC') を当てる」のが最も短く、濁点も1文字にまとまります。 逆方向(全角→半角)は NFKC では戻せないため、対応表が必要です。この記事では、手元の Node.js v20.20.1 で実測した結果をもとに、仕組みと落とし穴、そのまま使えるコードを整理します。ブラウザごとの差は未検証です。

結論:用途別のおすすめ

やりたいこと方法注意点
半角カナ → 全角カナ(カナだけ)カナの範囲だけに normalize('NFKC')後述の「ア゙」は2文字のまま残る
文字列全体を一括で正規化したいnormalize('NFKC')全角英数が半角に、① ㈱ も変わる
全角カナ → 半角カナ対応表で置換(NFKCでは戻せない)ヷ ヺ の扱いを決めておく
手作業・単発の変換全角 ⇔ 半角 変換ツールカタカナ・英字・数字・記号を個別に選べる

なぜ「ガ」は2文字になるのか

半角カナの濁点「゙」と半濁点「゚」は、独立した文字として存在します。ガ は「カ」と「゙」の2文字の並びです。実測したコードポイントは次のとおりです。

文字列コードポイント文字数
ガ(半角)U+FF76 U+FF9E2
ガ(全角)U+30AC1
パ(半角)U+FF8A U+FF9F2
パ(全角)U+30D11

「ガギグゲゴ」は10文字、normalize('NFKC') をかけると「ガギグゲゴ」の5文字になりました。このため、半角カナを全角にすると文字数が減り、逆に全角から半角にすると最大で2倍近くに増えます。 文字数で入力制限をかけているフォームや、文字数カウントの結果が変換の前後で合わない場合は、まずこの点を疑ってください。全角・半角が混在したデータ全体の整え方は、全角・半角が混在したデータを一括で揃える方法で運用面から整理しています。

一番短い方法:normalize('NFKC')

JavaScript の String.prototype.normalize('NFKC') は、互換文字を標準的な文字に置き換え、さらに合成できるものは1文字にまとめます。半角カナに対しては、次のとおり動きました。

"ガ".normalize("NFKC");         // "ガ"(1文字)
"パソコン".normalize("NFKC");      // "パソコン"(5文字→4文字)
"ヴ".normalize("NFKC");         // "ヴ"
"ニホンゴ データ".normalize("NFKC"); // "ニホンゴ データ"(10文字→8文字)
"。「」、・ー".normalize("NFKC");    // "。「」、・ー"

濁点付きの ヴ が ヴ に、句読点や長音の半角(。 「 」 、 ・ ー)も全角になることを確認しています。対応表を手で作る必要がなく、「ヴ」「ヷ」「ヺ」といった取りこぼしやすい組み合わせも含めて変換されるのが利点です。

ただし「全体」にかけると関係ない文字まで変わる

NFKC は半角カナ専用の変換ではありません。互換文字すべてが対象になるため、文字列全体にかけると次のような変化が起きます。

入力NFKC後文字数の変化
ABC123(全角英数)ABC123(半角)6 → 6
(ア)(ア)3 → 3
①②122 → 2
㈱(株)1 → 3
㍿株式会社1 → 4
㌔キロ1 → 2
ⅢIII1 → 3
~(全角チルダ)~1 → 1

「カタカナだけ全角に揃えたい。英数字は触りたくない」という場面では、全体に NFKC をかけると、全角で揃えていた英数字が半角に変わります。 氏名・住所の登録データや、固定長ファイルの項目では、このような変更が後から問題になりかねません。

「半角カナを直したい」のか「文字列を正規化したい」のかで、使う範囲を分ける。

半角カナの範囲だけを変換するコード

半角カナは U+FF61〜U+FF9F のブロックにまとまっています。この範囲の連続部分だけに NFKC をかければ、英数字や ① ㈱ には手が入りません。

const halfKanaToFull = (s) =>
  s.replace(/[。-゚]+/g, (run) => run.normalize("NFKC"));

halfKanaToFull("ニホンゴ データ パソコン ヴァイオリン ABC 123 ①㈱");
// "ニホンゴ データ パソコン ヴァイオリン ABC 123 ①㈱"

実測では、ABC、全角の 123、①、㈱ はそのまま残り、半角カナだけが全角になりました。範囲の U+FF61 は半角の句点「。」、U+FF9F は半濁点「゚」で、半角カナのブロックの両端にあたります。

落とし穴1:「ア゙」のように濁点が付かない文字は2文字のまま

半角カナの ゙ は、どの文字の後ろにも打てます。たとえば ア゙ や、単独の ゙ です。全角の「ア゛」を意図した入力などで混ざることがありますが、Unicode には「ア」に濁点が付いた1文字が無いため、NFKC をかけても1文字にはまとまりません。

入力NFKC後コードポイント
ア゙ア゙(見た目は濁点付きのア)U+30A2 U+3099(2文字)
゙(単独)゙U+3099(結合用の濁点)

見た目は「ア゙」でも、実際は「ア」と結合用の濁点の2文字です。文字数の上限チェックや、全角カナだけの正規表現(/^[ァ-ヺー]+$/ など)では、この結合用の濁点が弾かれます。 変換後に /[゙゚]/ が残っていないか調べると、取りこぼしを検出できます。正規表現でカナを判定する際の考え方は、正規表現でひらがな・カタカナ・漢字を判定も参考にしてください。

const bad = halfKanaToFull("ア゙");
/[゙゚]/.test(bad); // true(結合用の濁点が残っている)

落とし穴2:単純な置換表は順番を間違えると壊れる

自作の対応表で split().join() を繰り返す方式では、濁点付きの組み合わせを先に置換しないと、濁点が取り残されます。 実測では次のようになりました。

// 「カ」→「カ」を先にやってしまうと…
"ガ".split("カ").join("カ"); // "ガ"(U+30AB U+FF9E)
// その後で「ガ」→「ガ」の置換を試みても、もう「ガ」は存在しない

結果は、全角の「カ」に半角の濁点「゙」がぶら下がった状態で、見た目にも不自然です。表は 「濁点・半濁点付き → 濁点なし」の順 に並べる必要があります。当サイトの全角 ⇔ 半角 変換ツールは、ソースを確認したところ、濁点付き(ガ→ガ など)を表の先頭に置く並びになっています。そのため ガギ パソコン は ガギ パソコン に変換されます。なお同じ処理を表のロジックで再現して試した結果、ア゙ は ア と半角の ゙ に分かれて残り、単独の ゙ も変換されませんでした。濁点が付かない組み合わせは、元データ側で確認してください。

逆方向(全角→半角)は表が必要

NFKC は「半角カナ→全角カナ」の方向にだけ働きます。全角カナに NFKC をかけても ガ は ガ のままで、半角にはなりません。全角から半角に戻すには、対応表が必要です。

手書きすると取りこぼしが出やすいため、NFKC の結果から対応表を自動生成する方法が確実です。

const toHalf = new Map();
const halfChars = [];
for (let c = 0xff61; c <= 0xff65; c++) halfChars.push(String.fromCodePoint(c)); // 。「」、・
for (let c = 0xff66; c <= 0xff9d; c++) halfChars.push(String.fromCodePoint(c)); // ヲ〜ン

for (const h of halfChars) {
  toHalf.set(h.normalize("NFKC"), h);
  for (const mark of ["゙", "゚"]) {
    const full = (h + mark).normalize("NFKC");
    if ([...full].length === 1) toHalf.set(full, h + mark); // 1文字にまとまる組み合わせだけ登録
  }
}

const fullKanaToHalf = (s) => [...s].map((ch) => toHalf.get(ch) ?? ch).join("");

fullKanaToHalf("ニホンゴ データ パソコン ヴァイオリン ABC 123 。「」、・ー");
// "ニホンゴ データ パソコン ヴァイオリン ABC 123 。「」、・ー"

この方法で生成された対応表は89件でした。内訳は、句読点などの記号5件、濁点なしの清音・小書き文字など56件、濁点付き23件(カ行・サ行・タ行・ハ行の20件に ヴ ヷ ヺ を加えたもの)、半濁点付き5件です。往復の確認では、ニホンゴ データ 。「」、・ を全角にしてから半角に戻すと、元の文字列と一致しました。

「ヷ」「ヺ」はどうするか

この表には、ヷ(ヷ)と ヺ(ヺ)も入ります。実務のデータで使われることはほとんどなく、対象外にしたい場合は、対応表から削除してください。前述のツールの対応表には ヷ ヺ が入っておらず、全角の ヷ を半角にしようとしても変換されません。何を変換対象にするかは、連携先のシステムの仕様に合わせて決めるのが実務的です。

落とし穴3:文字数の数え方で結果が変わる

「ガ」は何文字か、という問いには、数え方によって答えが変わります。Node.js で実測した結果は次のとおりです。

数え方ガギグガギグ
.length(UTF-16の単位数)63
[...s].length(コードポイント数)63
Intl.Segmenter(書記素の数)33

Intl.Segmenter で書記素単位に数えると、ガ は1つのまとまりとして数えられました。.length で数えるのか、見た目の文字数で数えるのかによって、半角カナの文字数は倍近く違います。 入力フォームの文字数制限が「見た目の文字数」を意図しているのに .length で判定していると、半角カナで入力した人だけ早く上限に達します。ただしこの結果は Node.js v20 での実測で、ブラウザでの挙動は未検証です。

バイト数は文字コードで変わる

固定長ファイルなど、バイト数で桁が決まっている場合も注意が必要です。

  • UTF-8:半角カナは1文字3バイトです。ガ は6バイト、ガ は3バイトで、半角のほうが大きくなります(コードポイントがU+0800以降のため、UTF-8では3バイトになります)。
  • Shift_JIS:半角カナは1バイトです。TextDecoder('shift_jis') で確認したところ、バイト列 B6 DE が ガ に、83 4B が ガ に復号されました。どちらも2バイトで、半角にしてもバイト数は変わりません。

つまり、「半角にすれば短くなる」という思い込みは、文字コード次第で外れます。連携先の仕様が文字数で決まっているのかバイト数で決まっているのか、先に確認してください。

落とし穴4:分解された濁点(Macのファイル名など)

Mac で作られたファイル名では、ガ が「カ」と結合用の濁点(U+3099)に分解されていることがあります。この文字列を、前述の対応表式の変換にそのまま通すと、半角の「カ」の後ろに結合用の濁点が残る、という中途半端な結果になります。

// 分解された「ガ」(U+30AB U+3099) を単純な表で半角にした例
// → "ガ"(U+FF76 U+3099)。カの後ろに結合用の濁点が付いた状態

実測では、分解された ガ に対して ガ→ガ の置換表をかけても一致せず、カ と結合用の濁点だけが組み合わさりました。変換の前に normalize("NFC") で合成しておくと、ガ が1文字に戻り、対応表で正しく ガ に変換できます。Mac のファイル名で濁点が分離する問題の原因と対処は、Macのファイル名の濁点が分離する問題にまとめています。

半角カナ→全角カナの変換でも、半角カナの変換結果に結合用の濁点が混ざった場合は、最後に normalize("NFC") を呼ぶと合成されます。実測では、カ の後ろに結合用の濁点が付いた文字列が、変換後に1文字の ガ へまとまりました。

const fix = (s) =>
  s.replace(/[。-゚]+/g, (run) => run.normalize("NFKC")).normalize("NFC");

ただし NFC は半角カナと無関係の文字にも作用するため、対象を広げる場合は影響を確認してください。

実務で使うときのチェックリスト

  1. 半角カナだけを直したいのか、全体を正規化したいのかを決める(全体なら NFKC、カナだけなら範囲指定)
  2. 変換後に /[゙゚]/ が残っていないか確認する(ア゙ などの取りこぼし検出)
  3. 文字数制限が「見た目」か「.length」か「バイト」かを確認する
  4. 全角→半角が必要なら、NFKCから対応表を生成する。ヷ ヺ の扱いも決める
  5. パスワード・APIキー・商品コードには変換をかけない(1文字違うだけで別物になるため)

メール本文や送信データに半角カナが混ざったときの文字化けは、メールの文字化けの直し方で受信側・送信側に分けて整理しています。環境依存文字(① ㈱ など)の置き換えは、機種依存文字の直し方も合わせて確認してください。

検証した範囲について

この記事の数値と出力は、Node.js v20.20.1 で実行した結果です。次の点は未検証です。

  • Chrome・Safari・Firefox など、ブラウザでの normalize と Intl.Segmenter の挙動
  • Node.js の別バージョンや、Unicode のバージョンが異なる環境での結果
  • Shift_JIS のバイト列は、TextDecoder での復号結果から確認したもので、エンコード側(JavaScript から Shift_JIS へ書き出す処理)は試していません

また、全角・半角のどちらで保持するべきかは、連携先のシステムの仕様で決まります。氏名のフリガナ、銀行の振込データなど、仕様が定められている項目は、取引先や提供元の最新の資料で確認してください。

まとめ

  1. 半角カナの濁点・半濁点は独立した1文字のため、ガ は2文字になり、全角にすると1文字になる
  2. 半角カナ→全角カナは、/[。-゚]+/ の範囲だけに normalize('NFKC') をかけるのが短くて安全
  3. 文字列全体に NFKC をかけると、全角英数が半角に、① ㈱ ㍿ なども別の文字列に変わる
  4. ア゙ のように濁点が付かない組み合わせは、変換後も結合用の濁点が残るので、/[゙゚]/ で検出する
  5. 全角→半角は NFKC では戻せない。NFKC の結果から対応表を自動生成すると取りこぼしが少ない
  6. 文字数は数え方(.length・書記素・バイト)で倍近く変わるので、制限の定義を先に確認する

単発の変換なら、ブラウザ上で完結する全角 ⇔ 半角 変換ツールが手軽です。変換の前後で文字数が合っているかは、文字数カウントツールで確認できます。

この記事の内容は一般的な技術情報の整理であり、特定のシステムや取引先の仕様について結論を示すものではありません。

この記事で使うツール

関連記事