Analog Studio

Javascriptで電話番号をハイフン区切りに整形する方法

概要

お問い合わせフォームなどで電話番号を入力してもらいたいことがありますよね。
見やすさや管理のしやすさから3つの入力欄を設けている場合が多いです。

しかし、ユーザの観点から云えば入力欄を切り替えなければならず入力が面倒だと感じます。
そこで今回は、入力された電話番号をハイフン (-) 区切りに変換する方法を紹介します。
また不要な文字は削除して半角の整形された電話番号を返すようにしてユーザの負担を最小限にしょましょう。

SQL などの DB に保存する際に「市外局番、市内局番、識別番号」と分けたい場合にも区切り文字があるので簡単に分けて保存しておくこともできます。
では、実際にやっていきましょう。

電話番号の仕様

まずは大前提として、国内で使用されている一般的な電話番号のみを対象としています。
詳しい仕様については総務省が公開している情報をご確認下さい。

Q&A を見るとどんな規則で電話番号が決まっているのかが載っています。
また、市外局番は平成30年6月14日時点でのデータを基にしています。(当記事執筆時点で最新データ)

実際に使用される電話番号の区切りは「電気通信番号指定状況」と「市外局番の一覧」にある資料から確認できます。
大まかな規則は何となくありますが明確な規則などはなく「この番号はこの区切り」となっています。

完成した変換用関数とデータ

まずは完成した変換用関数とデータを以下に示します。
詳しいことはどうでもいいよ!という方はここまでを参考にして頂ければ結構です。

以下のコードを定義して変換したい電話番号っぽい数字を引数に渡せば変換結果が返ってきます。
例)getFormatPhone("09012345678"); // -> 090-1234-5678

// 電話番号を整形する var getFormatPhone = function($INPUT, $STRICT){ $STRICT = $STRICT || false; // 市外局番のグループ定義 // データは http://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/number_shitei.html より入手後、整形 var group = { 5 : { "01267" : 1, "01372" : 1, "01374" : 1, "01377" : 1, "01392" : 1, "01397" : 1, "01398" : 1, "01456" : 1, "01457" : 1, "01466" : 1, "01547" : 1, "01558" : 1, "01564" : 1, "01586" : 1, "01587" : 1, "01632" : 1, "01634" : 1, "01635" : 1, "01648" : 1, "01654" : 1, "01655" : 1, "01656" : 1, "01658" : 1, "04992" : 1, "04994" : 1, "04996" : 1, "04998" : 1, "05769" : 1, "05979" : 1, "07468" : 1, "08387" : 1, "08388" : 1, "08396" : 1, "08477" : 1, "08512" : 1, "08514" : 1, "09496" : 1, "09802" : 1, "09912" : 1, "09913" : 1, "09969" : 1, }, 4 : { "0123" : 2, "0124" : 2, "0125" : 2, "0126" : 2, "0133" : 2, "0134" : 2, "0135" : 2, "0136" : 2, "0137" : 2, "0138" : 2, "0139" : 2, "0142" : 2, "0143" : 2, "0144" : 2, "0145" : 2, "0146" : 2, "0152" : 2, "0153" : 2, "0154" : 2, "0155" : 2, "0156" : 2, "0157" : 2, "0158" : 2, "0162" : 2, "0163" : 2, "0164" : 2, "0165" : 2, "0166" : 2, "0167" : 2, "0172" : 2, "0173" : 2, "0174" : 2, "0175" : 2, "0176" : 2, "0178" : 2, "0179" : 2, "0182" : 2, "0183" : 2, "0184" : 2, "0185" : 2, "0186" : 2, "0187" : 2, "0191" : 2, "0192" : 2, "0193" : 2, "0194" : 2, "0195" : 2, "0197" : 2, "0198" : 2, "0220" : 2, "0223" : 2, "0224" : 2, "0225" : 2, "0226" : 2, "0228" : 2, "0229" : 2, "0233" : 2, "0234" : 2, "0235" : 2, "0237" : 2, "0238" : 2, "0240" : 2, "0241" : 2, "0242" : 2, "0243" : 2, "0244" : 2, "0246" : 2, "0247" : 2, "0248" : 2, "0250" : 2, "0254" : 2, "0255" : 2, "0256" : 2, "0257" : 2, "0258" : 2, "0259" : 2, "0260" : 2, "0261" : 2, "0263" : 2, "0264" : 2, "0265" : 2, "0266" : 2, "0267" : 2, "0268" : 2, "0269" : 2, "0270" : 2, "0274" : 2, "0276" : 2, "0277" : 2, "0278" : 2, "0279" : 2, "0280" : 2, "0282" : 2, "0283" : 2, "0284" : 2, "0285" : 2, "0287" : 2, "0288" : 2, "0289" : 2, "0291" : 2, "0293" : 2, "0294" : 2, "0295" : 2, "0296" : 2, "0297" : 2, "0299" : 2, "0422" : 2, "0428" : 2, "0436" : 2, "0438" : 2, "0439" : 2, "0460" : 2, "0463" : 2, "0465" : 2, "0466" : 2, "0467" : 2, "0470" : 2, "0475" : 2, "0476" : 2, "0478" : 2, "0479" : 2, "0480" : 2, "0493" : 2, "0494" : 2, "0495" : 2, "0531" : 2, "0532" : 2, "0533" : 2, "0536" : 2, "0537" : 2, "0538" : 2, "0539" : 2, "0544" : 2, "0545" : 2, "0547" : 2, "0548" : 2, "0550" : 2, "0551" : 2, "0553" : 2, "0554" : 2, "0555" : 2, "0556" : 2, "0557" : 2, "0558" : 2, "0561" : 2, "0562" : 2, "0563" : 2, "0564" : 2, "0565" : 2, "0566" : 2, "0567" : 2, "0568" : 2, "0569" : 2, "0572" : 2, "0573" : 2, "0574" : 2, "0575" : 2, "0576" : 2, "0577" : 2, "0578" : 2, "0581" : 2, "0584" : 2, "0585" : 2, "0586" : 2, "0587" : 2, "0594" : 2, "0595" : 2, "0596" : 2, "0597" : 2, "0598" : 2, "0599" : 2, "0721" : 2, "0725" : 2, "0735" : 2, "0736" : 2, "0737" : 2, "0738" : 2, "0739" : 2, "0740" : 2, "0742" : 2, "0743" : 2, "0744" : 2, "0745" : 2, "0746" : 2, "0747" : 2, "0748" : 2, "0749" : 2, "0761" : 2, "0763" : 2, "0765" : 2, "0766" : 2, "0767" : 2, "0768" : 2, "0770" : 2, "0771" : 2, "0772" : 2, "0773" : 2, "0774" : 2, "0776" : 2, "0778" : 2, "0779" : 2, "0790" : 2, "0791" : 2, "0794" : 2, "0795" : 2, "0796" : 2, "0797" : 2, "0798" : 2, "0799" : 2, "0820" : 2, "0823" : 2, "0824" : 2, "0826" : 2, "0827" : 2, "0829" : 2, "0833" : 2, "0834" : 2, "0835" : 2, "0836" : 2, "0837" : 2, "0838" : 2, "0845" : 2, "0846" : 2, "0847" : 2, "0848" : 2, "0852" : 2, "0853" : 2, "0854" : 2, "0855" : 2, "0856" : 2, "0857" : 2, "0858" : 2, "0859" : 2, "0863" : 2, "0865" : 2, "0866" : 2, "0867" : 2, "0868" : 2, "0869" : 2, "0875" : 2, "0877" : 2, "0879" : 2, "0880" : 2, "0883" : 2, "0884" : 2, "0885" : 2, "0887" : 2, "0889" : 2, "0892" : 2, "0893" : 2, "0894" : 2, "0895" : 2, "0896" : 2, "0897" : 2, "0898" : 2, "0920" : 2, "0930" : 2, "0940" : 2, "0942" : 2, "0943" : 2, "0944" : 2, "0946" : 2, "0947" : 2, "0948" : 2, "0949" : 2, "0950" : 2, "0952" : 2, "0954" : 2, "0955" : 2, "0956" : 2, "0957" : 2, "0959" : 2, "0964" : 2, "0965" : 2, "0966" : 2, "0967" : 2, "0968" : 2, "0969" : 2, "0972" : 2, "0973" : 2, "0974" : 2, "0977" : 2, "0978" : 2, "0979" : 2, "0980" : 2, "0982" : 2, "0983" : 2, "0984" : 2, "0985" : 2, "0986" : 2, "0987" : 2, "0993" : 2, "0994" : 2, "0995" : 2, "0996" : 2, "0997" : 2, "0180" : 3, "0570" : 3, "0800" : 3, "0990" : 3, "0120" : 3, }, 3 : { "011" : 3, "015" : 3, "017" : 3, "018" : 3, "019" : 3, "022" : 3, "023" : 3, "024" : 3, "025" : 3, "026" : 3, "027" : 3, "028" : 3, "029" : 3, "042" : 3, "043" : 3, "044" : 3, "045" : 3, "046" : 3, "047" : 3, "048" : 3, "049" : 3, "052" : 3, "053" : 3, "054" : 3, "055" : 3, "058" : 3, "059" : 3, "072" : 3, "073" : 3, "075" : 3, "076" : 3, "077" : 3, "078" : 3, "079" : 3, "082" : 3, "083" : 3, "084" : 3, "086" : 3, "087" : 3, "088" : 3, "089" : 3, "092" : 3, "093" : 3, "095" : 3, "096" : 3, "097" : 3, "098" : 3, "099" : 3, "050" : 4, "020" : $STRICT ? 3 : 4, "070" : $STRICT ? 3 : 4, "080" : $STRICT ? 3 : 4, "090" : $STRICT ? 3 : 4, }, 2 : { "03" : 4, "04" : 4, "06" : 4, } }; // 市外局番の桁数を取得して降順に並べ替える var code = []; for(num in group){ code.push(num * 1); } code.sort(function($a, $b){ return ($b - $a); }); // 入力文字から数字以外を削除してnumber変数に格納する var number = String($INPUT).replace(/[0-9]/g, function($s){ return String.fromCharCode($s.charCodeAt(0) - 65248); }).replace(/\D/g, ""); // 電話番号が10~11桁じゃなかったらfalseを返して終了する if(number.length < 10 || number.length > 11){ return false; } // 市外局番がどのグループに属するか確認していく for(var i = 0, n = code.length; i < n; i++){ var leng = code[i]; var area = number.substr(0, leng); var city = group[leng][area]; // 一致する市外局番を見付けたら整形して整形後の電話番号を返す if(city){ return area + "-" + number.substr(leng, city) + (number.substr(leng + city) !== "" ? "-" + number.substr(leng + city) : ""); } } };

実行結果

<INPUT> 要素に change イベントを登録して変更されたら変換されるようにすると以下のようになります。

HTML5 なら <INPUT> 要素の type 属性に "tel" を指定しておくと良いでしょう。
PC なら半角入力になります。スマホなら半角数字とハイフンの入力に切り替わります。
※ PC は全角入力にすることもできます。
※上のサンプルでは全角数字なども入力しやすいように type 属性は未指定です。

<HTML> <input type="tel" id="sample" placeholder="電話番号を入力して下さい"> <Javascript> window.addEventListener("DOMContentLoaded", function(){ document.getElementById("sample").addEventListener("change", function(){ var p = getFormatPhone(this.value); if(p){ this.value = p; } }, false); }, false); <CSS> ::placeholder{ color : #969696; } #sample{ padding : 10px; border : 1px solid #707070; border-radius : 7px; }

電話番号の整形コード解説

では、実際にどんな処理で電話番号の区切りを見付けているのかを解説します。

ちなみにですが、元ネタは Qiita の[PHP] ハイフンなしの電話番号からハイフン付き電話番号を復元する (@mpywさん) です。
こちらを基にデータを最新版に更新し、Javascript 用に作り直しています。(動作原理は同じです)
※データに違いがあるかは未検証です。

また、以下で "市外局番" や "市内局番" という言葉を共通して使いますが、固定電話以外はそれぞれ "1つ目の数字の集まり"、"2つ目の数字の集まり" と読み替えてご理解頂ければと思います。

データ構造

電話番号のデータは2次元連想配列として値を格納しています。

$STRICT = $STRICT || false; var group = { 5 : { "01267" : 1, "01372" : 1, "01374" : 1, "01377" : 1, /* (以下中略) */ "09969" : 1, }, 4 : { "0123" : 2, "0124" : 2, "0125" : 2, "0126" : 2, /* (以下中略) */ "0120" : 3, }, 3 : { "011" : 3, "015" : 3, "017" : 3, "018" : 3, "019" : 3, /* (以下中略) */ "099" : 3, "050" : 4, "020" : $STRICT ? 3 : 4, "070" : $STRICT ? 3 : 4, "080" : $STRICT ? 3 : 4, "090" : $STRICT ? 3 : 4, }, 2 : { "03" : 4, "04" : 4, "06" : 4, } };

1層目には市外局番の桁数をキーとしており、このキーで入力値を区切って実際に存在するかを確認しています。

2層目は実際に使われている市外局番をキーとしてその後に続く市内局番の桁数を値として持っています。
こうすることで1層目の桁数で区切った時に市外局番が存在するかが一発で取得できます。

仮に1次元の連想配列で作ると市外局番を1つずつ確認していくと for ループが長くなるのでデータが多い場合には動作が遅くなります。
(この程度のデータ量なら1次元配列でも全然問題ないと思います)

ここで 020 / 070 / 080 / 090 だけ値が $STRICT 引数によって条件分けされていますね。
これは携帯電話や PHS などの番号は、正確には総務省の資料から "0*0 - 3桁 - 5桁" という形式となっています。
しかしながら一般的には "0*0 - 4桁 - 4桁" の方が見慣れているので正確な区切りが要求されない限りは一般的な方が良いので条件分けしています。

第2引数に設定できるようにしてありますが、不要な時に省略できるよう "$STRICT || false" で省略時には "false" が代入されます。
IE10 以前のブラウザを無視できるのであれば関数定義式を以下のようにしても同じように動きます。
(PHP と同じように初期値を書けます)

※今回の関数なら "false" であることを確認しているわけではないので省略して "undefine" のままでも問題なく動きます。ただ、キモチワルイので何らかの値で確定させます。

// 古いブラウザまで考慮する場合 var getFormatPhone = function($INPUT, $STRICT){ $STRICT = $STRICT || false; /* 処理 */ }; // 古いブラウザを無視できる場合 var getFormatPhone = function($INPUT, $STRICT = false){ /* 処理 */ };

市外局番の桁数キーを降順で並べる

次に市外局番の桁数が "大きい方" から検証していくので連想配列のキー (=桁数) を取得して並べ替えます。

for-in 構文でいいんじゃないの?と思った方もいると思いますが、Javascript の for-in 構文では配列のどこから実行されるか保証されないので、今回のように "大きい方から順番に" 実行したい時には使用することができません。
※私の環境 (Opera や Google Chrome) ではキーを昇順で並べ替えた順に実行されました。

// 市外局番の桁数を取得して降順に並べ替える var code = []; for(num in group){ code.push(num * 1); } code.sort(function($a, $b){ return ($b - $a); });

このコードではまずは for-in で全てのキーを取得してから Array.sort() メソッドで降順にしています。
取得できた桁数の配列値を順に使って連想配列にアクセスすれば桁数の大きな方から配列が取得できます。

sort メソッドの引数に関数を渡すと任意の判定条件で並べ替えができます。
数値として比較して降順になるように値を返せばOKです。

入力値を数字のみに変換しておく

メインの処理へ入る前に入力値を数字のみの文字列に変換しておく必要があります。
そこで、全角数字を半角に変換して数字以外の文字を全て削除してしまいましょう。

全角文字から半角文字への変換は郵便番号検索と同じ手法で、文字コードを -65248 することで変換できます。  ・郵便番号から自動で住所を検索する方法

// 入力文字から数字以外を削除してnumber変数に格納する var number = String($INPUT).replace(/[0-9]/g, function($s){ return String.fromCharCode($s.charCodeAt(0) - 65248); }).replace(/\D/g, ""); // 電話番号が10~11桁じゃなかったらfalseを返して終了する if(number.length < 10 || number.length > 11){ return false; }

まずは、引数 $INPUT に代入される値は文字列以外に数字が入力されることもありますので、String.replace() メソッドが使えるように文字列へ変換します。
色々な方法がありますが、簡潔な方法として String() に $INPUT を渡して文字列を生成する方法があります。
ここでは String() を使いましたが空文字を足して文字列に変換しても良いです。

文字列になったら正規表現 (/[0-9]/g) で全ての全角数字を拾って文字コードを差分だけ引いてまた文字 (=半角数字) に直して置換します。
半角数字になったら数字以外 (/\D/g) を削除して数字のみの文字列が完成です。

ついでに電話番号は10~11桁なので、範囲外の時は false を返して処理を終了させておきます。
見やすさの為にここで処理していますが、関数内の一番最初で処理させた方が無効な入力値で無駄な処理がされなくなります。

市外局番とそれに続く市内局番の桁数を検索

最後に市外局番とそれに続く市内局番の桁数を検索して整形後の電話番号を返していきます。

// 市外局番がどのグループに属するか確認していく for(var i = 0, n = code.length; i < n; i++){ var leng = code[i]; var area = number.substr(0, leng); var city = group[leng][area]; // 一致する市外局番を見付けたら整形して整形後の電話番号を返す if(city){ return area + "-" + number.substr(leng, city) + (number.substr(leng + city) !== "" ? "-" + number.substr(leng + city) : ""); } }

配列 code[] には市外局番の桁数が大きいものから順に格納されているので、順番に変数 leng に代入して使用します。
number.substr(0, leng) で市外局番に当たる可能性がある数字が抽出され、変数 city で連想配列で市外局番のキーが存在するか、存在すれば市内局番の桁数を代入します。

市外局番がデータ内に存在すれば市内局番の桁数が決まるので残りの区切り位置が決まり整形完了となります。
一番最後の三項演算子は2つ区切りの番号を想定していますが、おそらく使われていないので number.substr(leng + city) だけに変えても良いと思います。

実装例

最後に実装例を以下に示します。
上の方でも示したサンプルのように chnage イベントに登録しておくのが無難だと思います。


まとめ

以上、電話番号をハイフン区切りで整形する方法をまとめました。
電話番号を3つの欄で入力させるような利便性の低いフォームが早くなくなればいいですね!