1

I'm writing some integration of legacy code that expects date/time as a GregorianCalendar.

So, I'm creating the GregorianCalendar using from(ZonedDateTime) method.

However, the result is showing some inconsistent behaviour when the GregorianCalendar is created from the ZonedDateTime versus when the GregorianCalendar is created, let's say, from a string parsing or even from epoch millis. For example, when setting the Calendar day_of_week. To demonstrate this (https://onecompiler.com/java/3yssz4q86):

import java.util.*;
import java.time.*;

public class Main {
  public static void main(String[] args) {
    final GregorianCalendar calendarPure = new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles"));
    printInfo(calendarPure);

    ZoneId zone = ZoneId.of("America/Los_Angeles");
    final ZonedDateTime zdtNow = ZonedDateTime.now(zone);
    
    final GregorianCalendar calendarFromZdt = GregorianCalendar.from(zdtNow);
    printInfo(calendarFromZdt);
    
    final GregorianCalendar calendarFromZdtEpoch = new GregorianCalendar();
    calendarFromZdtEpoch.setTimeInMillis(zdtNow.toInstant().toEpochMilli());

    printInfo(calendarFromZdtEpoch);
  }
  
  private static void printInfo(GregorianCalendar cal) {
    System.out.printf("Now: %tc%n", cal);
    System.out.printf("Last Sunday: %tc%n", getLastSunday(cal));
    System.out.printf("Week of Year: %d%n", cal.get(Calendar.WEEK_OF_YEAR));
    System.out.printf("Time in Millis: %d%n", cal.getTimeInMillis());
    System.out.printf("toString(): %s%n", cal);
    System.out.println();
  }
  
  public static GregorianCalendar getLastSunday(GregorianCalendar referenceDate) {
    final GregorianCalendar lastSundayMidNight = (GregorianCalendar)referenceDate.clone();
    lastSundayMidNight.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
    return lastSundayMidNight;
  }

}

The output is:

Now: Wed Dec 28 16:45:36 PST 2022
Last Sunday: Sun Dec 25 16:45:36 PST 2022
Week of Year: 53
Time in Millis: 1672274736631
toString(): java.util.GregorianCalendar[time=1672274736631,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=53,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=362,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=45,SECOND=36,MILLISECOND=631,ZONE_OFFSET=-28800000,DST_OFFSET=0]

Now: Wed Dec 28 16:45:36 PST 2022
Last Sunday: Sun Jan 01 16:45:36 PST 2023
Week of Year: 52
Time in Millis: 1672274736668
toString(): java.util.GregorianCalendar[time=1672274736668,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=52,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=362,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=45,SECOND=36,MILLISECOND=668,ZONE_OFFSET=-28800000,DST_OFFSET=0]

Now: Thu Dec 29 00:45:36 UTC 2022
Last Sunday: Sun Dec 25 00:45:36 UTC 2022
Week of Year: 53
Time in Millis: 1672274736668
toString(): java.util.GregorianCalendar[time=1672274736668,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=11,WEEK_OF_YEAR=53,WEEK_OF_MONTH=5,DAY_OF_MONTH=29,DAY_OF_YEAR=363,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=5,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=45,SECOND=36,MILLISECOND=668,ZONE_OFFSET=0,DST_OFFSET=0]

I noticed that, from GregorianCalendar.toString() that prints the internal fields a different in firstDayOfWeek and minimalDaysInFirstWeek. That could be the cause for the divergence in the after results. I wonder where the GregorianCalendar is getting these values from. Maybe there is a way to set something on ZonedDateTime object that will ensure the results will be the same? Couldn't find anything on javadocs.

Note: Please, don't tell me not to use GregorianCalendar. I know, I know. I'm just writing an integration for two pieces of code that I'm not allowed to modify, one uses GregorianCalendar (sigh), the other uses ZonedDateTime.

L. Holanda
  • 4,432
  • 1
  • 36
  • 44
  • 1
    What is your LOCALE? – Jim Garrison Dec 29 '22 at 01:11
  • 2
    Can you elaborate what you're using `GregorianCalendar` for? It's based on `Calendar`, which is known to be almost impossible to use correctly. – Louis Wasserman Dec 29 '22 at 01:16
  • 1
    I think the accepted answer to [this question](https://stackoverflow.com/q/69300459/18157) is what you're looking for. Java has ***three*** different locale settings, and a careful reading of the Javadoc shows the "pure" `GregorianCalendar` and the one from ZDT use _different_ locales. As @LouisWasserman says, avoid `GregorianCalendar` if at all possible, it's a minefield. – Jim Garrison Dec 29 '22 at 01:16
  • 2
    Pro Tip: The only absolutely "safe" object that has no pitfalls is `Instant`. It represents a unique point on the time line. Everything else has nuances related to politics and representation, and is incredibly messy. The newer `java.time` classes are a huge improvement, but there are still traps and unintuitive behaviors. – Jim Garrison Dec 29 '22 at 01:26
  • 2
    `ZonedDateTime` uses ISO chronology, so the `GregorianCalendar` that you get from it uses ISO weeks: the week starts on Monday, and week 1 is the first week that has at least 4 days of the new year in it. The `GregorianCalendar` that you create yourself follows your default locale (as @JimGarrison hinted), which probably has a different definition of weeks. Try printing `cal.getFirstDayOfWeek()` and `cal.getMinimalDaysInFirstWeek()`. You may also try using the corresponding setters. – Ole V.V. Dec 29 '22 at 05:43
  • Or from the docs: *The `getFirstDayOfWeek()` and `getMinimalDaysInFirstWeek()` values are initialized using locale-dependent resources when constructing a `GregorianCalendar`. The week determination is compatible with the ISO 8601 standard when `getFirstDayOfWeek()` is `MONDAY` and `getMinimalDaysInFirstWeek()` is `4`, which values are used in locales where the standard is preferred. These values can explicitly be set by calling `setFirstDayOfWeek()` and `setMinimalDaysInFirstWeek()`.* – Ole V.V. Dec 29 '22 at 05:51
  • And about the `from` method: *the return GregorianCalendar ... uses ISO 8601 standard for week definitions, which has `MONDAY` as the `FirstDayOfWeek` and `4` as the value of the `MinimalDaysInFirstWeek`.* – Ole V.V. Dec 29 '22 at 05:52
  • (What I said also means that [the documentation of `from()`](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/GregorianCalendar.html#from(java.time.ZonedDateTime)) is telling a lie when it says *Obtains an instance of `GregorianCalendar` with the default locale from a `ZonedDateTime` object.*. The default locale plays no effective role in the creation of the `GregorianCalendar`.) – Ole V.V. Dec 29 '22 at 08:42
  • 1
    All of the above suggests: _"Friends don't let friends use `GregorianCalendar`"_ – Jim Garrison Dec 29 '22 at 18:32
  • Well... As I said... I'm just making a bridge from an API to another. The target one uses GregorianCalendar and there is nothing I can do about it. – L. Holanda Dec 30 '22 at 23:20
  • Adding `calendarFromZdt.setFirstDayOfWeek(Calendar.SUNDAY);` and `calendarFromZdt.setMinimalDaysInFirstWeek(1);` did the trick. Thank you. – L. Holanda Dec 30 '22 at 23:26
  • Thanks @OleV.V. Your comments put all together completely answers the question. Put all together in an answer and I'll accept it. – L. Holanda Dec 30 '22 at 23:27
  • Great to read. I no longer contribute answers to Stack Overflow for reasons that I try to explain in [my proflie](https://stackoverflow.com/users/5772882/ole-v-v). Please, if you put together the answer — I can’t promise to accept the answer, but will be happy to upvote it (and you can accept it yourself 12 hours later). – Ole V.V. Dec 31 '22 at 06:43

0 Answers0