ハイフンなし電話番号にハイフン付きに変換する

Salesforceの電話型はハイフンの有無に関わらず電話番号として認識してくれますが、ハイフン付き電話番号に統一したいこともあるでしょう。あるんです。ありました。
Apexでは見かけなかったので、先人たちの教えを参考に書いたものをメモとして残しておきます。

# 電話番号とは
[総務省の電話番号に関するQ&A](https://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/number_shitei.html)をもとに、電話番号の条件を書き出してみます。
一般的な国内電話番号のみとし、特番、シャープダイヤル、衛星電話、国際電話は除外します。

* 共通
    * 1桁目は必ず0から始まる（国内プレフィクス）
* 固定電話（全10桁）
    * 市外局番：国内プレフィクスを含む2〜5桁
    * 市内局番：市外局番の桁数に応じ1〜4桁間で変動
        * 市内局番 + 市外局番 = 必ず6桁
    * 加入者番号：4桁固定
* 携帯電話（全11桁）
    * 市外局番：070/080/090のいずれか
        * 今後060が携帯電話向けとして使用予定
    * 市内局番：0以外の数字から始まる4桁固定
    * 加入者番号：4桁固定
* 特殊番号（全10桁）
    * 着信課金用電話番号：0120-DEF-XXX
    * 着信課金用電話番号：0800-DEF-XXX
    * 統一番号用電話番号：0570-DEF-XXX

# 固定電話の市外/市内局番一覧をcsv化
固定電話の桁数変動する市内局番と市外局番の条件付けが難しいので、[総務省の市外局番の一覧](https://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/shigai_list.html)で配布されている市内局番一覧をcsv化して静的リソースに配置。
そこから読み込んで処理を行うことにしました。

![スクリーンショット 2022-12-20 11.57.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3075648/49a70ad0-98ce-6341-64bf-285ff50fce2a.png)
こんな感じでcsv化して配置します。


# 処理を書く
```Apex
// 電話番号桁数
private static final Integer LAND_PHONE_LEN = 10;
private static final Integer MOBILE_PHONE_LEN = 11;

// 市外局番 : 市内局番桁数Map
private static final Map<String, Integer> AREA_CODE_MAP = new Map<String, Integer>();
static {
    // ★総務省の市外局番一覧ページから取得しています★
    StaticResource sr = [SELECT Id, Body FROM StaticResource WHERE Name = 'PhoneNumberAreaCodeList' LIMIT 1];
    List<String> body = sr.Body.toString().split('\r\n|\n');
    body.remove(0);
    for (String b : body) {
        List<String> vals = b.split(',');
        // key: 国内プレフィクス0付与
        // val: 正規表現で[最初の1桁]と[残り桁]をチェックするので-1しておく
        AREA_CODE_MAP.put('0' + vals.get(0), vals.get(1).length() - 1);
    }
    // 特殊番号は市内局番全桁でチェックするので-1しない
    AREA_CODE_MAP.put('0120', 3); // 着信課金用電話番号:0120-DEF-XXX
    AREA_CODE_MAP.put('0800', 3); // 着信課金用電話番号:0800-DEF-XXX
    AREA_CODE_MAP.put('0570', 3); // 統一番号用電話番号:0570-DEF-XXX
}

/**
 * ハイフンなしの電話番号にハイフンを付与する
 * @param phone 電話番号
 * @return ハイフンあり電話番号
 */
public static String restoreHyphen(String phone) {
    
    // 数字のみ抽出
    String val = convPickNum(phone);
    
    // 携帯 & IP電話番号チェック
    if (val.length() == MOBILE_PHONE_LEN) {
        // 050:IP電話 060:携帯電話向け使用予定 070,080,090:携帯電話番号
        // 1桁目は1〜9であること
        Matcher m = Pattern.compile('^(0[56789]0)([1-9]\\d{3})(\\d{4})$').matcher(val);
        if (m.find()) {
            // 携帯電話
            val = m.group(1) + '-' + m.group(2) + '-' + m.group(3);
        }
    }
    
    // 固定電話番号 & 特殊電話番号
    if (val.length() == LAND_PHONE_LEN) {
        // 市外局番2〜5桁、桁数が大きい順で検索する
        for (Integer i = 5; i >= 2; i--) {
            // 市外局番取得
            String areaCode = phone.left(i);
            if (! AREA_CODE_MAP.containsKey(areaCode)) continue;
            
            String regex;
            if (areaCode == '0120' || areaCode == '0800' || areaCode == '0570') {
                // 特殊番号は市内局番全桁チェック
                regex = '^(' + areaCode +')(\\d{' + AREA_CODE_MAP.get(areaCode) + '})(\\d{3})$';
            } else {
                // 市内局番の1桁目が2〜9であること
                regex = '^(' + areaCode +')([2-9]\\d{' + AREA_CODE_MAP.get(areaCode) + '})(\\d{4})$';
            }
            Matcher m = Pattern.compile(regex).matcher(val);
            if (m.find()) {
                val = m.group(1) + '-' + m.group(2) + '-' + m.group(3);
                break;
            }
        }
    }
    return val;
}

/**
 * 数字のみ抽出
 */
public static String convPickNum(String str) {
    
    // 空白ならリターン
    if(String.isBlank(str)) return '';
    
    Pattern p = Pattern.compile('\\d+');
    Matcher m = p.matcher(str);
    
    List<String> s = new List<String>();
    while (m.find()) {
        s.add(m.group());
    }
    return (! s.isEmpty()) ? String.join(s, '') : '';
}
```
パターンに一致しない番号が来た際はハイフンなしで返却します。

# 参考にさせていただいた記事
* https://qiita.com/kennyj@github/items/dde3880abb5071f649ab
* https://qiita.com/mpyw/items/431c0c8cb70084a74be5
* https://qiita.com/tomgoodsun/items/f3050689ac84827d5af9