Skip to content

Timezone the binary search implementation doesn't pivot on string length correctly with trailing literals. #386

@jmax01

Description

@jmax01

Enviroment

  • Joda-Time 2.9.2 - 2.9.4
  • TimeZone.getDefault(): sun.util.calendar.ZoneInfo[id="America/Chicago",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/Chicago,offset=-21600000,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]]
  • DateTimeZone.getDefault(): America/Chicago

Problem description

It appears that #332 actually made parsing worse as it fails to handle literals after ZoneIds.

2.9.1

2.9.1 is able to round trip (ZonedDateTime.toString()->DateTime->ZonedDateTime) 600 of 617 ZoneIds using a formatter designed to mimic ZonedDateTime's toString scheme (it is defined in the test below, it may not be perfect but worked for ~97% of ZoneIds).

Total Count: 617
Translation success Count: 600
Translation failed Count: 17

The failed ones are listed below:

  • Etc/GMT Invalid format: "2016-07-13T19:26:56.352Z[Etc/GMT]" is malformed at "[Etc/GMT]"
  • Etc/GMT+1 Invalid format: "2016-07-13T18:26:56.367-01:00[Etc/GMT+1]" is malformed at "[Etc/GMT+1]"
  • Etc/GMT-1 Invalid format: "2016-07-13T20:26:56.367+01:00[Etc/GMT-1]" is malformed at "[Etc/GMT-1]"
  • GMT Invalid format: "2016-07-13T19:26:56.461Z[GMT]" is malformed at "[GMT]"
  • SystemV/AST4 Invalid format: "2016-07-13T15:26:56.508-04:00[SystemV/AST4]" is malformed at "[SystemV/AST4]"
  • SystemV/AST4ADT Invalid format: "2016-07-13T16:26:56.508-03:00[SystemV/AST4ADT]" is malformed at "[SystemV/AST4ADT]"
  • SystemV/CST6 Invalid format: "2016-07-13T13:26:56.508-06:00[SystemV/CST6]" is malformed at "[SystemV/CST6]"
  • SystemV/CST6CDT Invalid format: "2016-07-13T14:26:56.508-05:00[SystemV/CST6CDT]" is malformed at "[SystemV/CST6CDT]"
  • SystemV/EST5 Invalid format: "2016-07-13T14:26:56.508-05:00[SystemV/EST5]" is malformed at "[SystemV/EST5]"
  • SystemV/EST5EDT Invalid format: "2016-07-13T15:26:56.508-04:00[SystemV/EST5EDT]" is malformed at "[SystemV/EST5EDT]"
  • SystemV/HST10 Invalid format: "2016-07-13T09:26:56.508-10:00[SystemV/HST10]" is malformed at "[SystemV/HST10]"
  • SystemV/MST7 Invalid format: "2016-07-13T12:26:56.508-07:00[SystemV/MST7]" is malformed at "[SystemV/MST7]"
  • SystemV/MST7MDT Invalid format: "2016-07-13T13:26:56.508-06:00[SystemV/MST7MDT]" is malformed at "[SystemV/MST7MDT]"
  • SystemV/PST8 Invalid format: "2016-07-13T11:26:56.508-08:00[SystemV/PST8]" is malformed at "[SystemV/PST8]"
  • SystemV/PST8PDT Invalid format: "2016-07-13T12:26:56.508-07:00[SystemV/PST8PDT]" is malformed at "[SystemV/PST8PDT]"
  • SystemV/YST9 Invalid format: "2016-07-13T10:26:56.508-09:00[SystemV/YST9]" is malformed at "[SystemV/YST9]"
  • SystemV/YST9YDT Invalid format: "2016-07-13T11:26:56.508-08:00[SystemV/YST9YDT]" is malformed at "[SystemV/YST9YDT]"

2.9.2 - 2.9.4

Total Count: 617
Translation success Count: 3
Translation failed Count: 614

The 614 fail with errors like this one:
Invalid format: "2016-07-13T23:13:04.160+03:00[Africa/Addis_Ababa]" is malformed at "[Africa/Addis_Ababa]"

Test case

    @Test
    public void test() {
        class Results {
            final DateTimeFormatter ZONED_ISO_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder().appendYear(4, 4)
                .appendLiteral('-')
                .appendMonthOfYear(2)
                .appendLiteral('-')
                .appendDayOfMonth(2)
                .appendLiteral('T')
                .appendHourOfDay(2)
                .appendLiteral(':')
                .appendMinuteOfHour(2)
                .appendLiteral(':')
                .appendSecondOfMinute(2)
                .appendLiteral('.')
                .appendFractionOfSecond(0, 9)
                .appendTimeZoneOffset("Z", true, 2, 2)
                .appendOptional(new DateTimeFormatterBuilder().appendLiteral('[')
                    .appendTimeZoneId()
                    .appendLiteral(']')
                    .toParser())
                .toFormatter();

            final DateTimeFormatter ZONED_ISO_DATE_TIME_PRINTER = new DateTimeFormatterBuilder().appendYear(4, 4)
                .appendLiteral('-')
                .appendMonthOfYear(2)
                .appendLiteral('-')
                .appendDayOfMonth(2)
                .appendLiteral('T')
                .appendHourOfDay(2)
                .appendLiteral(':')
                .appendMinuteOfHour(2)
                .appendLiteral(':')
                .appendSecondOfMinute(2)
                .appendLiteral('.')
                .appendFractionOfSecond(0, 9)
                .appendTimeZoneOffset("Z", true, 2, 2)
                .appendLiteral('[')
                .appendTimeZoneId()
                .appendLiteral(']')
                .toFormatter();

            final ZoneId zoneId;

            final ZonedDateTime zonedDateTime;

            final String zonedDateTimeAsString;

            DateTime dateTime;

            String dateTimeAsString;

            String toDateTimeError;

            ZonedDateTime zonedDateTimeFromDateTime;

            String toZonedDateTimeError;

            Boolean roundTripEquals;

            Boolean roundTripIsEquals;

            Results(ZoneId zoneId) {
                this.zoneId = zoneId;
                this.zonedDateTime = ZonedDateTime.now(zoneId);
                this.zonedDateTimeAsString = this.zonedDateTime.toString();
                this.toDateTime();
                this.fromDateTime();
            }

            private void fromDateTime() {

                if (this.dateTime != null) {

                    try {
                        this.zonedDateTimeFromDateTime = ZonedDateTime.parse(this.dateTimeAsString);
                        this.roundTripEquals = this.zonedDateTime.equals(this.zonedDateTimeFromDateTime);
                        this.roundTripIsEquals = this.zonedDateTime.isEqual(this.zonedDateTimeFromDateTime);

                    } catch (Exception e) {
                        this.toZonedDateTimeError = e.getMessage();
                    }
                }
            }

            void toDateTime() {
                try {
                    this.dateTime = this.ZONED_ISO_DATE_TIME_FORMATTER.parseDateTime(this.zonedDateTimeAsString);
                    this.dateTimeAsString = this.dateTime.toString(this.ZONED_ISO_DATE_TIME_PRINTER);
                } catch (Exception e) {
                    this.toDateTimeError = e.getMessage();
                }
            }

            public boolean failedToTranslation() {
                return !(this.toDateTimeError == null && this.toZonedDateTimeError == null);
            }

            public boolean equalityFailed() {
                return !this.failedToTranslation() && (!(this.roundTripEquals || this.roundTripIsEquals));
            }

            @Override
            public String toString() {
                return new StringBuilder().append(this.zoneId)
                    .append('-')
                    .append('[')
                    .append(this.failedToTranslation() ? "translation: failed" : "translation: passed")
                    .append(this.roundTripEquals != null ? this.equalityFailed() ? ", equality: failed"
                            : ", equality: passed" : "")
                    .append(']')
                    .append("\n\n\t")
                    .append("ZonedDateTime: ")
                    .append(this.zonedDateTimeAsString)
                    .append("\n\t")
                    .append(Optional.ofNullable(this.dateTime)
                        .map(dt -> "DateTime: " + this.dateTime)
                        .orElseGet(() -> "ZoneDateTime to DateTime error: " + this.toDateTimeError))
                    .append(Optional.ofNullable(this.dateTimeAsString)
                        .map(dts -> "\n\tDateTime As ISO String: " + dts)
                        .orElse(""))
                    .append(Optional.ofNullable(this.zonedDateTimeFromDateTime)
                        .map(rd -> "\n\tZonedDateTime from DateTime: " + rd)
                        .orElse(""))
                    .append(Optional.ofNullable(this.roundTripEquals)
                        .map(b -> "\n\tRound Trip ZonedDateTime equals: " + b)
                        .orElse(""))
                    .append(Optional.ofNullable(this.roundTripIsEquals)
                        .map(b -> "\n\tRound Trip ZonedDateTime isEquals: " + b)
                        .orElse(""))
                    .append(Optional.ofNullable(this.toZonedDateTimeError)
                        .map(err -> "\n\tDateTime to ZonedDateTime error: " + err)
                        .orElse(""))
                    .append("\n\n")
                    .toString();

            }
        }

        List<Results> results = Stream.of(TimeZone.getAvailableIDs())
            .map(TimeZone::getTimeZone)
            .map(TimeZone::toZoneId)
            .map(Results::new)
            .collect(Collectors.toList());

        System.out.println("Total Count: " + results.stream()
            .count());

        System.out.println("Translation success Count: " + results.stream()
            .filter(r -> !r.failedToTranslation())
            .count());

        System.out.println("Translation failed Count: " + results.stream()
            .filter(r -> r.failedToTranslation())
            .count());

        System.out.println("Equality success Count: " + results.stream()
            .filter(r -> !r.equalityFailed() && !r.failedToTranslation())
            .count());

        System.out.println("Equality failed Count: " + results.stream()
            .filter(r -> r.equalityFailed())
            .count());

        System.out.println("Failed to translate:\n");
        results.stream()
            .filter(r -> r.failedToTranslation())
            .forEach(System.out::println);

        System.out.println("Successfully translated:\n");
        results.stream()
            .filter(r -> !r.failedToTranslation())
            .forEach(System.out::println);

        System.out.println("Failed to equality:\n");
        results.stream()
            .filter(r -> r.equalityFailed())
            .forEach(System.out::println);

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions