Форматирование телефонных номеров на PHP

Возникла задача автоматического форматирования телефонных номеров в виде страна (город) номер, и первым делом я обратился к существующим решениям.
К сожалению, оказалось, что все найденные решения основываются на обычном подгоне строки под пользовательский формат, имея ограниченную область применения и ошибки при выходе за ее пределы.

Для начала приведу обзор найденных решений. Тем, кому это не интересно, рекомендую прокрутить ниже до заголовка «Форматы телефонных номеров» — там уже представлен мой вариант разбора номера с ссылкой на код.

Минусующим, отписывайтесь, пожалуйста о причине минуса — исправлю недочеты, если есть.
 

Всеуничтожающий примитив


(Найденное решение. Мое ниже)
Первое, на что я наткнулся — были сообщения на форумах и банки скриптов, предлагающие решения следующего плана:

<?
function phone_number($sPhone){
    $sPhone = ereg_replace("[^0-9]",'',$sPhone);
    if(strlen($sPhone) != 10) return(False);
    $sArea = substr($sPhone, 0,3);
    $sPrefix = substr($sPhone,3,3);
    $sNumber = substr($sPhone,6,4);
    $sPhone = "(".$sArea.")".$sPrefix."-".$sNumber;
    return($sPhone);
}
?>


Один из простых вариантов шустрого форматирования телефонных номеров, но каждое такое решение ориентировано на телефонные номера из конкретной локальной зоны и не является решением задачи.
 

Форматирование с помощью sscanf


(Найденное решение. Мое ниже)

function formatPhone($phone) {
    if (empty($phone)) return "";
    if (strlen($phone) == 7)
        sscanf($phone, "%3s%4s", $prefix, $exchange);
    else if (strlen($phone) == 10)
            sscanf($phone, "%3s%3s%4s", $area, $prefix, $exchange);
        else if (strlen($phone) > 10)
                if(substr($phone, 0,1)=='1') {
                    sscanf($phone, "%1s%3s%3s%4s", $country, $area, $prefix, $exchange);
                }
                else{
                    sscanf($phone, "%3s%3s%4s%s", $area, $prefix, $exchange, $extension);
            }
            else
                return "unknown phone format: $phone";
    $out = "";
    $out .= isset($country) ? $country.' ' : '';
    $out .= isset($area) ? '(' . $area . ') ' : '';
    $out .= $prefix . '-' . $exchange;
    $out .= isset($extension) ? ' x' . $extension : '';
    return $out;
}


Не смотря на простое решение, эта функция уже умеет форматировать номера длиной 7, 10 и более цифр, но попадись ей номер из российской глубинки, она подавится и выдаст ошибочный результат.
 

Symphony, lib/helpers/PhoneHelper.php, format_phone


(Найденное решение. Мое ниже)

<?php
function format_phone($phone = '', $convert = false, $trim = true)
{
    // If we have not entered a phone number just return empty
    if (empty($phone)) {
        return '';
    }
 
    // Strip out any extra characters that we do not need only keep letters and numbers
    $phone = preg_replace("/[^0-9A-Za-z]/", "", $phone);
 
    // Do we want to convert phone numbers with letters to their number equivalent?
    // Samples are: 1-800-TERMINIX, 1-800-FLOWERS, 1-800-Petmeds
    if ($convert == true) {
        $replace = array('2'=>array('a','b','c'),
                 '3'=>array('d','e','f'),
                     '4'=>array('g','h','i'),
                 '5'=>array('j','k','l'),
                                 '6'=>array('m','n','o'),
                 '7'=>array('p','q','r','s'),
                 '8'=>array('t','u','v'), '9'=>array('w','x','y','z'));
 
        // Replace each letter with a number
        // Notice this is case insensitive with the str_ireplace instead of str_replace 
        foreach($replace as $digit=>$letters) {
            $phone = str_ireplace($letters, $digit, $phone);
        }
    }
 
    // If we have a number longer than 11 digits cut the string down to only 11
    // This is also only ran if we want to limit only to 11 characters
    if ($trim == true && strlen($phone)>11) {
        $phone = substr($phone,  0, 11);
    }
 
    // Perform phone number formatting here
    if (strlen($phone) == 7) {
        return preg_replace("/([0-9a-zA-Z])([0-9a-zA-Z])/", "$1-$2", $phone);
    } elseif (strlen($phone) == 10) {
        return preg_replace("/([0-9a-zA-Z])([0-9a-zA-Z])([0-9a-zA-Z])/", "($1) $2-$3", $phone);
    } elseif (strlen($phone) == 11) {
        return preg_replace("/([0-9a-zA-Z])([0-9a-zA-Z])([0-9a-zA-Z])([0-9a-zA-Z])/", "$1($2) $3-$4", $phone);
    }
 
    // Return original phone if not 7, 10 or 11 digits long
    return $phone;
}
?>


Функция позволяет не только форматировать в XXX-XXXX, (XXX) XXX-XXXX и X (XXX) XXX-XXXX, но и конвертировать номера, написанные цифрами. Ограниченность функции в форматировании номеров длиной 7, 10 и 11 символов никак не подходит.
 

Форматы телефонных номеров


Из вики-статьи видно, что никакого простого и удобного паттерна для быстрого форматирования всех номеров не существует. Коды стран регистрируются, подобно доменным зонам, а коды городов — остаются на совести каждой из стран.

Другими словами, маршрутизация звонков идет по маске, начиная с кода страны: звонок, направленный в конкретную страну далее пробивает себе маршрут в соответствии с кодами области, города, района и т.д. начиная с самой левой цифры, пока последнее звено не перебросит его на конкретный телефонный/факсовый аппарат. Проблема усложняется еще и тем, что коды городов внутри стран точно так же не поддаются единой сквозной стандартизации, т.е. в худшем из вариантов для правильного форматирования номеров придется использовать двумерный массив с кодами стран и их городов.

На самом деле, все оказалось не так страшно. В каждой стране можно разделить все коды городов на две части: на те, что в большинстве своем совпадают по длине, и все остальные. Этого достаточно, чтобы резко сократить область перебора кодов при сравнении. Т.е. можно создать массив из данных по каждой стране вида:

<?
$data = Array(
'Код страны'=>Array(
        'name'=>'Имя страны', // для удобства. Не будет использоваться.
        'cityCodeLength'=> обычная_длина_кода_города_для_этой_страны,
        'exceptions'=>Array(коды_городов_исключения),
    )
);
?>


Затем провести предварительную обработку данных, дополнив его полями, сужающими область перебора, exceptions_max и exceptions_min — максимальной и минимальной длиной кода городов-исключений, соответственно. Также необходимо учесть страны, в которых коды городов начинаются на 0 — отразим эту «особенность» полем zeroHack. Как пример:

<?
$data = Array(
'886'=>Array(
        'name'=>'Taiwan',
        'cityCodeLength'=>1,
        'zeroHack'=>false,
        'exceptions'=>Array(89,90,91,92,93,96,60,70,94,95),
        'exceptions_max'=>2,
        'exceptions_min'=>2
    ),
);
?>


После этого возьмем подходящие участки кода из решений выше и сделаем функцию форматирования:

<?
function phone($phone = '', $convert = true, $trim = true)
{
    global $phoneCodes; // только для примера! При реализации избавиться от глобальной переменной.
    if (empty($phone)) {
        return '';
    }
    // очистка от лишнего мусора с сохранением информации о "плюсе" в начале номера
    $phone=trim($phone);
    $plus = ($phone[ 0] == '+');
    $phone = preg_replace("/[^0-9A-Za-z]/", "", $phone);
    $OriginalPhone = $phone;
 
    // конвертируем буквенный номер в цифровой
    if ($convert == true && !is_numeric($phone)) {
        $replace = array('2'=>array('a','b','c'),
        '3'=>array('d','e','f'),
        '4'=>array('g','h','i'),
        '5'=>array('j','k','l'),
        '6'=>array('m','n','o'),
        '7'=>array('p','q','r','s'),
        '8'=>array('t','u','v'),
        '9'=>array('w','x','y','z'));
 
        foreach($replace as $digit=>$letters) {
            $phone = str_ireplace($letters, $digit, $phone);
        }
    }
 
    // заменяем 00 в начале номера на +
    if (substr($phone,  0, 2)=="00")
    {
        $phone = substr($phone, 2, strlen($phone)-2);
        $plus=true;
    }
 
    // если телефон длиннее 7 символов, начинаем поиск страны
    if (strlen($phone)>7)
    foreach ($phoneCodes as $countryCode=>$data)
    {
        $codeLen = strlen($countryCode);
        if (substr($phone,  0, $codeLen)==$countryCode)
        {
            // как только страна обнаружена, урезаем телефон до уровня кода города
            $phone = substr($phone, $codeLen, strlen($phone)-$codeLen);
            $zero=false;
            // проверяем на наличие нулей в коде города
            if ($data['zeroHack'] && $phone[ 0]=='0')
            {
                $zero=true;
                $phone = substr($phone, 1, strlen($phone)-1);
            }
 
            $cityCode=NULL;
            // сначала сравниваем с городами-исключениями
            if ($data['exceptions_max']!= 0)
            for ($cityCodeLen=$data['exceptions_max']; $cityCodeLen>=$data['exceptions_min']; $cityCodeLen--)
            if (in_array(intval(substr($phone,  0, $cityCodeLen)), $data['exceptions']))
            {
                $cityCode = ($zero ? "0" : "").substr($phone,  0, $cityCodeLen);
                $phone = substr($phone, $cityCodeLen, strlen($phone)-$cityCodeLen);
                break;
            }
            // в случае неудачи с исключениями вырезаем код города в соответствии с длиной по умолчанию
            if (is_null($cityCode))
            {
                $cityCode = substr($phone,  0, $data['cityCodeLength']);
                $phone = substr($phone, $data['cityCodeLength'], strlen($phone)-$data['cityCodeLength']);
            }
            // возвращаем результат
            return ($plus ? "+" : "").$countryCode.'('.$cityCode.')'.phoneBlocks($phone);
        }
    }
    // возвращаем результат без кода страны и города
    return ($plus ? "+" : "").phoneBlocks($phone);
}
 
// функция превращает любое число в строку формата XX-XX-... или XXX-XX-XX-... в зависимости от четности кол-ва цифр
function phoneBlocks($number){
    $add='';
    if (strlen($number)%2)
    {
        $add = $number[ 0];
        $add .= (strlen($number)<=5 ? "-" : "");
        $number = substr($number, 1, strlen($number)-1);
    }
    return $add.implode("-", str_split($number, 2));
}
 
// тесты
echo phone("+38 (044) 226-22-04")."<br />";
echo phone("0038 (044) 226-22-04")."<br />";
echo phone("+79263874814")."<br />";
echo phone("4816145")."<br />";
echo phone("+44 (0) 870 770 5370")."<br />";
echo phone("0044 (0) 870 770 5370")."<br />";
echo phone("+436764505509")."<br />";
echo phone("(+38-048) 784-15-46 ")."<br />";
echo phone("(38-057) 706-34-03 ")."<br />";
echo phone("+38 (044) 244 12 01 ")."<br />";
?>


, где global $phoneCodes; — тот самый массив с информацией по всем странам.

Выведет

+380(44)226-22-04
+380(44)226-22-04
+7(926)387-48-14
481-61-45
+44(0870)770-53-70
+44(0870)770-53-70
+43(6764)50-55-09
380(4878)415-46
380(5770)634-03
+380(44)244-12-01


Функция полностью решает поставленную задачу.
Из недостатков функции следует отметить отсутствие анализа медленных участков с целью оптимизаци, а также обработки телефонных номеров, где есть код города, но нет кода страны (в этом случае достаточно бить на блоки функцией phoneBlocks или воспользоваться одним из решений выше). При использовании ее в какой-либо реализации необходимо заменить глобальную переменную на ссылку в параметре, а также можно доработать или заменить формат вывода, за который отвечает функция phoneBlocks.

Быстродействие


Вопреки всем самым пессимистичным ожиданиям, код отрабатывает 10.000 номеров менее, чем за 2 секунды.

UPD Готовятся поправки:

  1. поддержка паттернов форматирования, принятых внутри конкретных стран («локально-принятые» нормы отображения номеров);
  2. добавление флага для указания, относительно какой страны выполнять форматирование номера;
  3. добавление параметра для указания формата вывода (в случае личных предпочтений и исключений);
  4. поддержка нелатинских буквенных номеров
  5. определение сотовых номеров и замена скобок на пробелы



Рекомендуем почитать

 

Добавить комментарий


Ваше имя:


Комментарий:


Введите: Картинка