JDK-8226601 : TZ database in "vanguard" format support
  • Type: CSR
  • Component: core-libs
  • Sub-Component: java.time
  • Priority: P3
  • Status: Closed
  • Resolution: Withdrawn
  • Submitted: 2019-06-21
  • Updated: 2019-09-25
  • Resolved: 2019-07-22
Related Reports
CSR :  
Description
Summary
-------

Support the "vanguard" format of the TZ Database.

Problem
-------

The [IANA time zone database][1] has been producing three formats of the same release, i.e., vanguard, main, and rearguard. JDK relies on the rearguard format which does not use negative DST and start/end time within the transition day. It is anticipated that rearguard format is going to be obsolete.

Solution
--------

Support negative DST offset and transition time beyond the transition day, so that JDK can use vanguard/main format files. Although the savings are expressed in negative in the vanguard files, JDK translates them into positive savings by offsetting the GMT (standard) offset by that negative savings value. For example for the "Eire" rule (excerpt only relevant portion:

    # Rule  NAME    FROM    TO  TYPE    IN  ON  AT  SAVE    LETTER/S
        :
    Rule    Eire    1981    max -   Mar lastSun  1:00u  0   -
    Rule    Eire    1996    max -   Oct lastSun  1:00u  -1:00   -
    
    # Zone  NAME        GMTOFF  RULES   FORMAT  [UNTIL]
    Zone    Europe/Dublin   -0:25:00 -  LMT 1880 Aug  2
        :
    # The next line is for when negative SAVE values are used.
                  1:00   Eire    IST/GMT
    # These three lines are for when SAVE values are always nonnegative.
    #            1:00   -   IST 1971 Oct 31  2:00u
    #            0:00   GB-Eire GMT/IST 1996
    #            0:00   EU  GMT/IST

In the vanguard format, the GMT offset is one hour and one hour negative savings in winter, JDK interprets it as GMT offset is Z and no savigns in winter (same behavior as before).

The serialized form of `ZoneOffsetTransitionRule` needs to be modified to accommodate the `time` of the cutover beyond 0:00-24:00 so that it can differentiate old and new forms.

Specification
-------------

Add the following two methods in `java.time.zone.ZoneOffsetTransitionRule` class:

    /**
     * Obtains an instance defining the yearly rule to create transitions between two offsets. 
     * <p>
     * Applications should normally obtain an instance from {@link ZoneRules}.
     * This factory is only intended for use when creating {@link ZoneRules}.
     *
     * @param month  the month of the month-day of the first day of the cutover week, not null
     * @param dayOfMonthIndicator  the day of the month-day of the cutover week, positive if the week is that
     *  day or later, negative if the week is that day or earlier, counting from the last day of the month,
     *  from -28 to 31 excluding 0
     * @param dayOfWeek  the required day-of-week, null if the month-day should not be changed
     * @param time  the cutover time in the 'before' offset expressed using a duration to allow values outside the range 00:00 to 23:59, not null
     * @param timeDefnition  how to interpret the cutover
     * @param standardOffset  the standard offset in force at the cutover, not null
     * @param offsetBefore  the offset before the cutover, not null
     * @param offsetAfter  the offset after the cutover, not null
     * @return the rule, not null
     * @throws IllegalArgumentException if the day of month indicator is invalid
     * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight
     * @throws IllegalArgumentException if {@code time.getNano()} returns non-zero value
     * @since 14
     */
    public static ZoneOffsetTransitionRule of(
            Month month,
            int dayOfMonthIndicator,
            DayOfWeek dayOfWeek,
            Duration time, 
            TimeDefinition timeDefnition,
            ZoneOffset standardOffset,
            ZoneOffset offsetBefore,
            ZoneOffset offsetAfter)

     /**
      * Gets the cutover time in the 'before' offset expressed using a duration to allow values outside the range 00:00 to 23:59
      * <p>
      * The time is interpreted using the time definition.
      *
      * @return the local time of the transition, expressed in a duration, not null
      * @since 14
      */
     public Duration getLocalTimeDuration()

Mark these methods as deprecated, they are superseded by the above methods.

    /**
     * Obtains an instance defining the yearly rule to create transitions between two offsets. 
     * <p>
     * Applications should normally obtain an instance from {@link ZoneRules}.
     * This factory is only intended for use when creating {@link ZoneRules}.
     *
     * @param month  the month of the month-day of the first day of the cutover week, not null
     * @param dayOfMonthIndicator  the day of the month-day of the cutover week, positive if the week is that
     *  day or later, negative if the week is that day or earlier, counting from the last day of the month,
     *  from -28 to 31 excluding 0
     * @deprecated This method cannot handle transition ocurring outside of 00:00-24:00 time of
     *  the transition day. Use {@code of()} method that takes {@link java.time.Duration}.
     * @param dayOfWeek  the required day-of-week, null if the month-day should not be changed
     * @param time  the cutover time in the 'before' offset, not null
     * @param timeEndOfDay  whether the time is midnight at the end of day
     * @param timeDefnition  how to interpret the cutover
     * @param standardOffset  the standard offset in force at the cutover, not null
     * @param offsetBefore  the offset before the cutover, not null
     * @param offsetAfter  the offset after the cutover, not null 
     * @return the rule, not null
     * @throws IllegalArgumentException if the day of month indicator is invalid
     * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight
     * @throws IllegalArgumentException if {@code time.getNano()} returns non-zero value
     */
    @Deprecated(since="14", forRemoval=true)
    public static ZoneOffsetTransitionRule of(
            Month month,
            int dayOfMonthIndicator,
            DayOfWeek dayOfWeek,
            LocalTime time,
            boolean timeEndOfDay,
            TimeDefinition timeDefnition,
            ZoneOffset standardOffset,
            ZoneOffset offsetBefore,
            ZoneOffset offsetAfter)

    /**
     * Gets the local time of day of the transition which must be checked with
     * {@link #isMidnightEndOfDay()}.
     * <p>
     * The time is converted into an instant using the time definition.
     *
     * @deprecated This method cannot handle transition ocurring outside of 00:00-24:00 time of
     *  the transition day. Use {@link getLocalTimeDuration()} method instead.
     * @throws DateTimeException if the cut over time is outside of the transition day.
     * @return the local time of day of the transition, not null
     */
    @Deprecated(since="14", forRemoval=true)
    public LocalTime getLocalTime() 
    
    /**
     * Is the transition local time midnight at the end of day.
     * <p>
     * The transition may be represented as occurring at 24:00.
     *
     * @deprecated This method is superseded by {@link getLocalTimeDuration()}.
     * @return whether a local time of midnight is at the start or end of the day
     */
    @Deprecated(since="14", forRemoval=true)
    public boolean isMidnightEndOfDay()

Change the method description of `writeReplace()` method related to `timeByte` and `timeSecs` from:

    /**
     * Writes the object using a
     * <a href="{@docRoot}/serialized-form.html#java.time.zone.Ser">dedicated serialized form</a>.
     * @serialData
     * Refer to the serialized form of
     * <a href="{@docRoot}/serialized-form.html#java.time.zone.ZoneRules">ZoneRules.writeReplace</a>
     * for the encoding of epoch seconds and offsets.
     * <pre style="font-size:1.0em">{@code
     *
     *      out.writeByte(3);                // identifies a ZoneOffsetTransition
     *      final int timeSecs = (timeEndOfDay ? 86400 : time.toSecondOfDay());
     *      final int stdOffset = standardOffset.getTotalSeconds(); 
     *      final int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset;
     *      final int afterDiff = offsetAfter.getTotalSeconds() - stdOffset;
     *      final int timeByte = (timeSecs % 3600 == 0 ? (timeEndOfDay ? 24 : time.getHour()) : 31);
     *      final int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255);
     *      final int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3);
     *      final int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3);  
     *      final int dowByte = (dow == null ? 0 : dow.getValue());
     *      int b = (month.getValue() << 28) +          // 4 bits
     *              ((dom + 32) << 22) +                // 6 bits
     *              (dowByte << 19) +                   // 3 bits
     *              (timeByte << 14) +                  // 5 bits
     *              (timeDefinition.ordinal() << 12) +  // 2 bits
     *              (stdOffsetByte << 4) +              // 8 bits
     *              (beforeByte << 2) +                 // 2 bits
     *              afterByte;                          // 2 bits
     *      out.writeInt(b); 
     *      if (timeByte == 31) {
     *          out.writeInt(timeSecs);                          
     *      } 
     *      if (stdOffsetByte == 255) {
     *          out.writeInt(stdOffset);
     *      } 
     *      if (beforeByte == 3) {
     *          out.writeInt(offsetBefore.getTotalSeconds());
     *      } 
     *      if (afterByte == 3) {
     *          out.writeInt(offsetAfter.getTotalSeconds());
     *      } 
     * }
     * </pre> 
     *
     * @return the replacing object, not null
     */

to:

    /**
     * Writes the object using a
     * <a href="{@docRoot}/serialized-form.html#java.time.zone.Ser">dedicated serialized form</a>.
     * @serialData
     * Refer to the serialized form of
     * <a href="{@docRoot}/serialized-form.html#java.time.zone.ZoneRules">ZoneRules.writeReplace</a>
     * for the encoding of epoch seconds and offsets.
     * <pre style="font-size:1.0em">{@code
     *
     *      out.writeByte(4);                // identifies a ZoneOffsetTransitionRule, version 2.
     *      final long timeSecs = time.toSeconds();
     *      final int stdOffset = standardOffset.getTotalSeconds();
     *      final int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset;
     *      final int afterDiff = offsetAfter.getTotalSeconds() - stdOffset;
    
     *      final int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255);
     *      final int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3);
     *      final int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3);
     *      final int dowByte = (dow == null ? 0 : dow.getValue());
     *      int b = (month.getValue() << 28) +          // 4 bits
     *              ((dom + 32) << 22) +                // 6 bits
     *              (dowByte << 19) +                   // 3 bits
     *              (timeByte << 14) +                  // 5 bits
     *              (timeDefinition.ordinal() << 12) +  // 2 bits
     *              (stdOffsetByte << 4) +              // 8 bits
     *              (beforeByte << 2) +                 // 2 bits
     *              afterByte;                          // 2 bits
     *      out.writeInt(b);
     *      out.writeLong(timeSecs);
    
    
     *      if (stdOffsetByte == 255) {
     *          out.writeInt(stdOffset);
     *      }
     *      if (beforeByte == 3) {
     *          out.writeInt(offsetBefore.getTotalSeconds());
     *      }
     *      if (afterByte == 3) {
     *          out.writeInt(offsetAfter.getTotalSeconds());
     *      }
     * }
     * </pre>
     *
     * @return the replacing object, not null
     */

Make the following changes in `java.util.SimpleTimeZone` class:

Parameter description of `startTime` in its constructors, and in `setStartRule()` methods from:

    The daylight saving time starting time in local wall clock
    time, which is local standard time in this case.

to:

    The daylight saving time starting time in local wall clock
    time (in milliseconds, can be outside 00:00-24:00), which is local
    standard time in this case.

And similar changes should be made to the methods that take `endTime`, i.e. from:

    The daylight saving time ending time in local wall clock
    time, which is local standard time in this case.

to:

    The daylight saving time ending time in local wall clock (in milliseconds, can be outside of 00:00-24:00), which is local
    standard time in this case.

  [1]: https://www.iana.org/time-zones


Comments
Withdrawing this CSR, as it would make the resulting tzdb.dat format incompatible to the previously released JDKs. It would make it difficult for TZupdater to accommodate this change.
22-07-2019