Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zone parsing for GMT0

Tags:

java

java-time

I have GMT0 as the default timezone in a system and it causes problem when I'm serializing it and deserializing it just after that.

System.setProperty("user.timezone","GMT0");
DateTimeFormatter zoneFormatter = new DateTimeFormatterBuilder()
            .appendZoneOrOffsetId()
            .toFormatter();
String formatted = zoneFormatter.format(ZonedDateTime.now());
System.out.println(formatted);
System.out.println(zoneFormatter.parse(formatted));

The first System.out.println prints GMT0 while the second throws the following problem.

Exception in thread "main" java.time.format.DateTimeParseException: Text 'GMT0' could not be parsed, unparsed text found at index 3
    at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1952)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1777)

is it an expected behavior? Is there a way to do that in a safe manner?

like image 802
Nicolas Henneaux Avatar asked Aug 23 '18 08:08

Nicolas Henneaux


1 Answers

As you noticed in the comments, that's a bug in JDK 8, fixed only in versions >= 9.

If you're using JDK 8 and can't/won't upgrade it, there's a workaround. You can treat the "GMT" part as a literal (the text "GMT" itself) and consider the 0 as the offset seconds, using the respective ChronoField:

DateTimeFormatter zoneParser = new DateTimeFormatterBuilder()
    // text "GMT"
    .appendLiteral("GMT")
    // offset seconds
    .appendValue(ChronoField.OFFSET_SECONDS)
    .toFormatter();
System.out.println(zoneParser.parse("GMT0"));

Keep in mind that this works only for offset zero. For any other values (such as "GMT2" or "GMT-2") this won't work, because it'll consider the values "2" and "-2" as seconds, but they actually mean "hours".


In case you need to parse all offset values in this format ("GMTn")

Well, JDK 8 also can't handle one-digit offsets, and it always requires a signal, either + or -. So "GMT2" and "GMT-2" won't work with the current API.

There's a harder alternative, though: create your own TemporalField, representing "offset hours". All the details about how to do it are in the documentation, and I'm not sure if all methods are correctly implemented - I'm just sure about isSupportedBy, getFrom and adjustInto, the others maybe need some improvement/adjustment:

public class OffsetHours implements TemporalField {

    @Override
    public TemporalUnit getBaseUnit() {
        return ChronoUnit.HOURS;
    }

    @Override
    public TemporalUnit getRangeUnit() {
        return ChronoUnit.FOREVER;
    }

    @Override
    public ValueRange range() {
        return ValueRange.of(ZoneOffset.MIN.getTotalSeconds() / 3600, ZoneOffset.MAX.getTotalSeconds() / 3600);
    }

    @Override
    public boolean isDateBased() {
        return false;
    }

    @Override
    public boolean isTimeBased() {
        return true;
    }

    @Override
    public boolean isSupportedBy(TemporalAccessor temporal) {
        return temporal.isSupported(ChronoField.OFFSET_SECONDS);
    }

    @Override
    public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
        ValueRange rangeInSecs = temporal.range(ChronoField.OFFSET_SECONDS);
        return ValueRange.of(rangeInSecs.getMinimum() / 3600, rangeInSecs.getMaximum() / 3600);
    }

    @Override
    public long getFrom(TemporalAccessor temporal) {
        return temporal.getLong(ChronoField.OFFSET_SECONDS) / 3600;
    }

    @Override
    public <R extends Temporal> R adjustInto(R temporal, long newValue) {
        return (R) temporal.with(ChronoField.OFFSET_SECONDS, newValue * 3600);
    }
}

Now you create an instance of this field and use it in your parser:

// the new field
OffsetHours offsetHoursField = new OffsetHours();
DateTimeFormatter zoneParser = new DateTimeFormatterBuilder()
    // text "GMT"
    .appendLiteral("GMT")
    // offset hours
    .appendValue(offsetHoursField)
    .toFormatter();

I also recommend creating a TemporalQuery to convert the parsed result to a ZoneOffset:

// get hours and create offset from hours value
TemporalQuery<ZoneOffset> getOffsetFromHours = temporal -> {
    return ZoneOffset.ofHours((int) temporal.getLong(offsetHoursField));
};

Now you can parse it:

ZoneOffset offsetZero = zoneParser.parse("GMT0", getOffsetFromHours);
ZoneOffset offsetTwo = zoneParser.parse("GMT2", getOffsetFromHours);
ZoneOffset offsetMinusTwo = zoneParser.parse("GMT-2", getOffsetFromHours);

You can improve it letting the OffsetHours field to be a static instance (or maybe an enum), so you don't need to create it all the time.

like image 168
zod124 Avatar answered Nov 08 '22 02:11

zod124