JVM: As time goes by


I just want to parse a timestamp on the JVM

If you are in a hurry and just want to know which JVM (Java/Kotlin) library can parse which date formats look –>here<– for an overview. Alternatively, read on when you want to know why this page exists…

But Why?

I have been developing software on the JVM platform roughly 20 years now. Every now and then, I need to parse a date from its text representation into the seconds since epoch or just to have it as an object to do some date arithmetics. And I can’t remember how to do it for the life of me. Maybe it’s my age, maybe it’s the various approaches of date parsing since Java SE 1.4, or I just have a rare form of dyslexia that interferes with my brain when I try to remember the Java/Kotlin date API.

To solve this problem once and for all, I did what any respectable Java developer would do, and overengineered a solution for this problem ;-).

We can use the work of Iain MacDonald who listed all possible RFC 3339 and ISO 8601 formats with a tiring precision as a starting point.

From there we can extract a wide range of interesting datetime representations

2024-05-16T15:22:07Z
2024-05-16T15:22:07.5Z
2024-05-16T15:22:07.53Z
2024-05-16T15:22:07.534Z
2024-05-16T15:22:07.534635Z
2024-05-16t15:22:07z
2024-05-16t15:22:07.534z
2024-05-16T17:22:07+02:00
[...]

That we just need to iterate and feed it into all JVM based parsers that we can find

For example java.time.Instant

class Instant : Parser {
    override fun parse(timeFormat: TimeFormat) =
        listOf(parseAndBenchmark("java.time.Instant.parse(String)", timeFormat) {
            Instant.parse(timeFormat.now).epochSecond
        })
}

or the more interesting java.time.format.DateTimeFormatter

class DateTimeFormatter : Parser {
    override fun parse(timeFormat: TimeFormat) = listOf(
        "DateTimeFormatter.BASIC_ISO_DATE.parse(String)" to DateTimeFormatter.BASIC_ISO_DATE,
        "DateTimeFormatter.ISO_LOCAL_DATE.parse(String)" to DateTimeFormatter.ISO_LOCAL_DATE,
        "DateTimeFormatter.ISO_OFFSET_DATE.parse(String)" to DateTimeFormatter.ISO_OFFSET_DATE,
        "DateTimeFormatter.ISO_DATE.parse(String)" to DateTimeFormatter.ISO_DATE,
        "DateTimeFormatter.ISO_LOCAL_TIME.parse(String)" to DateTimeFormatter.ISO_LOCAL_TIME,
        "DateTimeFormatter.ISO_OFFSET_TIME.parse(String)" to DateTimeFormatter.ISO_OFFSET_TIME,
        "DateTimeFormatter.ISO_TIME.parse(String)" to DateTimeFormatter.ISO_TIME,
        "DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse(String)" to DateTimeFormatter.ISO_LOCAL_DATE_TIME,
        "DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(String)" to DateTimeFormatter.ISO_OFFSET_DATE_TIME,
        "DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(String)" to DateTimeFormatter.ISO_ZONED_DATE_TIME,
        "DateTimeFormatter.ISO_DATE_TIME.parse(String)" to DateTimeFormatter.ISO_DATE_TIME,
        "DateTimeFormatter.ISO_ORDINAL_DATE.parse(String)" to DateTimeFormatter.ISO_ORDINAL_DATE,
        "DateTimeFormatter.ISO_WEEK_DATE.parse(String)" to DateTimeFormatter.ISO_WEEK_DATE,
        "DateTimeFormatter.ISO_INSTANT.parse(String)" to DateTimeFormatter.ISO_INSTANT,
        "DateTimeFormatter.RFC_1123_DATE_TIME.parse(String)" to DateTimeFormatter.RFC_1123_DATE_TIME,
    ).map {
        parseAndBenchmark(it.first, timeFormat) {
            Instant.from(it.second.parse(timeFormat.now)).epochSecond
        }
    }
}

after that we just need some glue code to gather the results and generate a nice overview page. The parseAndBenchmark method also measure the execution time for each parse method with 100.000 iterations, and records an average in nanoseconds that is shown on the results page, and as a heatmap for each method.

JVM as times goes by

If you want to experiment on your own, as always the full source code is available here

Results and Findings

Looking at the results there were some interesting findings

For the (relatively common) formats:

  • 2024-05-16T15:22:07Z
  • 2024-05-16T15:22:07.5Z
  • 2024-05-16T15:22:07.53Z
  • 2024-05-16T15:22:07.534Z
  • 2024-05-16T15:22:07.534635Z
  • 2024-05-16t15:22:07z
  • 2024-05-16t15:22:07.534z
  • 2024-05-16T17:22:07+02:00
  • 2024-05-16T17:22:07.534+02:00
  • 2024-05-16T17:22:07.534635+02:00
  • 2024-05-16T15:22:07-00:00
  • 2024-05-16T15:22:07.534-00:00
  • 2024-05-17T00:07:07+08:45
  • 2024-05-16T15:22:07+00:00
  • 2024-05-16T15:22:07.534+00:00
  • 2024-05-16T17:22:07+02
  • 2024-05-16T17:22:07.5+02
  • 2024-05-16T17:22:07.53+02
  • 2024-05-16T17:22:07.534+02
  • 2024-05-16T17:22:07.534635+02
  • 2024-05-16T17:22:07.5+02:00
  • 2024-05-16T17:22:07.53+02:00
  • 2024-05-16T17:22+02:00
  • 2024-05-16T17:22+02
  • 2024-05-16T15:22Z

The functions

  • DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(String)
  • DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(String)

are able to parse all of them. Second place goes to

  • java.time.Instant.parse(String)
  • DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(String)
  • DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(String)
  • DateTimeFormatter.ISO_DATE_TIME.parse(String)
  • DateTimeFormatter.ISO_INSTANT.parse(String)

which also are able to parse the majority. Interestingly org.joda.time.LocalDateTime.parse(String) fails at this completely. On the other hand for

  • 2024-05-16T17:22:07
  • 2024-05-16T17:22:07.5
  • 2024-05-16T17:22:07.53
  • 2024-05-16T17:22:07,534
  • 2024-05-16T17:22:07.534
  • 2024-05-16T17:22:07,534635
  • 2024-05-16T17:22:07.534635
  • 2024-137T17:22:07
  • 2024-137T17:22:07.5
  • 2024-137T17:22:07.53
  • 2024-137T17:22:07,534
  • 2024-137T17:22:07.534
  • 2024-137T17:22:07,534635
  • 2024-137T17:22:07.534635

org.joda.time.LocalDateTime.parse(String) and java.time.LocalDateTime.parse(String) excel and all other methods fail. Joda also is better at parsing partials formats like

  • 2024-05-16T17
  • 2024-05-16T17,3
  • 2024-05-16T17.3
  • 2024-05-16T17:22
  • 2024-05-16T17:22,1
  • 2024-05-16T17:22.1

Performance wise org.joda.time.LocalDateTime.parse(String) and java.time.LocalDateTime.parse(String) are noticeably slower in the non-happy path

JVM as times goes by

DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse(String) and DateTimeFormatter.ISO_DATE_TIME.parse(String) have some heavy outliers for failed parse attempts.

JVM as times goes by

In general for most parse attempts the worst case time is roughly double the parse time when the parse succeeds.

The benchmark is not absolute because it depends on the machine where it was executed, but it should give a good indication of relative timings.

Of course, you could just specify the format, and instruct the parser to parse it like this DateTimeFormatter.ofPattern("%Y-%M-%Dt%h:%m:%sz").parse(String) but apart from only being able to parse this single format, it’s only half the fun and I would not be able to rant about JVM date parsing :-)

jvm  java 

See also

Let's work together!