RegEx за съвпадение на модела на датата в Java

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]))$

Дати на съвпадение: 2018-02-01 , 2019-02-13 , 2020-02-25

Non-matching dates: 2000-02-30, 2400-02-62, 2018/02/28

3.5. Matching 31-Day Months

The months January, March, May, July, August, October, and December should match for between 1 and 31 days:

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

Matching dates: 2018-01-31, 2021-07-31, 2022-08-31

Non-matching dates: 2018-01-32, 2019-03-64, 2018/01/31

3.6. Matching 30-Day Months

The months April, June, September, and November should match for between 1 and 30 days:

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

Matching dates: 2018-04-30, 2019-06-30, 2020-09-30

Non-matching dates: 2018-04-31, 2019-06-31, 2018/04/30

3.7. Gregorian Date Matcher

Now we can combine all of the patterns above into a single matcher to have a complete GregorianDateMatcher satisfying all of the constraints:

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 of the four branches. Thus, the valid date of February either matches the first branch of February 29th of a leap year either the second branch of any day from 1 to 28. The dates of remaining months match third and fourth branches.

Since we haven't optimized this pattern in favor of a better readability, feel free to experiment with a length of it.

At this moment we have satisfied all the constraints, we introduced in the beginning.

3.8. Note on Performance

Анализирането на сложни регулярни изрази може значително да повлияе на ефективността на потока на изпълнение. Основната цел на тази статия не беше да научи ефективен начин за тестване на низ за неговото членство в набор от всички възможни дати.

Помислете за използването на LocalDate.parse (), предоставено от Java8, ако е необходим надежден и бърз подход за проверка на датата.

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

В тази статия научихме как да използваме регулярни изрази за съвпадение на строго форматираната дата от григорианския календар, като предоставяме правила за формата, диапазона и продължителността на месеците.

Целият код, представен в тази статия, е достъпен в Github. Това е проект, базиран на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.