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

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

1. Вступление

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

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

Для введения в регулярные выражения обратитесь кour Guide To Java Regular Expressions API.

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

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

Давайте также включим понятие годаleap, то есть года, содержащего день 29 февраля. According to the Gregorian calendar, we’ll call a year leap if the year number can be divided evenly by 4 except for those which are divisible by 100 but including those which are divisible by 400.

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

Примеры действительных дат:

  • 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, который предоставляет единственный методmatches:

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();
    }
}

Здесь мы указываем, чтоa valid date must consist of three groups of integers separated by a dash. Первая группа состоит из четырех целых чисел, а остальные две группы имеют по два целых числа в каждой.

Соответствующие даты: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])$

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

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

Чтобы правильно сопоставить високосные годы, мы должны сначалаidentify when we have encountered a leap year, а затем убедиться, что мы принимаем 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])) соответствует всем комбинациям летwhite-list, которые имеют делитель4 и не имеют делителя100

  • -02-29 соответствуетFebruary 2nd

Соответствующие даты:2020-02-29,2024-02-29,2400-02-29

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

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

Как и 29 февраля в високосные годы,we also need to match all other days of February (1 – 28) in all years:

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

Соответствующие даты:2018-02-01,2019-02-13,2020-02-25

Несовпадающие даты:2000-02-30,2400-02-62,2018/02/28

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-07-31,2022-08-31

Несовпадающие даты:2018-01-32,2019-03-64,2018/01/31

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

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

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

Соответствующие даты:2018-04-30,2019-06-30,2020-09-30

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

3.7. Григорианский дататор

Теперь мы можемcombine all of the patterns above into a single matcher to have a complete 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();
    }
}

We’ve used an alternation character “|” to match at least one четырех ветвей. Таким образом, действительная дата февраля соответствует либо первой ветви 29 февраля високосного года, либо второй ветви любого дня от1 до28. Даты оставшихся месяцев соответствуют третьей и четвертой ветвям.

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

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

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

Parsing complex regular expressions may significantly affect the performance of the execution flow. Основная цель этой статьи не состояла в том, чтобы изучить эффективный способ проверки строки на ее принадлежность к набору всех возможных дат.

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

4. Заключение

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

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