DateTimeFormatter week-based-year diff
Asked Answered
N

1

2

I'm migrating my application from Joda-Time to the Java 8 java.time.

One of the things I ran into is the printing of a week-based-year using a pattern in the DateTimeFormatter.

NOTE: I have seen this question: Java Time's week-of-week-based-year pattern parsing with DateTimeFormatter

According to the documentation

y       year-of-era                 year              2004; 04
Y       week-based-year             year              1996; 96

Yet when I try these two it seems the Y is always returning the same as y.

My testing code:

DateTimeFormatter yearF = DateTimeFormatter.ofPattern("yyyy").withZone(ZoneOffset.UTC);
DateTimeFormatter weekYearF = DateTimeFormatter.ofPattern("YYYY").withZone(ZoneOffset.UTC);

DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
    .appendValue(ChronoField.YEAR_OF_ERA)   .appendLiteral(" ") .append(yearF)
    .appendLiteral(" -- ")
    .appendValue(IsoFields.WEEK_BASED_YEAR) .appendLiteral(" ") .append(weekYearF)
    .toFormatter()
    .withZone(ZoneOffset.UTC);

System.out.println(dateTimeFormatter.toString());

ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(946778645000L), ZoneOffset.UTC);
for (int i = 2000 ; i < 2020; i ++ ) {
    System.out.println(dateTime.withYear(i).format(dateTimeFormatter));
}

The output:

Value(YearOfEra)' '(Value(YearOfEra,4,19,EXCEEDS_PAD))' -- 'Value(WeekBasedYear)' '(Localized(WeekBasedYear,4,19,EXCEEDS_PAD))
2000 2000 -- 1999 2000
2001 2001 -- 2001 2001
2002 2002 -- 2002 2002
2003 2003 -- 2003 2003
2004 2004 -- 2004 2004
2005 2005 -- 2004 2005
2006 2006 -- 2006 2006
2007 2007 -- 2007 2007
2008 2008 -- 2008 2008
2009 2009 -- 2009 2009
2010 2010 -- 2009 2010
2011 2011 -- 2010 2011
2012 2012 -- 2012 2012
2013 2013 -- 2013 2013
2014 2014 -- 2014 2014
2015 2015 -- 2015 2015
2016 2016 -- 2015 2016
2017 2017 -- 2017 2017
2018 2018 -- 2018 2018
2019 2019 -- 2019 2019

Looking at the years where it matters (like 2000, 2005, 2009 and 2016) the output of .appendValue(IsoFields.WEEK_BASED_YEAR) and .ofPattern("YYYY") are different.

In Java Time's week-of-week-based-year pattern parsing with DateTimeFormatter it is stated that this has to do with the localization (as can be clearly seen as a difference in the toString() of the DateTimeFormatter).

Now there are few things I do not understand/need:

  1. So the 'week-based-year' varies with the Locale, fine. Yet what I do not understand that apparently in some Locales the week-base-year is always identical to the 'normal' year. Why is that?

  2. Why hasn't the parsing of the YYYY been mapped to the ISO-8601 definition instead of the (terribly confusing!) Localized form.

  3. Where can I find proper documentation of this? The apparent 'official' documentation from Oracle is vague to say the least. Answer: I found much more extensive documentation with the DateTimeFormatterBuilder.

Nepenthe answered 24/9, 2017 at 10:36 Comment(2)
For your first question, 02-01-2000 is still in week 52 of year 1999. Which gives you back 1999. Same for 2005 and 2016.Expedite
@YoshuaNahar: What I meant is that I expect the year and week-based-year to be different every few years. I am surprised that apparently in some Locales there is no difference.Nepenthe
M
6

The week-based-year field, according to javadoc, depends on 2 things: what's the first day of the week, and the minimum number of days in the first week.

The ISO standard defines Monday as the first day of the week, and a minimum of 4 days in the first week:

System.out.println(WeekFields.ISO.getFirstDayOfWeek()); // Monday
System.out.println(WeekFields.ISO.getMinimalDaysInFirstWeek()); // 4

(WeekFields.ISO.weekBasedYear() is equivalent to IsoFields.WEEK_BASED_YEAR, with minor differences regarding another calendar systems)

Considering, for example, January 2nd 2009, which was a Friday. Checking the javadoc for the week-based-year field:

Week one(1) is the week starting on the getFirstDayOfWeek() where there are at least getMinimalDaysInFirstWeek() days in the year. Thus, week one may start before the start of the year.

Considering ISO definition (week starts on Monday, and minimum days in first week is 4), week 1 starts on December 29th 2008 and ends in January 4th 2009 (that's the first week that starts on a Monday and it has at least 4 days in 2009), so January 2nd 2009 has a week-based-year equals to 2009 (with ISO definition):

// January 2st 2009
LocalDate dt = LocalDate.of(2009, 1, 2);
System.out.println(dt.get(WeekFields.ISO.weekBasedYear())); // 2009
System.out.println(dt.get(WeekFields.ISO.weekOfWeekBasedYear())); // 1
// WeekFields.ISO and IsoFields are equivalent
System.out.println(dt.get(IsoFields.WEEK_BASED_YEAR)); // 2009
System.out.println(dt.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR)); // 1

But if I consider a WeekFields instance for en_MT locale (English (Malta)), the first day of week is Sunday, and the minimum number of days in first week is 4:

WeekFields wf = WeekFields.of(new Locale("en", "MT"));
System.out.println(wf.getFirstDayOfWeek()); // Sunday
System.out.println(wf.getMinimalDaysInFirstWeek()); // 4
System.out.println(dt.get(wf.weekBasedYear())); // 2008
System.out.println(dt.get(wf.weekOfWeekBasedYear())); // 53

The first week that starts on a Sunday and has at least 4 days in 2009 is the week from January 4th to 10th. So, according to the week definition for en_MT locale, January 2nd 2009 belongs to the 53th week of the week-based-year 2008.

Now, if I take the ar_SA locale (Arabic (Saudi-Arabia)), the week starts on Saturday, and the minimum number of days in first week is 1:

WeekFields wf = WeekFields.of(new Locale("ar", "SA"));
System.out.println(wf.getFirstDayOfWeek()); // Saturday
System.out.println(wf.getMinimalDaysInFirstWeek()); // 1
System.out.println(dt.get(wf.weekBasedYear())); // 2009
System.out.println(dt.get(wf.weekOfWeekBasedYear())); // 1

For this locale, week 1 starts in December 27th 2008 and ends in January 2nd 2009 (it's the first week that starts on a Saturday and has at least 1 day in 2009). So, the week-based-year for January 2nd 2009 in ar_SA locale is also 2009 (the same value I've got using IsoFields, even though the week definition is completely different from ISO).


While IsoFields.WEEK_BASED_YEAR uses ISO's definition, the pattern YYYY will use the WeekFields instance corresponding to the locale set in the formatter (or the JVM default locale, if none is set).

Depending on the definition of each locale (first day of week and minimum number of days in first week), the week-based-year from the localized pattern (YYYY) might have the same value (or not) of the ISO field.

Although it sounds strange that a week can start or end in another year, the javadoc says that it's perfectly valid:

The first and last weeks of a year may contain days from the previous calendar year or next calendar year respectively.


The pattern letters of java.time were based on CLDR (The Unicode Common Locale Data Repository). This link about Week based patterns says:

The year indicated by Y typically begins on the locale’s first day of the week and ends on the last day of the week

Anyway, CLDR is all about localization, so Y is localized as well - as stated by Stephen Colebourne's comment below:

The whole purpose of CLDR is localization, so yes, the "Y" pattern letter is localized. While I understand the desire for a pattern letter that always operates using the ISO rules, it doesn't exist and getting CLDR to add it would be hard to impossible. (Java is following CLDR closely)


My conclusion is, if you want ISO week fields, don't use the localized patterns. Or, as a - not ideal, quite ugly - workaround, use a locale that matches ISO's week definition (in my JVM, Locale.FRENCH does the trick, as WeekFields.ISO.equals(WeekFields.of(Locale.FRENCH)) returns true). The only problem is that the locale will also affect other fields (in case you have month or day of week names, such as MMM or EEE, and any other locale sensitive data).

Minnow answered 24/9, 2017 at 22:9 Comment(3)
The whole purpose of CLDR is localization, so yes, the "Y" pattern letter is localized. While I understand the desire for a pattern letter that always operates using the ISO rules, it doesn't exist and getting CLDR to add it would be hard to impossible. (Java is following CLDR closely). The key here however is that unlike Java 7, a formatter can be created that can handle this use case via DateTimeFormatterBuilder.Trespass
@Trespass I've updated the answer (quoting your comment, if you don't mind). Thanks a lot!Minnow
Tip: If indeed standard ISO 8601 weeks are desired, see the ThreeTen-Extra project for its YearWeek class.Agan

© 2022 - 2024 — McMap. All rights reserved.