Форматирование телефонных номеров на 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 Готовятся поправки:
- поддержка паттернов форматирования, принятых внутри конкретных стран («локально-принятые» нормы отображения номеров);
- добавление флага для указания, относительно какой страны выполнять форматирование номера;
- добавление параметра для указания формата вывода (в случае личных предпочтений и исключений);
- поддержка нелатинских буквенных номеров
- определение сотовых номеров и замена скобок на пробелы
Рекомендуем почитать