郵便番号の正規表現(JavaScript)|全角・〒・ハイフンの揺れと先頭0落ちの対処
住所入力フォームの郵便番号欄は、/^\d{3}-\d{4}$/ と書けば終わりのように見えて、実際には「ハイフンなしで入力された」「全角で貼り付けられた」「〒が付いていた」「保存したら先頭の0が消えた」といった事故が起きやすい箇所です。
結論から言うと、郵便番号も「数字7桁に正規化する → 形式を判定する」の2段階に分けるのが最も壊れにくい方法です。この記事では Node.js v20.20.1 で実際に動かした結果をもとに、そのコードと見落としやすい落とし穴を整理します。ブラウザ各種や他バージョンの Node.js は検証していません。
電話番号版の考え方は 電話番号の正規表現(JavaScript) にまとめています。郵便番号は桁数が固定なので単純ですが、単純だからこそ見落とす穴があります。
結論:正規化してから判定する
使うコードは次のとおりです。
// 入力を「数字7桁」に正規化する。形式が合わなければ null
function toPostal(input) {
let s = String(input).normalize("NFKC").trim();
s = s.replace(/^〒\s*/, ""); // 先頭の〒を外す
s = s.replace(/[‐-―−ー]/g, "-"); // NFKCで直らないハイフン類を統一
return /^\d{3}-?\d{4}$/.test(s) ? s.replace("-", "") : null;
}
// 表示用に 123-4567 の形へ整える
const format = (digits) => digits.replace(/^(\d{3})(\d{4})$/, "$1-$2");
実際の入力に対する結果は次のとおりでした(Node.js v20.20.1 で実行)。
| 入力 | 結果 |
|---|---|
123-4567 | "1234567" |
1234567 | "1234567" |
1234567(全角) | "1234567" |
123-4567(全角ハイフン) | "1234567" |
〒123-4567 | "1234567" |
〒 123‐4567(U+2010のハイフン) | "1234567" |
123−4567(U+2212のマイナス) | "1234567" |
123ー4567(長音記号) | "1234567" |
123 4567(空白区切り) | null |
123-456(桁不足) | null |
12-34567(区切り位置が違う) | null |
1234567890(10桁) | null |
| 空文字 | null |
内部ではハイフンなしの7桁文字列で持ち、表示するときだけ format で整形する形にしておくと、保存・比較・重複判定がすべて楽になります。
\d は全角数字にマッチしない
「\d は数字なので全角も通るだろう」と考えがちですが、JavaScript の \d は半角の 0-9 だけです。実測では次のとおりでした。
/^\d{3}-\d{4}$/.test("123-4567"); // false
/^\d{7}$/u.test("1234567"); // false(uフラグを付けても)
/^[0-9]{7}$/.test("1234567"); // false
スマホの日本語入力では、全角数字のまま確定してしまうことは珍しくありません。「数字のはずなのにエラーになる」というお問い合わせの典型的な原因です。判定の前に normalize("NFKC") をかければ、全角数字は半角になります(上の表の 1234567 → "1234567")。
なお、アラビア・インド数字(U+0661〜U+0667 の7文字)は \d にも通らず、NFKC をかけても変わりませんでした。日本の住所フォームで実際に入力される場面は多くないと考えられますが、「数字に見える文字がすべて通る」わけではない点は知っておくと安心です。
NFKCで直るハイフン、直らないハイフン
全角入力のハイフンにはいくつか種類があり、NFKC で半角ハイフンになるものとならないものがあります。実測結果は次のとおりです。
| 文字 | コードポイント | NFKC後 |
|---|---|---|
-(全角ハイフンマイナス) | U+FF0D | - に変わる |
﹣(小さいハイフンマイナス) | U+FE63 | - に変わる |
‐(ハイフン) | U+2010 | 変わらない |
−(マイナス記号) | U+2212 | 変わらない |
―(ホリゾンタルバー) | U+2015 | 変わらない |
—(emダッシュ) | U+2014 | 変わらない |
–(enダッシュ) | U+2013 | 変わらない |
ー(長音記号) | U+30FC | 変わらない |
ー(半角の長音記号) | U+FF70 | ー(U+30FC)になる |
ここで問題になるのは、キーボードの全角ハイフンが U+FF0D で来るとは限らないことです。Word やメモアプリからコピーしたテキストでは U+2010 や U+2212 が混ざることがあり、NFKC だけでは直りません。そこで冒頭のコードでは、NFKC のあとに [‐-―−ー] を半角ハイフンへ置き換えています。
長音記号(ー)をハイフン扱いにするのは、入力欄の全体が郵便番号である前提だからできる処理です。住所などの文章中の「ー」まで置き換えてしまうと、「スカイツリー」が壊れます。文章全体に適用しないよう注意してください。
全角と半角の変換そのものを確かめたいときは、全角⇔半角変換ツール で数字と記号の変換結果を見比べられます。
〒マークと空白をどう扱うか
郵便番号をコピーすると、〒123-4567 のように〒付きで取れることがよくあります。〒(U+3012)は NFKC をかけても変わらないため、自分で外す必要があります。冒頭のコードの s.replace(/^〒\s*/, "") がその処理です。
一方、123 4567 のように空白で区切った入力は null にしています。許可するかどうかは仕様の問題ですが、許可するなら区切り文字の置換に空白を含めるだけで済みます。ただし空白を許すと、1 2 3 4 5 6 7 のような入力まで通すかどうかも決める必要が出るため、迷ったら許可しないほうが保守しやすいと考えられます。
また、ゼロ幅スペース(U+200B)が混入した "1234567" は、trim() でも NFKC でも取り除かれず、\d{7} にも通りませんでした(toPostal は null)。コピー元が Web ページの場合に起こりうる、見た目に原因が分からないエラーです。エラー時に「半角数字7桁で入力してください」と出すだけでなく、貼り付け直しを促す一文を添えておくと問い合わせが減ります。
落とし穴1:数値にすると先頭の0が消える
郵便番号は「数字だけでできた文字列」であって、数値ではありません。数値型に変換すると、0 から始まる番号が壊れます。
Number("0600000"); // 600000(先頭の0が消える)
parseInt("060-0000"); // 60(ハイフン以降が切り捨てられる)
String(Number("0600000")); // "600000"(元の7桁に戻せない)
6桁になった 600000 から元の 0600000 を復元することはできません(桁数が足りない理由が「先頭の0」なのか「入力ミス」なのか区別できないため)。JSON で数値として保存すると、読み込み時にも同じことが起きます。API やデータベースでは文字列型(7桁の文字列)で扱うのが鉄則です。
Excel や CSV を経由する場合も同じ現象が起きます。詳しくは CSVをExcelで開くと0が消える・桁が変わる を参照してください。
HTML の入力欄については、type="number" ではなく次の指定が一般的とされています(ブラウザごとの挙動は今回は検証していません)。
<input type="text" inputmode="numeric" autocomplete="postal-code" />
type="number" は数値として扱われるため先頭の0の扱いが不安定になりやすく、ハイフンも入力できないため郵便番号には向きません。inputmode="numeric" はスマホで数字キーボードを出す指定、autocomplete="postal-code" はブラウザの自動入力に郵便番号欄だと伝える指定です。
落とし穴2:文章から抽出すると電話番号の一部を拾う
フォームの検証ではなく、住所を含む文章から郵便番号を抜き出したい場合は、アンカー(^ と $)が使えません。ここで、アンカーなしの単純な正規表現は思わぬものを拾います。
const loose = /\d{3}-?\d{4}/;
"TEL 03-1234-5678".match(loose)[0]; // "234-5678"(電話番号の一部)
"注文番号 20241234567".match(loose)[0]; // "2024123"(注文番号の一部)
そこで「前後に数字が続かない」条件を足したのが次の式です。
const strict = /(?<!\d)\d{3}-?\d{4}(?!\d)/g;
"住所: 100-0001 / 080-1234-5678".match(strict);
// ["100-0001", "080-1234"] ← 電話番号の先頭部分を拾ってしまう
数字の前後チェックだけでは、携帯電話番号 080-1234-5678 の先頭 080-1234 が郵便番号の形に一致してしまいました。ハイフンも「前後に続かない」条件に加えると解決します。
const strict = /(?<![\d-])\d{3}-?\d{4}(?![\d-])/g;
"住所: 100-0001 / 080-1234-5678".normalize("NFKC").match(strict); // ["100-0001"]
"TEL 03-1234-5678 〒150-0002".normalize("NFKC").match(strict); // ["150-0002"]
"注文 20241234567".match(strict); // null
"〒1000001 東京都".match(strict); // ["1000001"]
ただし、(03)1234-5678 のような括弧付きの電話番号などを含め、すべての文章で誤検出がゼロになる保証はありません。今回試したのは上の数パターンだけです。抽出した結果は、そのまま使わずユーザーに確認させる運用にするのが安全です。
また、全角で書かれた文章(郵便番号は123-4567です)は、正規化してからでないと抽出できませんでした(正規化前は null、NFKC 後は "123-4567")。抽出の前にも NFKC をかけることを忘れないでください。
落とし穴3:形式が合っても存在する番号とは限らない
次の正規表現は 000-0000 も通します(実測で true)。
/^\d{3}-?\d{4}$/.test("000-0000"); // true
当然ですが、7桁の数字であることと、実在する郵便番号であることは別です。正規表現でできるのは形式の検証までで、「その番号が実在するか」「住所と矛盾していないか」を確かめるには、郵便番号と住所の対応データが必要です。
日本郵便は郵便番号データを公開していると知られていますが、データの仕様や配布形態は変わることがあるため、利用する場合は日本郵便の公式サイトで最新の内容を確認してください。個人開発のフォームで実在チェックまで行うかどうかは、次のように考えると整理しやすくなります。
| 方針 | 向いている場面 |
|---|---|
| 形式(7桁)だけ検証する | 問い合わせフォーム、会員登録など。誤入力は後工程で補正できる |
| 住所の自動入力と組み合わせる | ECなど、住所の誤りが配送事故に直結する場面 |
| 実在チェックをかける | 帳票・請求書など、厳密さが求められる場面 |
形式だけの検証でも、「3桁-4桁になっているか」を満たしていれば、入力ミスの大半(桁の過不足)は弾けると考えられます。
落とし穴4:$ は末尾の改行にマッチしない
テキストエリアからのコピーなどで、末尾に改行が混ざることがあります。JavaScript の正規表現では、$ は末尾の改行にはマッチしません(m フラグなしの場合)。
/^\d{3}-?\d{4}$/.test("1234567\n"); // false
/^\d{3}-?\d{4}$/.test("123-4567\n"); // false
これ自体は「厳密に判定できている」という意味で悪くないのですが、サーバー側の言語によっては $ が末尾の改行を許すとされており、フロントとバックで判定が食い違う可能性があります(他言語の挙動は今回は検証していません)。食い違いを避けるには、判定の前に trim() をかけてから、フロントでもサーバーでも同じ手順で正規化するのが確実です。冒頭のコードは trim() を先に実行しているので、末尾の改行や空白は取り除かれます。
また、m フラグを付けると、複数行のどこか1行でも一致すれば true になります("abc\n1234567\nxyz" で true)。検証用の正規表現に m フラグは付けないでください。
フォーム実装のチェックリスト
最後に、郵便番号欄を作るときの確認項目をまとめます。
| 項目 | 推奨 |
|---|---|
| 内部の保持形式 | ハイフンなし7桁の文字列 |
| 判定前の処理 | trim() → normalize("NFKC") → 〒除去 → ハイフン類の統一 |
| 判定の正規表現 | ^\d{3}-?\d{4}$(m フラグなし) |
| 表示 | 保存値を 123-4567 に整形して出す |
| input属性 | type="text" inputmode="numeric" autocomplete="postal-code" |
| 抽出用の式 | (?<![\d-])\d{3}-?\d{4}(?![\d-]) + ユーザー確認 |
| エラー文言 | 「半角数字7桁で入力してください」に加えて貼り付け直しの案内 |
正規表現の動作は 正規表現テスター で、入力文字列を変えながら確認できます。メールアドレス欄の考え方は メールアドレスの正規表現、全角・半角が混在したデータの一括変換は 全角・半角が混在したデータを一括で揃える方法 も参考になります。
まとめ
- 郵便番号は数値ではなく文字列で扱う。
Number()を通すと先頭の0が消え、元に戻せない \dは半角のみ。判定の前にnormalize("NFKC")で全角を半角にそろえる- NFKC で直らないハイフン類(U+2010・U+2212・U+2015・U+30FC など)は自分で置換する。ただし文章全体には適用しない
- 文章から抽出するときは、前後に数字とハイフンが続かない条件を付ける。それでも誤検出は残るため確認を挟む
- 正規表現で分かるのは形式まで。実在確認が必要なら、日本郵便の公式データなどを使う
なお、本記事の挙動は Node.js v20.20.1 での実測であり、ブラウザや他バージョンの Node.js、サーバー側の言語では結果が異なる可能性があります。本番に組み込む前に、実際の環境で確認してください。