«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 февраля

Даты совпадения: 2020-02-29, 2024-02-29, 2400-02 -29

Несовпадающие даты: 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.02.2000, 62.02.2018, 28.02.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]))$

Соответствие дат: 2018-01-31, 2021- 31.07, 31.08.2022

Несовпадающие даты: 32.01.2018, 64.03.2019, 31.01.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 г., 2020 г. 09-30

Несовпадающие даты: 2018-04-31, 2019-06-31, 2018/04/30

3.7. Gregorian Date Matcher

Теперь мы можем объединить все приведенные выше шаблоны в один сопоставитель, чтобы получить полный 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, поэтому его легко импортировать и запускать как есть.