28

This started as a simple error: I had YYYY instead of yyyy in my format string for a SimpleDateFormat object. But I'm totally baffled by the results of my tests with the incorrect format string.

This code:

@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());
        }
}

produces this output:

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

I've read the documentation on the 'Y' parameter here: https://docs.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html, but I still can't see the logic that's working here. Particularly the last instance: I can kinda-sorta understand how the dates in January (& maybe February) can be translated into December of the previous year, but moving the date of November 29th backwards by 11 months baffles me. And what's so special about December 27th?

Can anyone explain?

MORE INFORMATION

@Jan suggested that relying on the toString() method could be a problem, so I defined a date format to print YYYY MM dd '-' yyyy MM dd in the same code as above. Here is the additional output:

2016 12 27 - 2015 12 27
2016 12 27 - 2015 12 27
2027 12 27 - 2026 12 27
Dave Mulligan
  • 1,515
  • 3
  • 17
  • 28
  • 1
    relying on toString() to render something useful is optimistic as well. You might want to use different DateFormat for output. Like one including both yyyy and YYYY ? – Jan Jan 09 '16 at 09:08
  • See [here](http://stackoverflow.com/questions/8686331/y-returns-2012-while-y-returns-2011-in-simpledateformat) for explanation of year VS week year – theo Jan 09 '16 at 09:13
  • 1
    @theo Thanks, but that only explains variations if the date is in the first or last week of the year, which is only possibly the case for the first of my three examples. – Dave Mulligan Jan 09 '16 at 09:21
  • 1
    @Jan:good idea, but using a format with the format string of `yyyy YYYY` gives consistent results: #1 and #2 both give `2016 2015` and #3 gives `2027 2026`. Which doesn't get us anywhere. – Dave Mulligan Jan 09 '16 at 09:31
  • But does the DateFormat for output confirm the day to be dec 27th? – Jan Jan 09 '16 at 09:33
  • Yep, I'll edit the post to show that – Dave Mulligan Jan 09 '16 at 09:37
  • Upvoted, as proper naming of tests is important. – inanutshellus Dec 17 '20 at 22:50

2 Answers2

14

It's simple: December 27 2015 is day 1 of week 1 of week-year 2016 (and December 27 2026 is day 1 of week 1 of week-year 2027). This can be verified by adding these lines:

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));

If a SimpleDateFormat outputs a date it can use all fields: year, month, day, day of week, week of month, week in year, week-year etc.

On parsing, SimpleDateFormat expects a matching set of values: either day, month, year or day of week, week in year, week-year. Since you supplied a week-year but did not supply day of week and week in year, those to values have been assumed as 1.


The actual values depend on your locale:

  • which week of a year is week 1
  • which day is the first day of the week

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

On my system (using de-ch locale, with "EEE MMM dd HH:mm:ss zzz yyyy - YYYY-ww-u" as format) I get

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
Thomas Kläger
  • 10,196
  • 2
  • 16
  • 27
  • otherwise correct, but "and of week-year 2027" is certainly wrong. Fix that part of the answer. It would make no sense that 2015 would correspond to week-year 2027. – eis Jan 09 '16 at 11:01
  • Not quite right: the output for `u` in all of these cases is 7, not 1. But otherwise correct: the parsing is indeed completely ignoring the values provided for `MM` and `dd`. I can't find any documentation of this behavior. – Dave Mulligan Jan 09 '16 at 17:38
  • @DaveMulligan thanks for your feedback. The actual values depend on your systems locale, so I've enhanced my answer. – Thomas Kläger Jan 09 '16 at 22:31
  • FYI, the terribly troublesome date-time classes such as [`java.util.Date`](https://docs.oracle.com/javase/10/docs/api/java/util/Date.html), [`java.util.Calendar`](https://docs.oracle.com/javase/10/docs/api/java/util/Calendar.html), and `java.text.SimpleDateFormat` are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes built into Java 8 and later. See [*Tutorial* by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque Jul 06 '19 at 06:06
11

Definitions vary

A week can be defined in different ways. For example, in the United States, the first day of the week is considered to be Sunday most often while in Europe and many other places the first day is Monday.

Likewise, the week of a week-based year can also be defined in different ways.

  • Week # 1 contains January 1.
  • Week # 1 is the first week to contain a particular day-of-week such as Sunday.
  • Week # 1 is the first week to contain only days of the new year, with no dates from the previous year.
  • … and more

The legacy classes you are using implicitly use the definitions specified by a Locale. The locale being applied is also implicit, using the JVM’s current default Locale if you do not otherwise specify.

For even more interesting details about the difficulty in defining a week, see this Question, Different behavior of WeekFields on JVM 8 and JVM 10

ISO 8601

There is a practical international standard for date-time handling, ISO 8601.

The ISO 8601 definition of a week is:

  • Weeks start on a Monday
  • Week # 1 has the first Thursday of the calendar year
  • A year consists of either 52 or 53 whole weeks.
  • A year may have one or more days from the previous calendar year, and also from the following calendar year.

I suggest using the ISO 8601 standard definition whenever possible. This standard definition is simple and logical, with increasing adoption across industries.

java.time

The java.time classes offer some support for week of week-based year in the WeekFields class.

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

See this code run live at IdeOne.com.

ld.toString(): 2019-01-01

week: 1

org.threeten.extra.YearWeek

But if doing much of this work, I suggest adding the ThreeTen-Extra library to your project. You will find the YearWeek class to be helpful. This class offers several handy methods such as generating a LocalDate for any day within that week.

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

Notice how the first day of the year in the week-based year of 2019 is a date from the previous calendar year, 2018 rather than 2019.

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


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

Basil Bourque
  • 218,480
  • 72
  • 657
  • 915