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?
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".
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With