Перейти к основному содержимому

RegEx для сопоставления шаблона даты в Java

· 5 мин. чтения

1. Введение

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

В этой статье мы будем использовать пакет java.util.regex , чтобы определить, содержит ли данная строка допустимую дату или нет.

Введение в регулярные выражения см. в нашем Руководстве по API регулярных выражений Java .

2. Обзор формата даты

Мы собираемся определить действительную дату по международному григорианскому календарю. Наш формат будет следовать общему шаблону: ГГГГ-ММ-ДД.

Давайте также включим понятие високосного года, то есть года, содержащего день 29 февраля. Согласно григорианскому календарю, мы будем называть год високосным , если число года можно разделить без остатка на 4 , за исключением тех, которые делятся на 100 , но включая те, которые делятся на 400 .

Во всех остальных случаях мы будем называть год регулярным .

Примеры допустимых дат:

  • 2017-12-31
  • 2020-02-29
  • 2400-02-29

Примеры недопустимых дат:

  • 2017/12/31 : неправильный разделитель токенов
  • 2018-1-1 : отсутствуют ведущие нули
  • 2018-04-31 : неверные дни считаются в апреле
  • 2100-02-29 : этот год не високосный, так как значение делится на 100 , поэтому февраль ограничен 28 днями.

3. Внедрение решения

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

public interface DateMatcher {
boolean matches(String date);
}

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

3.1. Соответствие широкому формату

Мы начнем с создания очень простого прототипа, обрабатывающего ограничения формата нашего сопоставления:

class FormattedDateMatcher implements DateMatcher {

private static Pattern DATE_PATTERN = Pattern.compile(
"^\\d{4}-\\d{2}-\\d{2}$");

@Override
public boolean matches(String date) {
return DATE_PATTERN.matcher(date).matches();
}
}

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

Даты матчей: 2017-12-31 , 2018-01-31 , 0000-00-00 , 1029-99-72

Несовпадающие даты: 2018-01 , 2018-01-XX , 2020/02/29

3.2. Соответствие определенному формату даты

Наш второй пример принимает диапазоны токенов даты, а также наше ограничение форматирования. Для простоты мы ограничили наш интерес периодом с 1900 по 2999 год.

Теперь, когда мы успешно сопоставили наш общий формат даты, нам нужно еще больше ограничить его, чтобы убедиться, что даты действительно правильные:

^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$

Здесь мы представили три группы целочисленных диапазонов, которые должны совпадать:

(19|2[0-9])[0-9]{2}

охватывает ограниченный диапазон лет путем сопоставления числа, которое начинается с 19 или 2X , за которым следует пара любых цифр.

0[1-9]|1[012]

соответствует номеру месяца в диапазоне 01-12

0[1-9]|[12][0-9]|3[01]

соответствует номеру дня в диапазоне 01-31

Даты совпадений: 1900-01-01 , 2205-02-31 , 2999-12-31

Несовпадающие даты: 1899-12-31 , 2018-05-35 , 2018-13-05 , 3000-01-01 , 2018-01-XX

3.3. Соответствие 29 февраля

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

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

  • Если число, образованное двумя последними цифрами числа, делится на 4, то исходное число делится на 4.
  • Если последние две цифры числа равны 00, то число делится на 100.

Вот решение:

^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$

Выкройка состоит из следующих частей:

2000|2400|2800

соответствует набору високосных лет с делителем 400 в ограниченном диапазоне 1900-2999

19|2[0-9](0[48]|[2468][048]|[13579][26]))

соответствует всем комбинациям лет из белого списка , которые имеют делитель 4 и не имеют делителя 100

-02-29

матчи 2 февраля

Даты матчей: 29 февраля 2020 г., 29 февраля 2024 г., 29 февраля 24:00

Несовпадающие даты: 2019-02-29 , 2100-02-29 , 3200-02-29 , 2020/02/29

3.4. Соответствие общим дням февраля

Помимо сопоставления с 29 февраля в високосные годы, нам также необходимо сопоставить все остальные дни февраля (1–28) во все годы :

^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$

Даты матчей: 01.02.2018 , 13.02.2019 , 25.02.2020

Несовпадающие даты: 30 февраля 2000 г., 62 февраля 2400 г., 28 февраля 2018 г.

3.5. Соответствие 31-дневным месяцам

Месяцы январь, март, май, июль, август, октябрь и декабрь должны совпадать от 1 до 31 дня:

^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$

Даты матчей: 31 января 2018 г., 31 июля 2021 г., 31 августа 2022 г.

Несовпадающие даты: 32 января 2018 г., 64 марта 2019 г., 31 января 2018 г.

3.6. Соответствие 30-дневным месяцам

Месяцы апрель, июнь, сентябрь и ноябрь должны совпадать от 1 до 30 дней:

^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$

Даты матчей: 30 апреля 2018 г., 30 июня 2019 г., 30 сентября 2020 г.

Несовпадающие даты: 31.04.2018 , 31.06.2019 , 30.04.2018

3.7. Григорианский определитель дат

Теперь мы можем объединить все приведенные выше шаблоны в один сопоставитель, чтобы получить полный GregorianDateMatcher , удовлетворяющий всем ограничениям:

class GregorianDateMatcher implements DateMatcher {

private static Pattern DATE_PATTERN = Pattern.compile(
"^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$"
+ "|^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$"
+ "|^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$"
+ "|^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$");

@Override
public boolean matches(String date) {
return DATE_PATTERN.matcher(date).matches();
}
}

Мы использовали символ чередования «|». соответствовать хотя бы одной из четырех ветвей. Таким образом, действительная дата февраля соответствует либо первой ветви 29 февраля високосного года, либо второй ветви любого дня от 1 до 28 . Даты остальных месяцев совпадают с третьей и четвертой ветвями.

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

На данный момент мы удовлетворили все ограничения, которые мы ввели в начале.

3.8. Примечание о производительности

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

Рассмотрите возможность использования LocalDate.parse() , предоставляемого Java8, если требуется надежный и быстрый подход к проверке даты.

4. Вывод

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

Весь код, представленный в этой статье, доступен на Github . Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.