Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top

YYYY? yyyy!

14 Ноя 2024

Знаете ли вы, в чём разница между 'Y' и 'y' символами в паттерне даты в Java? В этой статье мы рассмотрим, как неправильное форматирование даты может привести к ошибке, а также расскажем вам про нашу новую диагностику V6122 для языка Java, которая убережёт вас от внезапных путешествий во времени.

1185_V6122Article_ru/image1.png

Вступление

Сдув пыль с нашего большого блокнота под названием "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. Вот небольшой фрагмент из таблицы, в котором описывается, как в паттерне даты интерпретируются интересующие нас буквенные символы:

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.

Ошибки в реальных проектах

Диагностика написана, пришло время тестировать.

При разработке анализатора один из этапов тестирования — прогон регрессионных тестов. На нашем сайте есть статья о том, как этот процесс у нас реализован. Если вкратце, то при добавлении новой диагностики мы анализируем большой пул Open Source проектов и сравниваем новые отчёты с эталонными.

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

Bouncy Castle

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

Код:

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 Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). SkeinParameters.java 246

Первым делом я решил заглянуть на GitHub. Вдруг, если это действительно ошибка, разработчики обнаружили это и закоммитили исправления? Так и произошло. Вот ссылка на коммит, можете ознакомиться. Все наши 'Y' (week year) символы в паттерне заменили на 'y' (year).

И здесь у вас может возникнуть вопрос, почему коммиты были залиты относительно давно. Давайте объясню. Задача наших регрессионных тестов заключается не в том, чтобы мы непрерывно контролировали качество того или иного Open Source проекта. Задача состоит в том, чтобы посмотреть, как при добавлении новой диагностики изменится отчёт: не должны пропасть старые срабатывания, не должны вылезти ошибки, которые сигнализируют о том, что анализатор сломался. Соответственно, проверяемый код должен быть одним и тем же.

Opengrok

Теперь давайте взглянем на второй проект, в котором сработала диагностика.

Срабатывание PVS-Studio:

V6122 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"); 
    ....
}

По аналогии с предыдущим проектом я побежал смотреть, что там с коммитами. Для начала стоит отметить, что в результате рефакторинга (ссылка на коммит) поле переехало к его классу-наследнику Repository. Я пошёл искать дальше и нашёл коммит с исправлением. '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, поэтому если у вас появилось желание попробовать наш анализатор, вы можете сделать это по этой ссылке.

А на этом всё, буду с вами прощаться. Надеюсь, внезапное путешествие на год вперёд (или назад) не застигнет вас врасплох.

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте, отфильтровано ли письмо в одну из следующих стандартных папок:

  • Промоакции
  • Оповещения
  • Спам