Pull to refresh

JavaScript: изучаем регулярные выражения на практике

Reading time18 min
Views68K

image


Регулярное выражение (далее также — регулярка) — это последовательность специальных символов, формирующих паттерн или шаблон (pattern), который сопоставляется со строкой.


Цель такого сопоставления может состоять либо в поиске подстроки в строке, например, для замены подстроки, либо в определении соответствия строки шаблону для валидации строки.


В данной статье мы сосредоточимся на валидации.


Что конкретно мы будем делать? Мы возьмем несколько регулярок из validator.js (наиболее популярной библиотеки для валидации данных с помощью регулярных выражений) и произведем их подробный разбор. Также мы рассмотрим несколько дополнительных регулярок и один алгоритм.


Как результат, мы реализуем несколько полезных функций, которые вы впоследствии сможете использовать в своих проектах.


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


Еще парочка полезных ссылок:



Для того, чтобы немного размять мозги, начнем с двух "валидаторов", в которых регулярные выражения не используются.


Является ли строка пустой?


Функция:


const isEmpty = (str) => str.trim().length === 0

console.log(isEmpty('not empty')) // false
console.log(isEmpty('   ')) // true

Метод trim() удаляет пробелы в начале и конце строки. Свойство length содержит количество символов, из которых состоит строка. Если строка не содержит символов, значит, ее свойство length имеет значение 0. В этом случае выражение str.length === 0 возвращает true. В противном случае, возвращается false.


Данную функцию можно переписать так:


const isEmpty = (str) => !str.trim().length
// `0` - ложное значение, `1` и больше - истинное значение,
// `!` - логическое НЕ, выполняет преобразование значения в логическое и меняет его на противоположное
// получается `!false`, или `true`

// или так
const _isEmpty = (str) => str.trim() === ''

Является ли значение "логическим"?


Функция:


const isBoolean = (str) => ['true', 'false', '1', '0'].indexOf(str) >= 0

console.log(isBoolean('true')) // true
console.log(isBoolean(false)) // false (см. ниже)

Метод indexOf() возвращает индекс элемента или -1 при отсутствии элемента в массиве. Если элемент в массиве есть, его индекс будет равным 0 или больше (в пределах длины массива — 1). В этом случае выражение arr.indexOf(str) >= 0 возвращает true. Иначе, возвращается false.


Обратите внимание: в данном случае логическими считаются не только значения true и false, но также 1 (обозначающее истину) и 0 (обозначающее ложь). Также обратите внимание, что функция принимает только строку (как и функции, которые будут рассматриваться в дальнейшем), поэтому второй вызов функции isBoolean() с логическим значением false возвращает false.


Данную функцию можно переписать так:


const bools = ['true', 'false', '1', '0']

const isBoolean = (str) => bools.indexOf(str) !== -1

// или так
const _isBoolean = (str) => bools.indexOf(str) > -1

Размялись? Отлично. Переходим к регуляркам.


Состоит ли строка только из букв?


Функция:


// для латиницы
const isAlpha = /^[A-Z]+$/i.test(str)

// для кириллицы
const _isAlpha = /^[А-ЯЁ]+$/i.test(str)

// для латиницы и криллицы
const __isAlpha = /^[A-ZА-ЯЁ]+$/i.test(str)

Посимвольный разбор:


  • ^ и $ — символы начала и конца строки, соответственно (границы)
  • [] — набор символов, перечисление (символ строки должен совпадать хотя бы с одним из вариантов)
  • A-Z или А-ЯЁ — диапазон букв (например, А-ЯЁ — это все буквы киррилицы от А до Я + буква Ё, которая стоит особняком)

Обратите внимание: A-Z — это не тоже самое, что символьный класс \w. \w означает любая буква латиницы ИЛИ любая цифра ИЛИ символ нижнего подчеркивания.


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

Читаем регулярку: строка ДОЛЖНА состоять хотя бы из одной (одной или более) буквы, независимо от регистра.


Состоит ли строка только из букв и/или целых чисел?


Функция:


// для латиницы
const isAlphaNumeric = (str) => /^[0-9A-Z]+$/i.test(str)

// для кириллицы
const _isAlphaNumeric = (str) => /^[0-9А-ЯЁ]+$/i.test(str)

// для латиницы и кириллицы
const __isAlphaNumeric = (str) => /^[0-9A-ZА-ЯЁ]+$/i.test(str)

Разбор: такие же спецсимволы, что и в предыдущем примере +


  • 0-9 — диапазон цифр от 0 до 9; данный диапазон можно заменить символьным классом \d, означающим любое число, для латиницы получится /^[\dA-Z]+$/i

Обратите внимание: здесь мы также не можем использовать \w из-за нижнего подчеркивания. Впрочем, мы можем исключить его из проверки, написав что-то вроде /^[^\W_]$/i, где [^] означает любой символ, кроме указанных в наборе (одним из таких символов является _), \W — НЕ буква латиницы, НЕ число и НЕ нижнее подчеркивание. Таким образом, [^\W_] означает любой символ, который НЕ является нижним подчеркиванием, а также не относится к буквам латиницы ИЛИ цифрам. Или в переводе на человеческий язык — любой символ, который является буквой латиницы или числом.


Читаем регулярку: строка ДОЛЖНА состоять хотя бы из одной буквы И/ИЛИ (арабского — в дальнейшем предполагается) числа, без учета регистра


Является ли значение почтовым индексом?


Функция:


const isPostalCode = (str) => /^\d{6}$/.test(str)

Разбор:


  • {6} — точно 6 вхождений предшествующего символа, т.е. символ перед { должен повторяться 6 раз, поскольку почтовый индекс (для России) состоит из 6 цифр (квантификатор)

Обратите внимание: в большинстве случаев при валидации предполагается, что строка прошла предварительную очистку от пробелов и других символов, которых в ней быть не должно (см. ниже).


Читаем: значение ДОЛЖНО состоять (точно — в дальнейшем предполагается) из 6 цифр.


Является ли значение номером паспорта?


Функция:


// оригинал из `validator.js`
const passportNumber = /^\d{2}\d{2}\d{6}$/
// можно упростить
const _passportNumber = /^\d{10}$/

const removeSpaces = (str) => str.replace(/\s+/g, '')

const isPassportNumber = (str) => _passportNumber.test(removeSpaces(str))

Разбор:


  • в самом регулярном выражении нет ничего нового — 10 цифр (или 2 цифры + 2 цифры + 6 цифр — в оригинале)
  • removeSpaces() — утилита для удаления из строки пробелов, поскольку номер паспорта может выглядеть как 12 34 567890, 1234 567890 и т.д.
  • \s означает пробельный символ: пробел, табуляция, перенос строки и т.д. (символьный класс)
  • g — глобальный поиск, т.е. будут обнаружены все пробелы, имеющиеся в строке, а не только первый (флаг)

Читаем: значение ДОЛЖНО состоять из 10 цифр.


Является ли значение числом (целым или с плавающей точкой/запятой)?


Функция:


const numeric = (delimiter = '.') =>
  new RegExp(`^[+-]?([0-9]*\\${delimiter})?[0-9]+$`)

const isNumeric = (str, delimiter) => numeric(delimiter).test(str)

Разбор:


  • numeric — функция, возвращающая регулярное выражение с указанным разделителем (delimiter), которым по умолчанию является символ .; это один из тех немногих случаев, когда для создания регулярки используется объект RegExp, позволяющий создавать регулярное выражение динамически, в отличие от //, создающего статический шаблон

Обратите внимание: символ . необходимо экранировать с помощью \ или, в данном случае, с помощью \\ (чтобы не экранировать $). Экранирование превращает . в обычную точку, в противном случае, этот символ будет иметь специальное значение — любой символ. Мы также экранируем любой другой символ, переданный в функцию isNumeric() в качестве разделителя (побочный эффект), но это не страшно.


  • ? — НОЛЬ или ОДИН предшествующий символ (квантификатор); по сути, применение этого квантификатора делает предшествующий символ опциональным (необязательным)


  • * — НОЛЬ или БОЛЕЕ предшествующих символов (квантификатор); этот квантификатор также делает предшествующий символ необязательным


  • [+-]? — необязательный + или -


  • ([0-9]*\.)? — необязательная группа, которая МОЖЕТ состоять из числа и точки ИЛИ только из точки (или другого разделителя)


  • [0-9]+ — хотя бы одна цифра



Читаем: значение ДОЛЖНО состоять хотя бы из одной цифры и МОЖЕТ включать знак + или - в начале строки, за которым МОЖЕТ следовать любое количество цифр и разделитель ИЛИ только разделитель


Является ли строка цветом в шестнадцатиричном формате?


Функция:


const hexColor = /^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$/i

const isHexColor = (str) => hexColor.test(str)

Разбор:


  • длина регулярки может испугать, но это всего лишь повторяющийся шаблон
  • #? — необязательный символ #
  • [0-9A-F]{n}n любых чисел ИЛИ букв латиницы от A до F (без учета регистра)
  • | — альтерация, ИЛИ (строка должна совпадать с одним из вариантов — наборов)

Читаем: строка ДОЛЖНА состоять ЛИБО из 3, ЛИБО из 4, ЛИБО из 6, ЛИБО из 8 букв латиницы от A до F (без учета регистра) И/ИЛИ цифр и МОЖЕТ включать символ # в начале


Является ли строка цветом в формате RGB или RGBA?


// RGB без альфа-канала - прозрачности
const rgbColorRegexp =
  /^rgb\((([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]),){2}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\)$/

// RGBA с прозрасностью
const rgbaColorRegexp =
  /^rgba\((([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]),){3}(0?\.\d|1(\.0)?|0(\.0)?)\)$/

// RGB в процентах
const rgbColorPercentRegexp =
  /^rgb\((([0-9]%|[1-9][0-9]%|100%),){2}([0-9]%|[1-9][0-9]%|100%)\)/

// RGBA в процентах
const rgbaColorPercentRegexp =
  /^rgba\((([0-9]%|[1-9][0-9]%|100%),){3}(0?\.\d|1(\.0)?|0(\.0)?)\)/

// строка должна совпадать хотя бы с одним вариантом
const isRgbColor = (str) =>
  rgbColorRegexp.test(str) ||
  rgbaColorRegexp.test(str) ||
  rgbColorPercentRegexp.test(str) ||
  rgbaColorPercentRegexp.test(str)

Двигаемся дальше.


Является ли значение номером сотового телефона?


Функция (для России):


const mobilePhoneRegexp = /^(\+?7|8)?9\d{9}$/

const removeNonDigits = (str) => replace(/\D+/g, '')

const isMobilePhone = (str) => mobilePhoneRegexp.test(removeNonDigits(str))

Разбор:


  • \+? — опциональный символ +; обратите внимание на экранирование
  • (\+?7|8)? — опциональная группа, которая МОЖЕТ состоять из символа + и числа 7 или 8 ИЛИ только из числа 7 или 8
  • removeNonDigits() — утилита для удаления всех символов, которые не являются числом (\D)

Читаем: строка ДОЛЖНА состоять из 10 цифр, первым из которых ДОЛЖНО БЫТЬ число 9, и МОЖЕТ включать символ + и число 7 или 8 ИЛИ только число 7 или 8 в начале


Лирическое отступление


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


// Определяем, является ли строка названием изображения в формате PNG, JPG, JPEG или SVG с помощью опережающей проверки
// Строка ДОЛЖНА состоять из одного и более (любого) символа, включать точку и заканчиваться на `png`, `jpg`, `jpeg` или `svg` (без учета регистра)
const isImage = (str) => /.+\.(?=png|jpe?g|gif|svg)/i.test(str)

// Определяем, что строка НЕ является названием изображения в формате WebP или AVIF с помощью негативной опережающей проверки
// ПОСЛЕ строки, состоящей из одного или более символа, НЕ ДОЛЖНО быть слова `webp` или `avif` (без учета регистра)
const isNotWebpOrAvif = (str) => /.+\.(?!webp|avif)/i.test(str)

// Что касается ретроспективной проверки, то я не смог придумать достойного примера - такого, который бы имел какие-то преимущества по сравнению с использованием метода `startsWith()`

// Определяем, что значение является долларами США с помощью ретроспективной проверки
// ПЕРЕД хотя бы одной цифрой ДОЛЖЕН быть символ `$`
const isUSD = (str) => /(?<=\$)\d+/.test(str)

// Определяем, что значение НЕ является евро с помощью негативной ретроспективной проверки
// ПЕРЕД хотя бы одной цифрой НЕ ДОЛЖНО быть символа `€`
const isNotEUR = (str) => /(?<!€)\d+/.test(str)

// Определяем, является ли строка эмодзи (или эмоджи)
// Флаг `u` и класс `\p{...}` - тема для отдельного разговора, however...
const isEmoji = (str) => /\p{So}/u.test(str)

// Универсальная функция для поиска символов на любом языке
const languageAgnostic = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/u

const isLanguageAgnosticSymbol = (str) => languageAgnostic.test(str)

Еще парочка полезных утилит


// Определяем, является ли строка HTML-тегом
// [^>] - означает любой символ, кроме `>`
// ([^>]+) - один или более таких символов
const isHTMLTag = (str) => /(^<([^>]+)>$)/i.test(str)

const tag =
  '<p id="myID" class="my-class" data-type="my_type" style="color: green;">'

console.log(isHTMLTag(tag)) // true

// Находим слова в одинарных или двойных (парных) кавычках
// обратите внимание: в данном случае мы не учитываем, что кавычки могут быть экранированы
const quotes = /(["'])([^"'])*\1/g
const getQuottedWords = (str) => str.match(quotes)

const str = `some string with "double quotes" and 'single quotes'`

console.log(getQuottedWords(str).join('\n'))
/*
  "double quotes"
  'single quotes'
*/

// или так (короче, но также без учета экранирования)
const _quotes = /(["']).*\1/g

// или так (с учетом экранирования)
const __quotes = /(["'])([^"'\\])*\1/g
const _getQuottedWords = (str) => str.match(__quotes)

const str = "some string with \"escaped double quotes\""

console.log(..._getQuottedWords(str)) // "escaped double quotes"

// Определяем, является ли строка временем в формате ЧЧ:ММ:СС
// Строка ДОЛЖНА начинаться с 1 с ведущим 0 или без него и любым числом (01-19) ИЛИ
// 2 и любым числом в диапазане от 0 до 3 (20-23) - часы,
// затем ДОЛЖНЫ следовать две группы, состоящие из двоеточия и любого двухзначного числа,
// первая цифра которого ДОЛЖНА находиться в диапазоне от 0 до 5 (01-59) - часы и минуты
const isTime = (str) => /([01]\d|2[0-3])(:[0-5]\d){2}$/.test(str)

console.log(isTime('12:00')) // false
console.log(isTime('12:00:00')) // true

Функция для удаления из строки ВСЕХ пробелов


const str = '  some   string  with   crazy    spaces '

const formatted = str
  .replace(/\s{2,}/g, ' ')
  .trim()
  .replace('crazy', 'normal')

console.log(formatted) // 'some string with normal spaces'

Функция для форматирования даты


// В таком формате возвращается дата из `<input type="date">`
const str = '2021-06-29'
// допустим, что мы хотим преобразовать ее в привычный для нас формат - `ДД.ММ.ГГГГ`

// С помощью обычных скобочных групп
// определяем скобочные группы 1, 2 и 3
const regexp = /(\d{4})-(\d{2})-(\d{2})/

// производим замену
const formatted = str.replace(regexp, '$3.$2.$1')
console.log(formatted) // 29.06.2021

// С помощью именованных групп захвата
// определяем группы `year`, `month` и `day`,
// чтобы не запоминать (считать) номера групп (количество скобок)
const _regexp = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/

// производим замену
const _formatted = str.replace(_regexp, '$<day>.$<month>.$<year>')
console.log(_formatted) // 29.06.2021

// С помощью функции-заменителя
// это не имеет особого смысла, просто для примера
const regexp = /(\d{4})-(\d{2})-(\d{2})/

const result = str.replace(
  regexp,
  // первый аргумент, который нам не нужен - это объект совпадения `match`
  (_, year, month, day) => `${day}.${month}.${year}`
)
console.log(result) // 29.06.2021

Теперь поговорим о строках, к содержанию которых предъявляются особые требования. Под такими строками я подразумеваю URL (или, если угодно, URI) и email. В принципе, сюда же можно отнести пароли, требования к которым предъявляются не нормативными документами (RFC), а клиентами/заказчиками/разработчиками.


URL


URL (Uniform Resource Locator — унифицированный указатель ресурса) — система унифицированных адресов электронных ресурсов, или единообразный определитель местонахождения ресурса.


Он состоит из следующих частей:



Требования, предъявляемые к "урлам", содержатся, в основном, в RFC 3986. Согласно этому документу URL может содержать следующие символы:


ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=

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


Также в названном документе приводится регулярка для разбора урлов (приложение B), которая выглядит так (числа — это номера групп):


^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
 12            3  4          5       6  7        8 9

Попробуем ее применить:


const url = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
const str = 'https://example.com/main?search#hash'
const result = str.match(url)
console.log(result)
/*
  [
    0: "https://example.com/main?search#hash"
    1: "https:"
    2: "https"
    3: "//example.com"
    4: "example.com"
    5: "/main"
    6: "?search"
    7: "search"
    8: "#hash"
    9: "hash"
    // ...
  ]
*/

Данная регулярка отлично подходит для простых URL (как в приведенном примере), но в более сложных случаях лучше использовать конструктор URL():


const str = 'https://user:pwd@example.com:80/main?query#anchor'
const result = new URL(str)
console.log(result)
/*
  {
    hash: "#anchor"
    host: "example.com:80"
    hostname: "example.com"
    href: "https://user:pwd@example.com:80/main?query#anchor"
    origin: "https://example.com:80"
    password: "pwd"
    pathname: "/main"
    port: "80"
    protocol: "https:"
    search: "?query"
  }
*/

Вот один из возможных вариантов регулярки для проверки URL с учетом протокола http(s), а также без учета некоторых символов:


/
  https?:\/\/               - `http` с необязательным `s`
  (www\.)?                  - опциональные `www` и `.`
  [-\w@:%\.\+~#=]{1,256}    - любые символы из набора в количестве 1-256
  \.                        - точка
  [a-z0-9()]{2,}            - домен размером от 2 символов
  \b                        - граница строки
  ([-\w()@:%\.\+~#=//?&]*)  - поддомен, строка запроса, якорь и т.д.
/i

Тестируем:


const url =
  /https?:\/\/(www\.)?[-\w@:%\.\+~#=]{1,256}\.[a-z0-9()]{1,6}\b([-\w()@:%\.\+~#=//?&]*)/i

const urls = [
  'http://www.example.ru',
  'https://www.example.ru',
  'http://example.ru',
  'http://www.example.ru/main',
  'htt://example.ru',
  'www.example.ru',
  'www.mp3.com',
  'www.e.org',
  'http://e.org',
  'http://www.e.org',
  'https://www.e.org',
  'www.ab.com',
  'http://ab.com',
  'http://www.ab.com',
  'https://www.ab.com',
  'www.example',
  'www.example-.ru',
  'www.-example.ru',
  'example.ru',
  'http://www.example',
  'http://example',
  'www.mp3#.com'
]

urls.forEach((u) => {
  if (url.test(u)) {
    console.log(`%cCorrect: ${u}`, 'color: green;')
  } else {
    console.info(`%cWrong: ${u}`, 'color: red;')
  }
})
/*
  Correct: http://www.example.ru
  Correct: https://www.example.ru
  Correct: http://example.ru
  Correct: http://www.example.ru/main
  Wrong: htt://example.ru
  Wrong: www.example.ru
  Wrong: www.mp3.com
  Wrong: www.e.org
  Correct: http://e.org
  Correct: http://www.e.org
  Correct: https://www.e.org
  Wrong: www.ab.com
  Correct: http://ab.com
  Correct: http://www.ab.com
  Correct: https://www.ab.com
  Wrong: www.example
  Wrong: www.example-.ru
  Wrong: www.-example.ru
  Wrong: example.ru
  Wrong: http://www.example
  Wrong: http://example
  Wrong: www.mp3#.com
*/

Email


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


Адрес электронной почты (email) состоит из из следующих частей:



Что касается символов, которые могут использоваться в адресе электронной почты, то здесь ситуация довольно неоднозначная, поскольку существует большое количество RFC, по-разному регламентирующих этот вопрос. Относительно полный список этих RFC можно найти здесь.


В простейшем случае, регулярное выражение для определения того, является ли значение email, может выглядеть так:


// (хотя бы один (любой) символ)@(хотя бы один символ).(хотя бы один символ)
const isEmail = (str) => /^(.+)@(.+)\.(.+)$/.test(str)

Или можно обойтись вообще без регулярки:


// преобразуем строку в массив
const isEmail = ([...str]) =>
  // проверяем, что в массиве есть символ `@`, он находится, как минимум, на второй позиции
  str.indexOf('@') > 0 &&
  // и является единственным
  str.indexOf('@') === str.lastIndexOf('@') &&
  // проверяем, что в массиве есть точка,
  str.indexOf('.') > 0 &&
  // она стоит после символа `@`
  str.indexOf('@') < str.indexOf('.') &&
  // и не является последним символом
  str.indexOf('.') < str.length - 1

Однако, любой символ — это как any в TypeScript — обуславливает ненадежность регулярного выражения. Но, с повышением точности регулярки, ее сложность растет в геометрической прогрессии.


Вот гораздо более продвинутый (надежный) пример:


/^
  (
    (
      [^<>()[\]\\.,;:\s@"]+   - один или более символ, кроме указанных
      (\. - точка
        [^<>()[\]\\.,;:\s@"]+ - один или более символ, кроме указанных
      )
      *                       - ноль или более
    )
    |                         - ИЛИ
    (".+")                    - один или более символ в двойных кавычках
  )
    @                         - символ `@`
  (
    (
      \[                      - открывающая квадратная скобка
        [0-9]{1,3}            - от 1 до 3 цифр
        \.                    - точка
        [0-9]{1,3}
        \.
        [0-9]{1,3}
        \.
        [0-9]{1,3}
      \]                      - закрывающая квадратная скобка
    )
    | - ИЛИ
    (
      (
        [-a-z0-9]+            - одна или более буква латиницы, цифра или символ `-`
        \.                    - точка
      )+                      - один или более
      [a-z]{2,}               - хотя бы `2` буквы латиницы
    )
  )
$/i                           - без учета регистра

Функция:


const email =
  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-z\-0-9]+\.)+[a-z]{2,}))$/i

const isEmail = (str) => email.test(str)

// тестируем
// вместо `@`, написали `_`
console.log(isEmail('my_mail.com')) // false
// пропустили `.`
console.log(isEmail('my@mailcom')) // false
// пропустили начало
console.log(isEmail('@mail.com')) // false
// ок
console.log(isEmail('my@mail.com')) // true

Пароль


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


  • определенная длина или диапазон пароля, т.е. ограничение минимального ИЛИ минимального и максимального количества символов
  • минимум одна большая буква
  • минимум одна маленькая буква
  • минимум одна цифра
  • минимум один спецсимвол

Эти требования можно комбинировать. Переведем их на язык регулярных выражений:


{8,}                        // от 8 символов
{8,20}                      // от 8 до 20 символов
(?=.*\d)                    // минимум одна цифра
(?=.*[a-z])                 // минимум одна буква в нижнем регистре
(?=.*[A-Z])                 // минимум одна буква в верхнем регистре
(?=.*[-#!$@%^&*_+~=:;?\/])  // минимум один символ из набора

Функция:


const password =
  /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[-#!$@%^&*_+~=:;?\/])[-\w#!$@%^&*+~=:;?\/]{8,}$/

const isPassword = (str) => password.test(str)

// буквы в верхнем и нижнем регистре
console.log(isPassword('Password')) // false
// + число
console.log(isPassword('Passw0rd')) // false
// + спецсимвол, но недостаточная длина
console.log(isPassword('Pas_w0r')) // false
// ok
console.log(isPassword('Pas$_W0rd')) // true

Не забывайте модифицировать [-\w#!$@%^&*+~=:;?\/] при добавлении/удалении групп.


Номер карты


Напоследок, рассмотрим один интересный алгоритм — алгоритм Луна, который используется для вычисления контрольной цифры номера пластиковой карты в соответствии со стандартом ISO/IEC 7812 с целью ее (номера) валидации.


В упрощенном виде этот алгорит включает следующие шаги:


  1. Цифры проверяемой последовательности нумеруются справа налево.
  2. Цифры, оказавшиеся на нечетных местах, остаются без изменений.
  3. Цифры, стоящие на четных местах, умножаются на 2.

Обратите внимание: речь идет не о "четности" числа, а о четности его позиции в строке.


  1. Если в результате такого умножения возникает число больше 9, оно заменяется суммой цифр получившегося произведения — однозначным числом, то есть цифрой.
  2. Все полученные в результате преобразования цифры складываются. Если сумма кратна 10, то исходные данные верны.

Реализация алгоритма из Википедии:


function luhnAlgorithm(value) {
  value = value.replace(/\D/g, '')

  // контрольная сумма
  let nCheck = 0
  // индикатор "четности" позиции числа
  let bEven = false

  // перебираем числа в обратном порядке
  for (let n = value.length - 1; n >= 0; n--) {
    // извлекаем число из строки
    // позиция первого извлеченного числа является нечетной
    let nDigit = parseInt(value.charAt(n), 10)

    // если позиция числа четная и при умножении на 2 число становится больше 9,
    // вычитаем из числа 9
    // результат будет таким же, как при сложении цифр, из которых состоит число
    // например, `6 * 2 = 12`, `1 + 2 = 3` и `12 - 9 = 3`
    if (bEven && (nDigit *= 2) > 9) {
      nDigit -= 9
    }

    // прибавляем число к сумме
    nCheck += nDigit
    // инвертируем индикатор
    bEven = !bEven
  }

  // если контрольная сумма делится на 10 без остатка,
  // значит, номер карты является валидным
  return nCheck % 10 == 0
}

validator.js предлагает такой вариант рассматриваемого алгоритма:


const creditCard =
  /^(?:4[0-9]{12}(?:[0-9]{3,6})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12,15}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11}|6[27][0-9]{14})$/

function isCreditCard(str) {
  const sanitized = str.replace(/[- ]+/g, '')
  if (!creditCard.test(sanitized)) {
    return false
  }
  let sum = 0
  let digit
  let tmpNum
  let shouldDouble
  for (let i = sanitized.length - 1; i >= 0; i--) {
    digit = sanitized.substring(i, i + 1)
    tmpNum = parseInt(digit, 10)
    if (shouldDouble) {
      tmpNum *= 2
      if (tmpNum >= 10) {
        sum += (tmpNum % 10) + 1
      } else {
        sum += tmpNum
      }
    } else {
      sum += tmpNum
    }
    shouldDouble = !shouldDouble
  }
  return !!(sum % 10 === 0 ? sanitized : false)
}

Попробуем реализовать этот алгоритм в одну строку:


// функция принимает строку
const isCreditCard = (str) =>
  // строка не должна быть пустой
  // можно воспользоваться нашей функцией `!isEmpty()` или
  str.trim().length !== 0 &&
  str
    // удаляем из строки все НЕ числа
    .replace(/\D/g, '')
    // преобразуем строку в массив
    .split('')
    // меняем порядок следования элементов массива на противоположный
    .reverse()
    // s - контрольная сумма
    // c - текущий символ строки, начиная с последнего
    // i - индекс элемента
    .reduce(
      (s, c, i) =>
        (s +=
          // если индекс элемента - нечетное число (1, 3 и т.д.),
          // значит, число находится на четной позиции, умножаем его на 2
          // если при этом число становится больше 9, вычитаем из него 9
          // преобразуем результат в число с помощью `+`
          +(i % 2 !== 0 && (c *= 2) > 9 ? (c -= 9) : c)),
      0) // начальная контрольная сумма
    // если контрольная сумма делится на 10 без остатка,
    // значит, номер карты является валидным
    % 10 === 0

console.log(isCreditCard('1234 5678 9009 8765')) // false
console.log(isCreditCard('5555 5555 5555 4444')) // true

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


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


Подборка функций для валидации, реализацией которых мы занимались на протяжении статьи:


const isEmpty = (str) => !str.trim().length

const isBoolean = (str) => ['true', 'false', '1', '0'].indexOf(str) > -1

const isAlpha = /^[A-ZА-ЯЁ]+$/i.test(str)

const isAlphaNumeric = (str) => /^[0-9A-ZА-ЯЁ]+$/i.test(str)

const numeric = (delimiter = '.') =>
  new RegExp(`^[+-]?([0-9]*\\${delimiter})?[0-9]+$`)
const isNumeric = (str, delimiter) => numeric(delimiter).test(str)

const isPostalCode = (str) => /^\d{6}$/.test(str)

const isPassportNumber = (str) => /^\d{10}$/.test(str)

const isMobilePhone = (str) => /^(\+?7|8)?9\d{9}$/.test(str)

const isURL = (str) => /https?:\/\/(www\.)?[-\w@:%\.\+~#=]{1,256}\.[a-z0-9()]{1,6}\b([-\w()@:%\.\+~#=//?&]*)/i.test(str)

const isEmail = (str) => /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-z\-0-9]+\.)+[a-z]{2,}))$/i.test(str)

const isStrongPassword = (str) => /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[-#!$@%^&*_+~=:;?\/])[-\w#!$@%^&*+~=:;?\/]{8,}$/.test(str)

const isCreditCard = (str) =>
  str.trim().length !== 0 &&
  str
    .replace(/\D/g, '')
    .split('')
    .reverse()
    .reduce((s, c, i) =>
      (s += +(i % 2 !== 0 && (c *= 2) > 9 ? (c -= 9) : c)), 0) % 10 === 0

Это все, о чем я хотел вам сегодня рассказать. Приветствуются любые замечания, дополнения и предложения.




Купить VPS-хостинг с быстрыми NVMе-дисками и посуточной оплатой у хостинга Маклауд.


Tags:
Hubs:
Total votes 25: ↑19 and ↓6+19
Comments7

Articles