9

I have a method to view a calendar in Java that calculates the date by year, day of the week and week-number.

Now when I calculates the dates from 2017 everything works. But when I calculates the dates from January 2018 it takes the dates of year 2017.

My code looks like

import java.time.temporal.IsoFields;
import java.time.temporal.ChronoField;
import java.time.LocalDate;

// .....

LocalDate desiredDate = LocalDate.now()
                    .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 1)
                    .with(ChronoField.DAY_OF_WEEK, 1)
                    .withYear(2018);

Which results in 2018-01-02 and it should be 2018-01-01. How is this possible?

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
JimmyD
  • 2,629
  • 4
  • 29
  • 58
  • Maybe the dayOfWeek index starts from 0 – Sayan Sil Nov 13 '17 at 07:45
  • ON the Javadoc of the ChronoFIeld it says: Monday (1) to Sunday (7) – JimmyD Nov 13 '17 at 07:46
  • @SayanSil a) it doesn't, and b) that wouldn't explain the result `2018-01-02` – eis Nov 13 '17 at 07:46
  • Yup. Checked. It starts from 1. Did you check for weeks other than the starting week of a year? – Sayan Sil Nov 13 '17 at 07:52
  • 3
    If you print out the incremental steps in the chain (`2017-11-13` -> `2017-01-02` -> `2017-01-02` -> `2018-01-02`) it becomes clearer what's going wrong. – dimo414 Nov 13 '17 at 08:01
  • Sigh... this is such an old problem with dates that are modified one field at a time, and has been a standard trap that programmers fall into for decades. It's sad to realize that the new `java.time` APIs still have this pothole. – Jim Garrison Nov 13 '17 at 08:11

2 Answers2

15

The order of invoked methods seems matter.
It you invoke them by descending time-granularity (year, week of week and day of week), you get the correct result :

long weekNumber = 1;
long dayOfWeek = 1;
int year = 2018;

LocalDate desiredDate = LocalDate.now()
    .withYear(year)
    .with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, weekNumber)
    .with(ChronoField.DAY_OF_WEEK, dayOfWeek );

System.out.println(desiredDate);

2018-01-01

Note that the problem origin comes from :

.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, weekNumber)

that sets the week number (1 to 53) according to the current year.
The Java LocalDate API cannot adapt this value if then you change the year with .withYear(year) as the week number information is not kept in the LocalDate instance.

You can indeed see in LocalDate implementation that LocalDate instances are defined by only 3 field : year, month and day.

public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
    ...
    private final int year;
    /**
     * The month-of-year.
     */
    private final short month;
    /**
     * The day-of-month.
     */
    private final short day;
    ...
}

So to be precise, the important thing is that :

.withYear(year) be invoked before

.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, weekNumber);
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • Tnx that did the job. – JimmyD Nov 13 '17 at 07:51
  • 2
    makes sense - methods adjust the previous result, they aren't evaluated at the same time. But I can understand someone getting caught by surprise due to this. – eis Nov 13 '17 at 07:57
  • 1
    @davidxxx the original version adjusts to the first Monday of 2017 (Jan 2nd) then changes the year. – assylias Nov 13 '17 at 08:13
  • 1
    If you try to build a date by taking an existing date and modifying it one field at a time there are cases where it is not possible to set the date correctly even if you order setting by granularity. This is a major pothole in the API. The only reliable way to set a date consistently in all cases is to use a constructor that sets all the fields simultaneously. – Jim Garrison Nov 13 '17 at 08:14
  • @assylias I understand. Thanks. I imagined that the API could do this kind of back change (adapting previous information with the new one). – davidxxx Nov 13 '17 at 08:18
  • @Jim Garrison Interesting thing but which constructor ? The `LocalDate.of()` factory method, doesn't allow to do what the OP asks. – davidxxx Nov 13 '17 at 08:23
  • 1
    @davidxxx each `with` returns a brand new immutable LocalDate (i.e. essentially a number of days since the epoch) that knows nothing of the previous operations. – assylias Nov 13 '17 at 08:33
  • @assylias It is right. Just `year`, `month` and `day` are used to represent a `LocalDate`. – davidxxx Nov 13 '17 at 08:50
  • @JimGarrison @davidxxx Yes, the `java.time`-API is missing a factory method for week dates like in [my library Time4J](http://time4j.net/javadoc-en/net/time4j/PlainDate.html#of-int-int-net.time4j.Weekday-). – Meno Hochschild Nov 14 '17 at 12:51
2

I want to mention, that there is another Problem(?) with LocalDate.

This Code does also create a wrong result:

    int jahr = Integer.parseInt(str[0]);
    int woche = Integer.parseInt(str[1]);

    LocalDate year = LocalDate.of(jahr, 1, 1);
    LocalDate week = year.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, woche);
    LocalDate day = week.with(wochentag);
    return day;

If you change the creation of the year variable to

 LocalDate year = LocalDate.now().withYear(jahr);

the code returns the expected result. It seems as the way you construct a LocalDate matters. I guess the timezone is omitted in the ".of()" version.

KFleischer
  • 942
  • 2
  • 11
  • 33