- PVSM.RU - https://www.pvsm.ru -
Знаете ли вы, в чём разница между 'Y' и 'y' символами в паттерне даты в Java? В этой статье мы рассмотрим, как неправильное форматирование даты может привести к ошибке, а также расскажем вам про нашу новую диагностику V6122 для языка Java, которая убережёт вас от внезапных путешествий во времени.
Сдув пыль с нашего большого блокнота под названием "TODO", мы наткнулись на один очень интересный кейс. Потенциальную проблему нам описали в комментарии к статье.
Кстати, вот вам идея на заметку: SimpleDateFormat в Java может подготовить сюрприз к Новому году: год, написанный маленькими буквами совсем не то же самое, что год, написанный большими. В последнюю неделю старого года вы можете вдруг оказаться в будущем, потому что "YYYY" — это 2022 для дат 27.12.2021 — 31.12.2021, а не 2021, как кто-то может ожидать (Пунктуация и орфография сохранены. — Прим. ред.)
Давайте разбираться.
Если вы вдруг забыли, что это за SimpleDateFormat такой, то можно освежить свои знания.
Для хранения и отображения дат зачастую нужно, чтобы они соответствовали какому-то определённому паттерну. И SimpleDateFormat — это класс, который позволяет нам удобно форматировать дату в соответствии с заданным паттерном. Также, помимо форматирования, SimpleDateFormat умеет парсить строки, превращая их в объект даты.
Это всё, что предлагает нам Java? Помимо SimpleDateFormat, форматировать даты умеет и класс DateTimeFormatter.
Давайте я покажу вам пример использования этих двух классов.
Форматирование даты через SimpleDateFormat:
public static void main(String[] args) {
Date date = new Date("2024/12/31");
var dateFormatter = new SimpleDateFormat("dd-MM-yyyy");
System.out.println(dateFormatter.format(date));
}
Вывод в консоль:
31-12-2024
Что здесь произошло?
У нас есть дата date, и мы хотим сохранить/отобразить её нужным нам образом. Мы создали объект SimpleDateFormat, передав в конструктор строку-паттерн. Дата будет отформатирована именно в соответствии с этим паттерном. Метод format возвращает строковое представление отформатированной даты. Именно то, что мы и хотели.
То же самое, но через DateTimeFormatter:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 12, 31);
var formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(formatter.format(date));
}
Вывод в консоль:
31-12-2024
Те же действия и тот же результат, но есть небольшое отличие — DateTimeFormatter позволяет форматировать лишь те даты, которые представлены классом, реализующим интерфейс TemporalAccessor. К примеру, таковыми являются классы LocalDate и LocalDateTime. SimpleDateFormat форматирует лишь объекты класса Date.
Вернёмся к комментарию. Действительно ли при использовании 'Y' в паттерне даты вместо 'y' результат может измениться?
Код:
public static void main(String[] args) {
Date date = new Date("2024/12/31");
var dateFormatter = new SimpleDateFormat("dd-MM-YYYY");
System.out.println(dateFormatter.format(date));
}
Вывод в консоль:
31-12-2025
Упс. А с DateTimeFormatter'ом будет также?
Код:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 12, 31);
var formatter = DateTimeFormatter.ofPattern("dd-MM-YYYY");
System.out.println(formatter.format(date));
}
Вывод в консоль:
31-12-2025
Мы действительно улетели на год вперёд. Давайте разбираться.
Первым делом я отправился в документацию класса SimpleDateFormat [1]. Вот небольшой фрагмент из таблицы, в котором описывается, как в паттерне даты интерпретируются интересующие нас буквенные символы:
Letter |
Date or Time Component |
Presentation |
Examples |
---|---|---|---|
y |
Year |
Year |
1996; 96 |
Y |
Week year |
Year |
2009; 09 |
Week year? Попробую объяснить.
Week year — это год, основанный на номере недели в году. Что это и для чего нужно?
Существуют задачи, в рамках которых важен порядковый номер недели в году. Для того, чтобы определить порядковый номер недели, нам нужно определиться с тем, какую неделю считать первой. Ведь практически всегда один год сменяется другим так, что часть недели приходится на старый год, а другая часть — на новый. Тогда как нам определиться, к какому году эта неделя относится? Регламентирует это стандарт ISO-8601.
По этому стандарту первая неделя года обязана удовлетворять следующим условиям:
первый день недели — понедельник;
минимальное количество дней года в неделе — четыре.
Из этого можно сформулировать простое правило: неделя будет считаться первой в году, когда четверг приходится на январь.
Теперь немного нагляднее.
Возьмём дату из примера — 31.12.2024. Снизу приведён фрагмент календаря, охватывающий неделю, в которую входит наша дата (декабрь 2024 — январь 2025):
ПН |
ВТ |
СР |
ЧТ |
ПТ |
СБ |
ВС |
---|---|---|---|---|---|---|
30 |
31 |
1 |
2 |
3 |
4 |
5 |
Эта неделя будет считаться первой неделей 2025 года, поскольку удовлетворяет приведённым выше условиям (в ней пять январских дней). Поэтому, используя спецификатор 'Y', мы получим 2025 год.
Как вы можете догадаться, в случае с DateTimeFormatter'ом ситуация обстоит точно так же.
Причём важно уточнить, что путешествовать мы можем не только в будущее, но и в прошлое.
Давайте возьмём другую дату — 01.01.2027.
Будет ли первое января входить в первую неделю 2027 года? Снова обратимся к календарю.
Снизу приведён фрагмент календаря, охватывающий неделю, к которой относится наша дата (декабрь 2026 — январь 2027):
ПН |
ВТ |
СР |
ЧТ |
ПТ |
СБ |
ВС |
---|---|---|---|---|---|---|
28 |
29 |
30 |
31 |
1 |
2 |
3 |
Поскольку в рамках этой недели всего три январских дня (а по условиям первой недели должно быть 4), то она будет считаться последней неделей 2026 года. Соответственно, наша дата при форматировании с использованием 'Y' символа отобразит нам 2026 год.
Доказательства в студию. Код:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2027, 1, 1);
var formatter = DateTimeFormatter.ofPattern("dd-MM-YYYY");
System.out.println(formatter.format(date));
}
Вывод в консоль:
01-01-2026
Итак, проблема разобрана. Нам она показалась достаточно интересной, чтобы добавить соответствующую диагностику в анализатор PVS-Studio для языка Java.
Диагностика написана, пришло время тестировать.
При разработке анализатора один из этапов тестирования — прогон регрессионных тестов. На нашем сайте есть статья [2] о том, как этот процесс у нас реализован. Если вкратце, то при добавлении новой диагностики мы анализируем большой пул Open Source проектов и сравниваем новые отчёты с эталонными.
В случае с этой диагностикой на нескольких проектах новые срабатывания были. Предлагаю на них взглянуть.
В этом проекте фрагменты кода, на которые указывает диагностика, идентичные, поэтому я приведу только один из них.
Код:
public Builder setPersonalisation(Date date, .... {
....
final OutputStreamWriter
out = new OutputStreamWriter(bout, "UTF-8");
final DateFormat
format = new SimpleDateFormat("YYYYMMdd"); // <=
out.write(format.format(date));
....
}
Предупреждение PVS-Studio:
V6122 [3] Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). SkeinParameters.java 246
Первым делом я решил заглянуть на GitHub. Вдруг, если это действительно ошибка, разработчики обнаружили это и закоммитили исправления? Так и произошло. Вот ссылка на коммит [4], можете ознакомиться. Все наши 'Y' (week year) символы в паттерне заменили на 'y' (year).
И здесь у вас может возникнуть вопрос, почему коммиты были залиты относительно давно. Давайте объясню. Задача наших регрессионных тестов заключается не в том, чтобы мы непрерывно контролировали качество того или иного Open Source проекта. Задача состоит в том, чтобы посмотреть, как при добавлении новой диагностики изменится отчёт: не должны пропасть старые срабатывания, не должны вылезти ошибки, которые сигнализируют о том, что анализатор сломался. Соответственно, проверяемый код должен быть одним и тем же.
Теперь давайте взглянем на второй проект, в котором сработала диагностика.
Срабатывание PVS-Studio:
V6122 [3] Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). RepositoryInfo.java 77
Код:
public class RepositoryInfo implements Serializable {
....
protected static final SimpleDateFormat
outputDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm Z");
....
}
По аналогии с предыдущим проектом я побежал смотреть, что там с коммитами. Для начала стоит отметить, что в результате рефакторинга (ссылка на коммит [5]) поле переехало к его классу-наследнику Repository. Я пошёл искать дальше и нашёл коммит [6] с исправлением. 'Y' символ в паттерне даты заменили на 'y':
public abstract class Repository extends RepositoryInfo {
....
protected static final SimpleDateFormat
OUTPUT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm Z");
....
}
Значит, и здесь это было ошибкой.
Мы в PVS-Studio рады любому фидбэку от сообщества. Поработав с комментарием от пользователя с Хабра, мы сделали Java-анализатор немного лучше, добавив полезную диагностику. Так что, если у вас есть какие-то мысли, которыми вы хотите с нами поделиться, мы с радостью пообщаемся с вами в комментариях к этой статье.
К слову, эта диагностика вышла в нашем октябрьском релизе 7.33, поэтому если у вас появилось желание попробовать наш анализатор, вы можете сделать это по этой ссылке [7].
А на этом всё, буду с вами прощаться. Надеюсь, внезапное путешествие на год вперёд (или назад) не застигнет вас врасплох.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Bogdanov. YYYY? yyyy! [8].
Автор: vlade1k
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/402521
Ссылки в тексте:
[1] документацию класса SimpleDateFormat: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/SimpleDateFormat.html
[2] есть статья: https://pvs-studio.ru/ru/blog/posts/java/0752/
[3] V6122: https://pvs-studio.ru/ru/docs/warnings/v6122/
[4] ссылка на коммит: https://github.com/bcgit/bc-java/commit/942fe041f2a5ef44e16eea48338a9244cdd1af93
[5] ссылка на коммит: https://github.com/oracle/opengrok/commit/fe6c99a860e45891d0216bd23af8641f0e247831
[6] коммит: https://github.com/oracle/opengrok/commit/2173ed7b20b4d6ae704b761403b2af7f52426e38#diff-4b1e319c4132c5cfd6da39c84ab50c2b427b78d3dec2b0139c94c63aefb4edba
[7] этой ссылке: https://pvs-studio.ru/ru/pvs-studio/try-free/?utm_source=website&utm_medium=habr&utm_campaign=article&utm_content=1185
[8] YYYY? yyyy!: https://pvs-studio.com/en/blog/posts/java/1185/
[9] Источник: https://habr.com/ru/companies/pvs-studio/articles/858512/?utm_source=habrahabr&utm_medium=rss&utm_campaign=858512
Нажмите здесь для печати.