Форматирование номеров телефона с помощью регулярных выражений

Такая элементарная вещь, как номер телефона, в письменных текстах живёт во множестве вариантов. Обыкновенный номер типа +7 (123) 123-45-67 можно встретить записанным без скобок или дефисов (+7 123 1234567), а то и вообще без пробелов (+71231234567). Не собираюсь оскорблять чувства пишущих, им и так непросто. Но уважающий себя веб-ресурс не может допустить такой типографической разношёрстности. Плюс к тому, необлагороженные номера неудобно читать человеку (то самое human-readable).

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

В качестве эталонной будет принята запись номера вида +7 (123) 123-45-67.

Найти все телефонные номера на странице можно таким способом:

  let pattern = new RegExp('(\+7|8)[\s(]*\d{3}[)\s]*\d{3}[\s-]?\d{2}[\s-]?\d{2}', 'g');    // создать регулярное выражение
  let phoneNumbers = document.body.innerText.match(pattern);    // применить на всём тексте документа

Метод match() вернёт и запишет в переменную phoneNumbers массив (объект типа Array) со всеми найденными на странице номерами.

Дисклеймер. Виденное выше регулярное выражение может вызвать сильное чувство боли в глазах у неподготовленного читателя. Если вы не очень знакомы с регулярками, автор всё-таки советует для начала проработать несколько первых глав учебника на LearnJavascript.

Разберём это выражение:

(\+7|8) +7 (123) 123-45-67

На практике встречается начало и с 8, и с +7.

Напомню: | означает или. То есть, на данной позиции паттерна может стоять любой из вариантов, разделённых |. Также важно не забыть поставить скобки :))
[\s(]*\d{3}[)\s]* +7 (123) 123-45-67
[\s(]* +7 (123) 123-45-67

То есть, фактически, эти части паттерна распознают первые три цифры после кода страны, причём как взятые в скобки, так и без (квантификатор * синонимичен {0,} и означает «либо ноль, либо сколько угодно»).

Также напомню: \d stands for символьная группа всех цифр (всё равно что [0-9]), \s — группа всех пробельных символов.
\d{3}[\s-]?\d{2}[\s-]?\d{2} +7 (123) 123-45-67
[\s-]? +7 (123) 123-45-67

Данная часть паттерна распознаёт последние 7 цифр номера. Они могут быть разделены дефисами, пробелами или вообще ничем.
Напоминалка: квантификатор ? синонимичен {0,1} и означает «либо одно вхождение, либо ничего»).

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

Ну хорошо, найти все номера получилось. К сожалению, задача этим не исчерпывается. Как же взять и заменить все вхождения разом?

Здесь поможет метод replace(). Если у регулярного выражения указан флаг 'g' (второй аргумент конструктора), то replace() заменит все вхождения на то, что указано. Например, задача замены всех повторяющихся пробелов в тексте на одинарные решается таким простым способом:

  text = text.replace(/\s{1,}/g, ' ');

На всякий: /\s{1,}/g — это краткая запись для new RegExp('\s+', 'g'). Также вместо {1,} можно писать просто +.

Но ведь у нас задача сложнее: нельзя же заменить разные номера телефонов на один! Тут самое время вспомнить о скобочных группах.

Скобочная группа — термин из теории регулярных выражений. Технически это просто часть паттерна, взятая в скобки. Но к любой такой группе можно обратиться по индексу.

Получается,

К счастью, replace() поддерживает работу с группами. Достаточно написать в шаблоне замены (второй параметр) ${номер_группы} ($1 или $5), и функция заменит эту конструкцию на содержимое скобочной группы.

То была идея, а теперь реализация:

  let pattern = /(\+7|8)[\s(]?(\d{3})[\s)]?(\d{3})[\s-]?(\d{2})[\s-]?(\d{2})/g;    // паттерн с проставленными скобками
  let phoneNumbers = document.body.innerText.match(pattern);    // найдём все номера
  let correctNumber = phoneNumbers[0].replace(pattern, '+7 ($2) $3-$4-$5');    // пробуем замену
  console.log(correctNumber);

В результате будет выведен аккуратный, каноничный телефонный номер. Работает!

Теперь, чтобы заменить все найденные номера телефонов на единообразные, добавим флаг g:

  let pattern = /(\+7|8)[\s(]?(\d{3})[\s)]?(\d{3})[\s-]?(\d{2})[\s-]?(\d{2})/g;
  document.body.innerHTML = document.body.innerHTML.replace(pattern, '+7 ($2) $3-$4-$5');

Усложним шаблон функции replace(), чтобы номер телефона был кликабельным (ссылка с префиксом tel:):

  let pattern = /(\+7|8)[\s(]?(\d{3})[\s)]?(\d{3})[\s-]?(\d{2})[\s-]?(\d{2})/g;
  document.body.innerHTML = document.body.innerHTML.replace(pattern, '<a href="tel:+7$2$3$4$5">+7 ($2) $3-$4-$5</a>');

То есть, для корректной работы ссылки нужно в её коде выписать все цифры номера подряд — как раз самый неудобный для чтения человеком вариант.