2018年に「電話番号の判定やりたくて」という記事を書いた。作り始めたばかりのgo-tel-num-parser-jpを紹介して、IsTelNumberの使い方を載せた短い記事だ。

その後も少し手を入れたが、リポジトリの更新は2020年で止まっていた。久しぶりに見返してみると、電話番号の区分は当時のままだし、判定方法にも気になるところがあった。そこで、現在公表されている番号情報を見ながら、番号区分と判定処理を更新した。

今回やったのは、新しい番号の正規表現を足すことだけではない。入力文字列の一部だけが電話番号に見えるとき、何を成功とするかも整理した。

何を判定するライブラリか

Goから使う小さなパッケージで、主な入口は二つある。

ok, kind := tnp.IsTelNumber("060-1234-5678")
// ok == true, kind == tnp.MobilePhone

number, err := tnp.CropTelNumber("連絡先: 0800-123-4567")
// number == "0800-123-4567", err == nil

IsTelNumberは入力全体が対応形式の電話番号かを判定する。CropTelNumberは文章などの中から最初に見つかった番号を取り出す。同じ正規表現を使うとしても、この二つで求める振る舞いは違う。

ここでいう「判定」は形式と区分の判定に限る。番号が実際に事業者へ指定されているか、現在使われているか、固定電話の市外局番が実在するかまでは確かめない。番号らしい形を見つける道具であって、電話をかけて到達できる番号の保証ではない。

2020年から増えたもの、直したもの

今回、READMEの対応表も作り直した。更新した区分のうち、特に分かりやすいのは次のあたりだ。

形式の例今回の扱い
060-1234-5678携帯電話の区分に追加
0200-12345-6789014桁のデータ伝送携帯電話として追加
0800-123-4567着信課金番号を11桁の形式に修正
0990-123-456情報料代理徴収の区分を追加
0600-123-4567FMC電話の区分を追加

020の番号には11桁のものと0200で始まる14桁のものがある。見た目が似ていても桁数は同じではない。0800も、以前のコードでは10桁の形で扱っていたので修正した。ハイフンなしの表記にも対応している。

もう一つ気を付けたいのが060と0600だ。前者を携帯電話の区分に加えたからといって、後者まで携帯電話として扱ってよいわけではない。0600は別のFMC区分として判定する。先頭の数文字だけで大ざっぱに分類すると、こういうところで間違える。

ただし、060を判定できることと、060の番号がすでに一般に利用されていることも別の話だ。番号計画上の形式を扱う実装として追加している。総務省の案内や指定状況へのリンクはREADMEにもまとめた。

正規表現に一致しただけでは困る

以前のIsTelNumberは、正規表現のMatchStringで判定していた。これは文字列のどこかに一致する部分があれば真になる。名前から期待する「この文字列全体が電話番号か」とは違う。

例えば、call 03-5321-1111は電話番号を含む文章だが、それ自体が電話番号ではない。IsTelNumberでは偽にし、CropTelNumberで取り出すべき入力だ。今回、この二つを共通の探索処理で扱いながら、IsTelNumberでは一致位置が文字列の先頭から末尾までかを確認するようにした。

CropTelNumberの方にも罠がある。01201234567のように一桁多い文字列から、先頭の0120123456だけを電話番号として切り出してしまうと、間違った番号を拾う。候補の直前や直後に数字が続いている場合は採用しないようにしている。文章中に複数の番号があれば、区分の列挙順ではなく、文字列内で最初に現れる完全な候補を返す。

ハイフンを除いた番号では、固定電話向けの広めのパターンがサービス番号の一部と重なることもある。0120や0570などを先に固有の区分で調べ、固定電話への誤った分類へ落ちないようにした。正規表現を一個ずつ見るだけでは、区分同士の重なりに気づきにくい。

03(5321)1111のような括弧付き表記も引き続き扱う。これは判定前に03-5321-1111へ置き換えるため、CropTelNumberから返る文字列もハイフン付きになる。

除外設定とテスト

以前から、特定の区分を判定対象から外すSetIgnoreTypesがある。今回、呼び出すたびに除外区分が積み重なる動作から、指定した区分で置き換える動作に変えた。引数なしのSetIgnoreTypes()で解除できる。

tnp.SetIgnoreTypes(tnp.MobilePhone)
// 携帯電話の区分を判定対象から外す

tnp.SetIgnoreTypes()
// 除外設定を解除する

共有する設定なので、読み書きはsync.RWMutexで保護した。一方で、パッケージ全体の設定であること自体は変わらない。呼び出しごとに異なる除外条件を渡すAPIではないので、利用する側ではその点に注意がいる。

今回は回帰テストも追加した。新しい区分の受理だけでなく、桁が一つ多い番号、以前の誤った0800形式、対象外の0170・0180、0600を携帯電話として扱わないことも確認する。手元のGo 1.27.1ではgo test ./...とgo test -race ./...が通っている。

ドキュメントも日本語のREADMEを整理し、英語版とMITライセンスを追加した。go.modの最低バージョンもGo 1.27.1に上げているので、以前の古いGo環境のまま使っていた場合はそこも変更点になる。

番号の「正しさ」には何段階かある

今回コードを直しながら、電話番号の「正しさ」は一つではないと改めて感じた。文字列全体が所定の桁数と形式か。どの番号区分に入るか。その番号が実際に割り当てられているか。今も使われていて発信できるか。後ろの方まで確認するには、正規表現だけでは足りない。

このパッケージが答えるのは最初の二つだ。だから、IsTelNumberが真でも実在性や到達性までは保証しないし、国際番号や緊急通報のような短縮番号も対象外にしている。この線引きはREADMEにも明記した。

2018年の記事では「作り始めた」としか書けなかった。今回は番号区分を今の資料に合わせ、判定と抽出の境界もテストで固定できた。小さなライブラリだが、久しぶりに開くと意外と直すところがあった。