Поводом для появления серии статей, первая из которых представлена вашему вниманию, послужил большой аналитический и практический материал, накопившийся в процессе работы над библиотекой MSLibrary for IOS. Библиотека MSLibrary включает множество классов, и еще больше функций и макросов, призванных упростить рутинный труд разработчиков, существенно сократить сроки разработки и размер кода. Но, всему свое время, о библиотеке мы расскажем чуть позже.
Итак, захват и верификация телефонных номеров с помощью регулярных выражений. Казалось бы, о чем здесь говорить? Кто умеет, напишет сам, а кто не умеет скопирует одно из множества готовых решений, разбросанных на просторах Всемирной паутины. Вопрос только в том, что он напишет и что скопирует и насколько этот код будет соответствовать поставленным задачам, а также действующим международным, отраслевым и корпоративным стандартам? Любое решение, даже самое простое, хорошо лишь в том случае, если разработчик полностью осознает его работу и абсолютно в нем уверен.
Любое решение, даже самое простое, хорошо лишь в том случае, если разработчик полностью осознает его работу и абсолютно в нем уверен.
По сути тема статьи разделяется на две уже в самом заголовке. Захват — это умение понять, что трестируемый набор цифр и знаков может быть телефонным номером. А верификация — это проверка соответствует ли этот набор неким, предварительно поставленным, условиям. В том, что эти задачи разные легко убедиться, посмотрев на приведенный пример:
+ 1 (408) - 996 - 10 - 10 = 1234 +14089961010;ext=1234
В первом случае некто попытался записать телефон в той манере в которой привык это делать, а во втором — телефон записан в соответствии с международным стандартом RFC 3966 . Проблема в том, что если мы попробуем использовать обе эти записи для набора телефонного номера, например в IOS приложении, то, к сожалению, ничего хорошего не получим. В первом случае система вообще ничего не поймет, а во втором, вместо добавочного номера «1234», система наберет совсем другие цифры (это очень убедительный эксперимент, можно попробовать. Код приведен ниже).
NSString *telString =@"tel:+14089961010;ext=1234";
NSURL *urlString = [NSURL URLWithString: telString ];
[[UIApplication sharedApplication] openURL:urlString];
В сущности обе задачи (захват и верификация) решаются одними и теми же методами, отличие состоит лишь в применяемых регулярных выражениях.
Первым делом следует посмотреть, что же написано в регулирующем данный вопрос документе RFC 3966 . А написано там в сильно упрощенном виде следующее:
Упрощенная структура telephone-uri в соответствии с RFC 3966
telephone-uri = "tel:" telephone-subscriber
telephone-subscriber = global-number global-number = global-number-digits *par par = extension | isdn-subaddress | parameter isdn-subaddress = ";isub=" 1*uric extension = ";ext=" 1*phonedigit global-number-digits = "+" *phonedigit DIGIT *phonedigit parameter = ";" pname ["=" pvalue ] pname = 1*( alphanum | "-" ) pvalue = 1*paramchar paramchar = param-unreserved | unreserved | pct-encoded unreserved = alphanum | mark mark = "-" | "_" | "." | "!" | "~" | "*" "'" | "(" | ")" param-unreserved = "[" | "]" | "/" | ":" | "&" | "+" | "$" phonedigit = DIGIT | [ visual-separator ] visual-separator = "-" | "." | "(" | ")" alphanum = ALPHA | DIGIT reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," uric = reserved | unreserved
где ALPHA и DIGIT, как следует из другого документа, — RFC 2396 :
DIGIT = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ALPHA = lowalpha | upalpha lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
Схематично telephone-uri можно изобразить следующим образом:
telephone-uri = собственно телефонный номер [некое необязательное дополнение]
рис. 1
Из документа RFC 3966 следует, что:
-
собственно телефонный номер = global-number-digits
-
некое необязательное дополнение = extension | isdn-subaddress | parameter
где:
-
extension — добавочный телефонный номер
-
isdn-subaddress 	 — субадрес ISDN
-
parameter — некоторые другие необязательные параметры
Поскольку в реальной жизни при работе с мобильными приложениями, в частности с IOS приложениями, да и с большинством сайтов, встречается только добавочный телефонный номер, схема telephone-uri преобразится следующим образом:
telephone-uri = global-number-digits [extension]
рис. 2
или, подставив соответствующие значения для global-number-digits и extension:
telephone-uri = "+" *phonedigit DIGIT *phonedigit [";ext=" 1*phonedigit]
рис. 3
где phonedigit состоит из цифр "[0-9]" и ограниченного ассортимента визуальных разделителей
"-" | "." | "(" | ")"
Вот здесь, казалось бы, и можно переходить к рассмотрению реализующих эту схему регулярных выражений. но… Многие авторы, справедливо замечают, что не любой набор цифр есть телефонный номер. Существует общепринятая международная практика, определяющая структуру телефонных номеров. Да, действительно есть и мы сейчас ее вкратце рассмотрим.
Международная структура телефонных номеров
В соответствии с существующей практикой, можно выделить следующие группы, в совокупности составляющие телефонный номер global-number-digits:
global-number-digits
-
"+" — знак, указывающий, что после него расположен международный код страны
-
country_code — одно-трехзначный код страны, например: 7, или 44 или 374
-
area_code — трехзначный код региона, например: 800
-
exchange — трехзначный номер станции, например: 555
-
subscriber_number — четырехзначный номер абонента, например: 1234
extension
-
extension — добавочный номер, например: 5678 (необязательный элемент)
В этом случае структура телефонного номера будет выглядеть следующим образом:
telephonNumber = "+" country_code [visual-separator] area_code [visual-separator] exchange [visual-separator] subscriber_number [visual-separator] [";ext=" extension]
рис. 4
Таким образом, если реализовать регулярное выражение, учитывающее стандарт RFC 3966 и Международную структуру телефонных номеров, иными словами, соответствующее схемам, изображенным на рис. 3 и рис. 4, следующие записи телефонных номеров будут вполне валидными:
+14089961010;ext=1234 +1(408)996-1010;ext=1234 +1-408-996-10-10;ext=1234 +1.408.996.1010;ext=1234
а следующие будут валидными только исходя из стандарта RFC 3966, поскольку визуальные сепараторы (visual-separator) находятся вне структуры, определяемой Международными стандартами телефонных номеров:
+1(4089)96-1010;ext=1234 +1-408996-10-10;ext=1234 +1.408996.1010;ext=1234
В реальной жизни пользователь может набирать телефонный номер либо по заданному в приложении или на сайте шаблону, либо как ему покажется правильным (то есть, как заблагорассудится). А приложение, в свою очередь, будет обрабатывать полученную строку в соответствии с заложенными в него стандартами. Что не всегда совпадает.
В реальной жизни пользователь может набирать телефонный номер либо по заданному в приложении или на сайте шаблону, либо как ему покажется правильным (то есть, как заблагорассудится). А приложение, в свою очередь, будет обрабатывать полученную строку в соответствии с заложенными в него стандартами. Что не всегда совпадает.
Корпоративные WEB стандарты набора телефонного номера
Почему WEB? Потому, что в IOS, да и практически во всех других системах и, естественно, на сайтах, самый простой способ осуществить телефонный звонок — это использовать хорошо известрую html схему:
<a href="tel:1-408-996-1010">1-408-996-1010</a>
рис. 5
В начале статьи уже был приведен пример кода для реализации этой схемы на Objective C, однако, для стройности изложения, стоит повториться:
NSString *telString =@"tel:+14089961010";
NSURL *urlString = [NSURL URLWithString: telString ];
[[UIApplication sharedApplication] openURL:urlString];
рис. 6
Перейдем к корпоративным стандартам:
Что говорят по этому поводу специалисты компании Google?
Always supply the phone number using the international dialing format: the plus sign (+), country code, area code and number. While not absolutely necessary, it’s a good idea to separate each segment of the number with a hyphen (-) for easier reading and better auto-detection.
То есть необходимо ставить знак "+" перед кодом страны и «может быть хорошей идеей» разделять сегменты (группы) телефонного номера визуальными сепараторами в виде дефисов "-". Что в общем-то вполне согласуется с документом RFC 3966 и Международной структурой телефонных номеров, иными словами, соответствует схемам, изображенным на рис. 3 и рис. 4. Однако есть одно существенное НО. Визуальные сепараторы ограничены одним знаком, дефисом "-". Это конечно не означает, что браузер неадекватно отреагирует на скобки или точки в качестве визуальных сепараторов, однако это требует тщательной проверки, в документе Google гарантии даются только на дефис. Кроме того, в данном разделе инструкции ничего не говорится о формате добавочного номера. Поскольку статья посвящена в основном IOS приложениям, мы не будем углубляться в специфику работы гуголовского софта, интересующие могут поэкспериментировать и рассказать о результатах.
Компания Apple еще более немногословна, в разделе Phone Links , по поводу допустимого формата телефонного номера, мы находим такую фразу:
For more information about the tel URL scheme, see RFC 2806 and RFC 2396.
Формально все правильно, зачем повторяться, если есть международные стандарты? Но дело в том, что корпоративные стандарты компании Apple, также как и компании Google, соответствуют лишь части этих международных стандартов, которые, к слову, часто носят рекомендательный характер.
Корпоративные стандарты соответствуют лишь части международных стандартов, которые, к слову, часто носят рекомендательный характер.
Что можно и чего нельзя использовать в телефонном номере для IOS приложения
Эксперименты показали, система адекватно реагирует на все четыре вида визуальных сепараторов, регламентированных в документе RFC 3966, а именно:
-
visual-separator = "-" | "." | "(" | ")"
Причем, сепараторы могут быть расположены в произвольных местах и, более того, могут быть непарными. В качестве примера, следующие записи телефонных номеров будут обрабатываться кодом, приведенном на рис. 6, одинаково и вполне адекватно:
+14089961010 +1(408)996-1010 +1-408-996-10-10 +1.408.996.1010 +1(4089)96-1010 +1-408996-10-10 +1.408996.1010 +1-4(0899(6-10-10 +1.40)8996.10(10
По-другому дело обстоит с набором добавочного номера. Как уже говорилось в начале статьи, система неправильно реагирует на предлагаемый в документе RFC 3966 префикс добавочного кода ";ext=". Нативными, то есть естественными для системы являются два символа: ";" и ",".
В первом случае, когда система встречает в телефонном номере сепаратор ";", набор останавливается и на экране появляются цифры добавочного номера. При нажатии на них набор продолжается.
Во втором случае, когда система встречает в телефонном номере сепаратор ",", набор приостанавливается на этом месте и автоматически продолжается после небольшой паузы приблизительно в 2 секунды. Визуальный сепаратор "," может быть не единичный, например ",,,,". В этом случае продолжительность паузы увеличивается пропорционально количеству знаков.
В случае появления сепаратора вида ";ext=", регламентируемого документом RFC 3966, происходит следующее: система принимает знак ";" за точку останова перед набором добавочного номера, а знаки «ext» интерпретирует как цифры с которых начинается добавочный номер.
Итак, мы рассмотрели теоретические и практические предпосылки и можем перейти ко второй части статьи, то есть собственно к регулярным выражениям для захвата и верификации телефонных номеров.
Захват и верификация телефонных номеров с помощью регулярных выражений, для IOS и не только… Часть 2 — coming soon.
Автор: MSLibrary