Parsing LocalTime with leap second

1k Views Asked by At

I am trying to understand how to build a custom DateTimeFormatter for my application. I basically need to handle time that are written like this "HHMMSS.FFFFFF".

I was able to get 99% of it using:

import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MICRO_OF_SECOND;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
public static final DateTimeFormatter MY_TIME;
static {
    MY_TIME = new DateTimeFormatterBuilder()
            .appendValue(HOUR_OF_DAY, 2)
            .appendValue(MINUTE_OF_HOUR, 2)
            .optionalStart()
            .appendValue(SECOND_OF_MINUTE, 2)
            .optionalStart()
            .appendFraction(MICRO_OF_SECOND, 0, 6, true)
            .toFormatter().withResolverStyle(ResolverStyle.STRICT);
}

I can process inputs just fine:

String text = "101530";
LocalTime lt = LocalTime.parse(text, MY_TIME);

or even

String text = "070907.0705";
LocalTime lt = LocalTime.parse(text, MY_TIME);

and

String text = "0000";
LocalTime lt = LocalTime.parse(text, MY_TIME);

But for some reason I cannot make sense of the API for handling leap second so the following always fails for me:

String text = "235960";
LocalTime lt = LocalTime.parse(text, MY_TIME);

How should I build my DateTimeFormatterBuilder so that leap second is handled ?


Update: I really like the ResolverStyle.STRICT, since it reject invalid inputs such as:

  • "251213" or,
  • "126100"

So I cannot use ResolverStyle.LENIENT in this case, I simply want the extra special case for leap second.

4

There are 4 best solutions below

0
On BEST ANSWER

Since this is of limited use for my users (editing of invalid time), I can handle this special case using simply:

if (text.length() >= 6 && "60".equals(text.substring(4, 6))) {
    String newText = text.substring(0, 4) + "59" + text.substring(6);
    return LocalTime.parse(newText, MY_TIME);
}
return LocalTime.parse(text, MY_TIME);

It seems this is a generally accepted hack:

0
On

Relevant description from java.time.Instant JavaDoc:

[...] this Java API defines its own time-scale, the Java Time-Scale.
...
Implementations of the Java time-scale using the JSR-310 API are not required to provide any clock that is sub-second accurate, or that progresses monotonically or smoothly. Implementations are therefore not required to actually perform the UTC-SLS slew or to otherwise be aware of leap seconds.
...
The Java time-scale is used for all date-time classes. This includes Instant, LocalDate, LocalTime, OffsetDateTime, ZonedDateTime and Duration.

In short you should not expect java.time API to be aware of leap seconds.

0
On

A local clock time showing leap second does not make much sense without also showing the calendar date and the timezone. Otherwise you don't know if the string to be parsed is really valid (the java.time-package will not help you to validate it) or is just meant in a lenient way (i.e. parsing "60" as next second).

About the leap second capabilities of java.time:

java.time would indeed tolerate any dummy date in combination with a leap second although this is wrong in most cases. And the DateTimeFormatter only accepts the UTC-offset zero but not expressions like "2012-07-01T08:59:60+0900".

Furthermore: java.time.Instant cannot store the leap second information but throws it away. You can only query the parser for a possible leap second flag. And then it is up to you what you want to do with this information.

Conclusion: This approach is fine for you if you want to ignore the leap second information but otherwise tolerate it in input.

Alternatives:

If you are really interested in parsing, validating and evaluating possible leap seconds then I recommend to use my library Time4J. But it does require a valid calendar date (leap seconds are inserted only on very few dates).

     ChronoFormatter<Moment> formatter = 
         ChronoFormatter.ofMomentPattern( 
             "uuuu-MM-dd'T'HH:mm:ss XXX", 
             PatternType.CLDR, 
             Locale.ROOT, 
             ZonalOffset.UTC 
         )
     Moment m = formatter.parse("2012-07-01T08:59:60+09:00");

     // this conversion throws away the leap second
     Instant i = m.toTemporalAccessor();

For more informations, see also my article on DZone.

2
On

Leap second handling only works for appendInstant: see DateTimeFormatter:parsedLeapSecond

Instant parsing handles the special "leap second" time of '23:59:60'. Leap seconds occur at '23:59:60' in the UTC time-zone, but at other local times in different time-zones. To avoid this potential ambiguity, the handling of leap-seconds is limited to DateTimeFormatterBuilder.appendInstant(), as that method always parses the instant with the UTC zone offset.