Как работает Java "недельный год"?

Это началось как простая ошибка: у меня была YYYY вместо YYYY в моей строке формата для объекта SimpleDateFormat. Но меня полностью сбило с толку результаты моих тестов с неправильной строкой формата.

Этот код:

@Test
public void whatTheHell() {
        try {
                SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/YYYY");

                Date d1 = sdf.parse("01/07/2016");
                Date d2 = sdf.parse("02/08/2016");
                Date d3 = sdf.parse("11/29/2027");

                System.out.println(d1.toString());
                System.out.println(d2.toString());
                System.out.println(d3.toString());
        } catch (ParseException pe) {
                fail("ParseException: " + pe.getMessage());
        }
}

производит этот вывод:

Sun Dec 27 00:00:00 PST 2015
Sun Dec 27 00:00:00 PST 2015
Sun Dec 27 00:00:00 PST 2026

Я прочитал документацию по параметру "Y" здесь: https://docs.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html, но я все еще не вижу логики, которая работает здесь. В частности, последний экземпляр: я могу как-то разобраться, как даты в январе (возможно, февраль) могут быть переведены в декабрь прошлого года, но переключение даты 29 ноября назад на 11 месяцев меня озадачивает. А что такого особенного 27 декабря?

Может ли кто-нибудь объяснить?

ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ

@Я предположил, что проблема с методом toString() может быть проблемой, поэтому я определил формат даты для печати YYYY MM dd '-' yyyy MM dd в том же коде, что и выше. Вот дополнительный вывод:

2016 12 27 - 2015 12 27
2016 12 27 - 2015 12 27
2027 12 27 - 2026 12 27

Ответ 1

Это просто: 27 декабря 2015 года - это день 1 недели 1-го года 2016 года (а 27 декабря 2026 года - день 1 недели 1-го года 2027 года). Это можно проверить, добавив следующие строки:

SimpleDateFormat odf = new SimpleDateFormat("YYYY-ww-u");
System.out.println(odf.format(d1));
System.out.println(odf.format(d2));
System.out.println(odf.format(d3));

Если SimpleDateFormat выводит дату, он может использовать все поля: год, месяц, день, день недели, неделя месяца, неделя в году, неделя-год и т.д.

При разборе SimpleDateFormat ожидает соответствующий набор значений: день, месяц, год или день недели, неделя в году, неделя-год. Поскольку вы указали день недели, но не указали день недели и неделю в году, эти значения были приняты равными 1.


Фактические значения зависят от вашей локали:

  • какая неделя года является неделей 1
  • какой день является первым днем недели

(см. https://docs.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html#week_and_year)

В моей системе (с использованием языка de-ch, с форматом "EEE MMM dd HH: mm: ss zzz yyyy - YYYY-ww-u")

Mo Jan 04 00:00:00 MEZ 2016 - 2016-01-1
Mo Jan 04 00:00:00 MEZ 2016 - 2016-01-1
Mo Jan 04 00:00:00 MEZ 2027 - 2027-01-1

Ответ 2

Определения могут быть разными

Неделю можно определить по-разному. Например, в Соединенных Штатах первым днем недели считается воскресенье, а в Европе и во многих других местах первым днем является понедельник.

Аналогично, неделя в недельном году также может быть определена по-разному.

  • Неделя № 1 содержит 1 января.
  • Неделя № 1 - это первая неделя, в которой указан определенный день недели, например воскресенье.
  • Неделя № 1 - это первая неделя, содержащая только дни нового года без дат предыдущего года.
  • ... и многое другое

Унаследованные классы, которые вы используете, неявно используют определения, указанные в Locale. Применяемый языковой стандарт также является неявным, используя текущее значение по умолчанию JVM Locale, если не указано иное.

Еще более интересные подробности о сложности определения недели см. в этом Вопросе, Различное поведение WeekFields в JVM 8 и JVM 10

ISO 8601

Существует практический международный стандарт для обработки даты и времени, ISO 8601.

Определение недели ISO 8601 :

  • Недели начинаются в понедельник
  • Неделя № 1 имеет первый четверг года
  • Год состоит из 52 или 53 недель.
  • В году может быть один или несколько дней из предыдущего календарного года, а также из следующего календарного года.

Я предлагаю по возможности использовать стандартное определение ISO 8601. Это стандартное определение является простым и логичным с растущим распространением в разных отраслях.

java.time

Классы java.time предлагают некоторую поддержку для недельного года в классе WeekFields.

LocalDate ld = LocalDate.of( 2019 , Month.JANUARY , 1 ) ;
long week = ld.get( WeekFields.ISO.weekOfWeekBasedYear() ) ;

Смотрите этот код на сайте IdeOne.com.

ld.toString(): 2019-01-01

week: 1

org.threeten.extra.YearWeek

Но если вы выполняете большую часть этой работы, я предлагаю добавить библиотеку ThreeTen-Extra в ваш проект. Вы найдете класс YearWeek полезным. Этот класс предлагает несколько удобных методов, таких как создание LocalDate для любого дня в пределах этой недели.

LocalDate ld = LocalDate.of ( 2019 , Month.JANUARY , 1 );
YearWeek yw = YearWeek.from ( ld );
LocalDate startOfWeek = yw.atDay ( DayOfWeek.MONDAY );

ld.toString(): 2019-01-01

yw.toString(): 2019-W01

startOfWeek.toString(): 2018-12-31

Обратите внимание, что первый день года в недельном 2019 году является датой предыдущего календарного года, а не 2019 года.

calendar for month of January 2019, with week number, showing first week starts in the previous calendar year on 2018-12-31


О java.time

Среда java.time встроена в Java 8 и более поздние версии. Эти классы вытесняют проблемные старые устаревшие классы даты и времени, такие как java.util.Date, Calendar и & SimpleDateFormat.

Чтобы узнать больше, см. Учебное пособие по Oracle. И поищите в Кару множество примеров и объяснений. Спецификация: JSR 310.

Проект Joda-Time, который теперь находится в режиме обслуживания, рекомендует выполнить переход на классы java.time.

Вы можете обмениваться объектами java.time напрямую с вашей базой данных. Используйте драйвер JDBC, совместимый с JDBC 4.2 или более поздней версией. Нет необходимости в строках, нет необходимости в классах java.sql.*.

Где взять классы java.time?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11 и более поздние версии - часть стандартного Java API с встроенной реализацией ,
    • Java 9 добавляет некоторые мелкие функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональности java.time перенесена на Java 6 & 7 в ThreeTen-Backport.
  • Android