﻿# YYYY? yyyy\!

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

![1185_V6122Article_ru/image1.png](https://import.viva64.com/docx/blog/1185_V6122Article_ru/image1.png)

## Вступление

Сдув пыль с нашего большого блокнота под названием "TODO", мы наткнулись на один очень интересный кейс\. Потенциальную проблему нам описали в комментарии к статье\.

<details>
   <summary>Комментарий\\\.</summary>

Кстати, вот вам идея на заметку: SimpleDateFormat в Java может подготовить сюрприз к Новому году: год, написанный маленькими буквами совсем не то же самое, что год, написанный большими\. В последнюю неделю старого года вы можете вдруг оказаться в будущем, потому что "YYYY" — это 2022 для дат 27\.12\.2021 — 31\.12\.2021, а не 2021, как кто\-то может ожидать \(_Пунктуация и орфография сохранены\. — Прим\. ред\._\)


</details>
Давайте разбираться\.

## Анализ проблемы

### Форматирование дат

Если вы вдруг забыли, что это за _SimpleDateFormat_ такой, то можно освежить свои знания\.

Для хранения и отображения дат зачастую нужно, чтобы они соответствовали какому\-то определённому паттерну\. И _SimpleDateFormat_ — это класс, который позволяет нам удобно форматировать дату в соответствии с заданным паттерном\. Также, помимо форматирования, _SimpleDateFormat_ умеет парсить строки, превращая их в объект даты\.

Это всё, что предлагает нам Java? Помимо _SimpleDateFormat_, форматировать даты умеет и класс_ DateTimeFormatter_\.

Давайте я покажу вам пример использования этих двух классов\.

Форматирование даты через _SimpleDateFormat_:

```cpp
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_:

```cpp
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' результат может измениться?

Код:

```cpp
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_'ом будет также?

Код:

```cpp
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_](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/SimpleDateFormat.html)\. Вот небольшой фрагмент из таблицы, в котором описывается, как в паттерне даты интерпретируются интересующие нас буквенные символы:

|\*\*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 год\.

Доказательства в студию\. Код:

```cpp
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\.

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

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

При разработке анализатора один из этапов тестирования — прогон регрессионных тестов\. На нашем сайте [есть статья](https://pvs-studio.ru/ru/blog/posts/java/0752/) о том, как этот процесс у нас реализован\. Если вкратце, то при добавлении новой диагностики мы анализируем большой пул Open Source проектов и сравниваем новые отчёты с эталонными\.

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

### Bouncy Castle

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

Код:

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v6122/) Usage of 'Y' \(week year\) pattern was detected: it was probably intended to use 'y' \(year\)\. SkeinParameters\.java 246

Первым делом я решил заглянуть на GitHub\. Вдруг, если это действительно ошибка, разработчики обнаружили это и закоммитили исправления? Так и произошло\. Вот [ссылка на коммит](https://github.com/bcgit/bc-java/commit/942fe041f2a5ef44e16eea48338a9244cdd1af93), можете ознакомиться\. Все наши 'Y' \(week year\) символы в паттерне заменили на 'y' \(year\)\.

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

### Opengrok

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

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

[V6122](https://pvs-studio.ru/ru/docs/warnings/v6122/) Usage of 'Y' \(week year\) pattern was detected: it was probably intended to use 'y' \(year\)\. RepositoryInfo\.java 77

Код:

```cpp
public class RepositoryInfo implements Serializable {
    ....
    protected static final SimpleDateFormat 
        outputDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm Z"); 
    ....
}
```

По аналогии с предыдущим проектом я побежал смотреть, что там с коммитами\. Для начала стоит отметить, что в результате рефакторинга \([ссылка на коммит](https://github.com/oracle/opengrok/commit/fe6c99a860e45891d0216bd23af8641f0e247831)\) поле переехало к его классу\-наследнику _Repository_\. Я пошёл искать дальше и нашёл [коммит](https://github.com/oracle/opengrok/commit/2173ed7b20b4d6ae704b761403b2af7f52426e38#diff-4b1e319c4132c5cfd6da39c84ab50c2b427b78d3dec2b0139c94c63aefb4edba) с исправлением\. 'Y' символ в паттерне даты заменили на 'y':

```cpp
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, поэтому если у вас появилось желание попробовать наш анализатор, вы можете сделать это по [этой ссылке](https://pvs-studio.ru/ru/pvs-studio/try-free/)\.

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