Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this quick tutorial, we’ll learn how to parse representations of dates from a Unix timestamp. Unix time is the number of seconds elapsed since January 1, 1970. However, a timestamp can represent time down to nanosecond precision. So, we’ll see the tools available and create a method to convert timestamps of any range to a Java object.

2. Old Way (Before Java 8)

Before Java 8, our simplest options were Date and Calendar. The Date class has a constructor that directly accepts a timestamp in milliseconds:

public static Date dateFrom(long input) {
    return new Date(input);
}

With Calendar, we have to call setTimeInMillis() after getInstance():

public static Calendar calendarFrom(long input) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(input);
    return calendar;
}

In other words, we must know if our input is in seconds, nanoseconds, or any other precision in between. Then, we have to convert our timestamp to milliseconds manually.

3. New Way (Java 8+)

Java 8 introduced Instant. This class has utility methods to create instances from seconds and milliseconds. Also, one of them accepts a nanoseconds adjustment parameter:

Instant.ofEpochSecond(seconds, nanos);

But we still must know in advance the precision of our timestamp. So, for example, some calculations are needed if we know our timestamp is in nanoseconds:

public static Instant fromNanos(long input) {
    long seconds = input / 1_000_000_000;
    long nanos = input % 1_000_000_000;

    return Instant.ofEpochSecond(seconds, nanos);
}

First, we divide our timestamp by one billion to get the seconds. Then, we use its remainder to get the part after seconds.

4. Universal Solution With Instant

To avoid extra work, let’s create a method that can convert any input to milliseconds, which most classes can parse. Firstly, we check in what range our timestamp is. Then, we perform calculations to extract the milliseconds. Moreover, we’ll use scientific notations to make our conditions more readable.

Also, remember that timestamps are signed, so we have to check both the positive and negative ranges (negative timestamps mean they’re counted backward from 1970).

So, let’s start by checking if our input is in nanoseconds:

private static long millis(long timestamp) {
    if (millis >= 1E16 || millis <= -1E16) {
        return timestamp / 1_000_000;
    }

    // next range checks
}

First, we check if it’s in the 1E16 range, which is one followed by 16 zeroes. Negative values represent dates before 1970, so we also have to check them. Then, we divide our value by one million to get to milliseconds.

Similarly, microseconds are in the 1E14 range. This time, we divide by one thousand:

if (timestamp >= 1E14 || timestamp <= -1E14) {
    return timestamp / 1_000;
}

We don’t need to change anything when our value is in the 1E11 to -3E10 range. That means our input is already in milliseconds precision:

if (timestamp >= 1E11 || timestamp <= -3E10) {
    return timestamp;
}

Finally, if our input isn’t any of these ranges, then it must be in seconds, so we need to convert this to milliseconds:

return timestamp * 1_000;

4.1. Normalizing Input for Instant

Now, let’s create a method that returns an Instant from input in any precision with Instant.ofEpochMilli():

public static Instant fromTimestamp(long input) {
    return Instant.ofEpochMilli(millis(input));
}

Note that every time we divide or multiply values, precision is lost.

4.2. Local Time With LocalDateTime

An Instant represents a moment in time. But, without a time zone, it’s not easily readable, as it depends on our location in the world. So, let’s create a method to generate a local time representation. We’ll use the UTC to avoid different results in our tests:

public static LocalDateTime localTimeUtc(Instant instant) {
    return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
}

Now, we can test how using wrong precisions can result in entirely different dates when methods expect specific formats. First, let’s pass a timestamp in nanoseconds we already know the correct date for, but convert it to microseconds and use the fromNanos() method we created earlier:

@Test
void givenWrongPrecision_whenInstantFromNanos_thenUnexpectedTime() {
    long microseconds = 1660663532747420283l / 1000;
    Instant instant = fromNanos(microseconds);
    String expectedTime = "2022-08-16T15:25:32";

    LocalDateTime time = localTimeUtc(instant);
    assertThat(!time.toString().startsWith(expectedTime));
    assertEquals("1970-01-20T05:17:43.532747420", time.toString());
}

This problem won’t happen when we use the fromTimestamp() method we created in the previous subsection:

@Test
void givenMicroseconds_whenInstantFromTimestamp_thenLocalTimeMatches() {
    long microseconds = 1660663532747420283l / 1000;

    Instant instant = fromTimestamp(microseconds);
    String expectedTime = "2022-08-16T15:25:32";

    LocalDateTime time = localTimeUtc(instant);
    assertThat(time.toString().startsWith(expectedTime));
}

5. Conclusion

In this article, we learned how to convert timestamps with core Java classes. Then, we saw how they can have different levels of precision and how that affects our results. Lastly, we created a simple way to normalize our input and get consistent results.

And, as always, the source code is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!