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, поэтому его легко импортировать и запускать как есть.