全角・半角変換とフォーム入力のクリーニングをJavaScriptでまとめて処理する

症状:入力のゆれで検索・照合・登録がズレる

業務フォームでは、同じ内容でも人によって書き方がばらつきます。会員番号を ABC123 と全角で打つ人、半角で打つ人。電話番号を 03-1234-5678 と書く人、03(1234)5678 と書く人。カナ氏名を全角で入れる人、半角カナで入れる人。さらにExcelやメールから貼り付けると、目に見えないゼロ幅スペースや前後の空白、改行が紛れ込みます。

これらをそのまま保存すると、「見た目は同じなのに一致しない」照合ズレや、重複データ、桁数チェックのすり抜けが起きます。この記事では、全角・半角の正規化からカナ変換、電話番号のハイフン統一、貼り付けテキストのクリーニングまで、業務フォームでよく必要になる文字処理を1本にまとめます。まず入力を実際に変換して確かめられるデモを置き、最後にコピペで使える関数集を載せます。

基本:normalize('NFKC')で全角をまとめて半角にする

全角の英数字や記号、半角カナの正規化は、JavaScript標準の String.prototype.normalize('NFKC') でまとめて処理できます。NFKCは「互換等価な文字を、代表的な形に揃える」正規化形式で、全角英数字を半角にし、半角カナを全角カナに直し、濁点付きの半角カナも1文字に合成します。

// 全角の英数字・記号・半角カナをまとめて正規化する
const normalized = input.value.normalize('NFKC');

'ABC123'.normalize('NFKC');  // → 'ABC123'
'パンダ'.normalize('NFKC');       // → 'パンダ'(半角カナ+濁点が合成される)
' '.normalize('NFKC');           // → ' '(全角スペースが半角スペースに)

会員番号や数量、英数字コードのような欄なら、これだけで入力ゆれの大半が吸収できます。ただし、この NFKC をすべての入力欄に一律でかけると事故ります。次でその線引きを決めます。

フィールドごとに適用範囲を分ける(すべてにNFKCをかけない)

NFKC は便利な半面、意図しない変換も一緒に起こします。とくに記号化された文字は、元の意味が失われる形に化けます。

'㈱'.normalize('NFKC');   // → '(株)'
'①'.normalize('NFKC');   // → '1'(丸数字がただの数字になる)
'㍿'.normalize('NFKC');   // → '株式会社'(1文字が4文字に増える)
'℡'.normalize('NFKC');   // → 'TEL'
'㌔'.normalize('NFKC');   // → 'キロ'

会社名の欄に と入れた値を一律で正規化すると、ユーザーが打った原文とは違う (株) が保存されます。丸数字を使った表記や、単位記号を含む自由記述も同様です。だからこそ、どの欄に何を適用するかを分けて考えます。

NFKCをかけてよい欄

会員番号や商品コードなどの英数字、数量、電話番号や郵便番号のような「表記を揃えたい」データ。ここは全角半角のゆれをなくすメリットが、副作用より大きい欄です。

安易にかけないほうがよい欄

住所、会社名、氏名、自由記述のコメント。ユーザーが打った文字をそのまま残したい欄です。 や丸数字が勝手に置き換わると、原文と食い違います。ここは前後の空白除去など最小限のクリーニングにとどめます。

英数字だけを半角に揃えたくて、記号の副作用は避けたい。そういうときは、NFKC ではなく、変換対象を全角英数字に限定した処理を使います。

// 副作用を避けたいとき: 全角の英数字だけを半角にする
function toHalfWidthAlnum(str) {
  return str.replace(/[A-Za-z0-9]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) - 0xFEE0);
  });
}

toHalfWidthAlnum('㈱ABC123'); // → '㈱ABC123'(㈱はそのまま、英数字だけ半角に)

全角英数字はUnicode上で半角と 0xFEE0 ずれた位置に並んでいるので、その分を引くだけで半角に戻せます。 や丸数字は対象範囲の外なので影響を受けません。「全部を正規化する」か「必要な文字だけ変換する」かを、欄の性質で選ぶのがポイントです。

デモ:入力した文字がどう変換されるか確かめる

下の入力欄に文字を打つか貼り付けると、NFKCだけをかけた結果と、そこに不可視文字の除去や空白整理まで加えたクリーニング後の結果を並べて表示します。ゼロ幅スペースや前後の空白などの見えないゴミは、入力欄では こう 目印付きで可視化します。上のサンプルボタンで、副作用が出るケースも試せます。

サンプルを入れる:
入力(不可視文字を可視化)
NFKC適用後
クリーニング後(NFKC+不可視除去+trim)

「㈱・丸数字」サンプルを入れると、NFKC適用後の欄で (株) に、1 に変わるのが見えます。会社名や自由記述の欄では、この変換をそのまま保存したくないはずです。一方「貼り付けゴミ入り」サンプルでは、NFKCだけでは残るゼロ幅スペースや前後の空白が、クリーニング後の欄で消えて文字数が減るのが確認できます。

カタカナとひらがなを相互に変換する

フリガナ欄をカタカナに統一したい、あるいは検索用にひらがなへ寄せたい、といった場面で使います。ひらがなとカタカナはUnicode上で 0x60 ずれて並んでいるので、その差分でシフトします。

// カタカナ → ひらがな
function kataToHira(str) {
  return str.replace(/[ァ-ヶ]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) - 0x60);
  });
}

// ひらがな → カタカナ
function hiraToKata(str) {
  return str.replace(/[ぁ-ゖ]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) + 0x60);
  });
}

kataToHira('タナカ'); // → 'たなか'
hiraToKata('たなか'); // → 'タナカ'

半角カナが混ざっている可能性があるときは、先に normalize('NFKC') で全角カナへ揃えてから、このシフトを通すと安定します。長音符 や中点はこの範囲の外なので、そのまま残ります。

電話番号・郵便番号のハイフンを統一する

電話番号や郵便番号は、区切り文字がばらつきやすい代表格です。全角ハイフン、半角ハイフン、長音符、括弧、全角スペースなど、人によってまちまちに入ります。数値項目なので、思い切って数字だけを取り出すのが一番確実です。

// 全角→半角にしてから、数字だけを取り出す
function toDigits(str) {
  return str.normalize('NFKC').replace(/[^\d]/g, '');
}

toDigits('03-1234-5678');   // → '0312345678'
toDigits('03(1234)5678');   // → '0312345678'
toDigits('123-4567');                 // → '1234567'(郵便番号)

ハイフンを残した形で整えたい場合は、正規化してから桁で区切り直します。区切り文字をそのまま保持しようとすると種類の判定が増えるので、いったん数字に落としてから整形するほうが単純です。

// 郵便番号を 123-4567 の形に整える
function formatPostalCode(str) {
  const digits = toDigits(str);
  if (digits.length !== 7) return str; // 桁が合わなければ触らない
  return digits.slice(0, 3) + '-' + digits.slice(3);
}

formatPostalCode('1234567'); // → '123-4567'

この処理は、郵便番号から住所を補完する入力欄とも相性がよいです。住所自動入力の実装は末尾の関連事例を参照してください。

貼り付けテキストの見えないゴミを取り除く

他のアプリからコピーした文字列には、画面に見えない文字がよく紛れ込みます。ゼロ幅スペース、BOM、制御文字、前後の空白、途中の改行やタブです。これらは NFKC では消えないので、別途クリーニングします。

function cleanPasted(str) {
  return str
    .normalize('NFKC')                                              // 全角→半角などの正規化
    .replace(/[\u200B-\u200D\uFEFF]/g, '')                          // ゼロ幅スペース・BOM
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') // 制御文字(改行・タブ以外)
    .replace(/[\t\r\n]+/g, ' ')                                     // 改行・タブを空白に
    .replace(/ {2,}/g, ' ')                                         // 連続する空白を1つに
    .trim();                                                        // 前後の空白を除去
}

// 例: 前後の全角空白と、語中に紛れたゼロ幅スペースが取り除かれる
cleanPasted('  みずほ銀行 '); // → 'みずほ銀行'

複数行を許容する textarea では、改行を空白に潰したくないこともあります。その場合は [\t\r\n]+ の行を外し、改行はそのまま残して前後の空白だけ整える、といった形に調整してください。どこまで削るかは欄の用途で決めます。

いつ変換するか:入力中ではなくフォーカスを離れたときに整える

変換のタイミングは、実装の使い勝手を大きく左右します。結論から言うと、入力欄からフォーカスが離れた blur のときに整形し、送信直前の submit でもう一度だけ最終正規化をかける二段構えを推奨します。

// 入力中は書き換えない。フォーカスを離れたときに整える
input.addEventListener('blur', function () {
  input.value = cleanPasted(input.value);
});

// 送信直前にまとめて最終正規化する(保険)
form.addEventListener('submit', function () {
  input.value = cleanPasted(input.value);
});

input イベントごとに書き換えるのは避けます。キーを打つたびに値を差し替えると、カーソル位置が末尾へ飛んだり、日本語入力(IME)の変換途中の文字列を勝手に確定・変換してしまい、入力が壊れます。ユーザーがまだ変換中の文字に手を入れると、体験が一気に悪くなります。

IME変換中のイベントの扱いは、Enter送信やインクリメンタル検索でも共通の落とし穴です。日本語入力の確定Enterでフォームが送信されてしまう問題は、日本語入力の確定Enterでフォームが送信される原因と直し方で詳しく扱っています。blursubmit のタイミングに寄せておけば、変換中のイベントと衝突しないので安全側です。

NFKCで変換される文字・されない文字(エッジケース一覧)

正規化を実装するうえで一番の勘所は、「何が変換され、何が変換されないか」を把握しておくことです。手元で実際に normalize('NFKC') を動かして確認した結果を、挙動ごとに3グループに分けて整理します。

入力 NFKC適用後 分類 実務での注意
ABC123 ABC123 望ましい コード・数量欄はこれで表記を揃えられる
パンダ(半角カナ+濁点) パンダ 望ましい 半角カナと濁点が1文字に合成される
全角スペース   半角スペース 望ましい 余分な全角空白が半角に揃う
(株) 副作用 会社名欄では原文を保持したいことが多い
1 副作用 丸数字が普通の数字になり意味が変わる
株式会社 副作用 1文字が4文字に増え、桁数が変わる
/ TEL / キロ 副作用 記号・単位が英字やカナに展開される
ゼロ幅スペース U+200B (そのまま残る) 要クリーニング NFKCでは消えない。正規表現で別途除去する
波ダッシュ U+FF5EU+301C (別文字のまま) 要クリーニング 見た目が似て別コード。照合ズレの原因になる
前後の空白・途中の改行 (そのまま残る) 要クリーニング trimと改行除去を別に用意する
絵文字・結合文字 (そのまま残る) 要クリーニング 正規化の対象外。必要なら個別に弾く

「望ましい」グループは積極的に正規化してよい変換、「副作用」グループは欄によっては困る変換、「要クリーニング」グループはNFKCでは手が届かず別処理が要る文字です。この3つを頭に入れておくと、どの欄にどこまでかけるかを迷わず決められます。

コピペ用:入力クリーニング関数のまとめ

ここまでの処理をひとまとめにしたものです。欄の種類に合わせて必要な関数を選んで使ってください。

// 1) 全角英数字だけを半角に(記号の副作用を避けたいとき)
function toHalfWidthAlnum(str) {
  return str.replace(/[A-Za-z0-9]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) - 0xFEE0);
  });
}

// 2) カナ⇔ひらがな
function kataToHira(str) {
  return str.replace(/[ァ-ヶ]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) - 0x60);
  });
}
function hiraToKata(str) {
  return str.replace(/[ぁ-ゖ]/g, function (c) {
    return String.fromCharCode(c.charCodeAt(0) + 0x60);
  });
}

// 3) 数字だけを取り出す(電話番号・郵便番号)
function toDigits(str) {
  return str.normalize('NFKC').replace(/[^\d]/g, '');
}

// 4) 貼り付けテキストのクリーニング(正規化+不可視文字除去+trim)
function cleanPasted(str) {
  return str
    .normalize('NFKC')
    .replace(/[\u200B-\u200D\uFEFF]/g, '')
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
    .replace(/[\t\r\n]+/g, ' ')
    .replace(/ {2,}/g, ' ')
    .trim();
}

強めに正規化してよい欄は cleanPasted、原文を残したい欄は前後の空白除去だけ、英数字コードは toHalfWidthAlnum、番号系は toDigits、というように割り当てます。適用のタイミングは blursubmit に寄せます。

まとめ

業務フォームの文字入力は、全角半角のゆれや貼り付けの見えないゴミが照合ズレや重複の原因になります。normalize('NFKC') を軸にすれば大半は揃えられますが、そのまま全欄に一律でかけると や丸数字まで書き換わってしまいます。

正規化は「全部に一律」ではなく、欄ごとに何をどこまで変換するかを決めてかける。英数字コードは強め、住所や氏名は最小限に。見えないゴミはNFKCの外なので別途クリーニングする。

変換のタイミングは入力中を避け、フォーカスが離れたときと送信直前に寄せると、IMEの変換とも衝突しません。上のデモで、自分が扱う欄の入力がどう変わるかを実際に確かめてから、関数を選んで組み込んでみてください。

関連するUI事例

入力の書式チェックや正規化と組み合わせて使える、コピペで動くフォームの事例です。