Thymeleaf Temporals and dates with Timezone

Viewed 4658

I've a Spring Boot Web application using Thymeleaf as View Layer and I need to show some dates with a specific Timezone (CET = UTC+1/UTC+2).

The server is set for UTC and all dates are stored in my DB as datetime in UTC format too and that's fine.

Now I want to show this dates on my HTML pages not in UTC format but in CET using Thymeleaf Temporals but it seem doesn't work.

The date object is a Java Instant.

The retrieved date from DB is (for example) 2021-02-17T16:18:21Z and is display like:

<div th:text="${#temporals.format(user.lastAccess, 'dd/MM/yyyy HH:mm')}"></div>
=> 17/02/2021 16:18

But I want to show it like:

17/02/2021 17:18

so I used:

or

<div th:text="${#temporals.format(user.lastAccess, 'dd/MM/yyyy HH:mm', new java.util.Locale('it', 'IT'))}"></div>

But the date is always displayed as UTC

17/02/2021 16:18

The Java8TimeDialect for Thymeleaf is correctly configured.

I'm using:

Spring Boot 2.2.4
Thymeleaf 3.0.11.RELEASE

What am I doing wrong?

Thanks.

3 Answers

There is no Thymeleaf temporals function which can perform that time zone conversion for you.

The Instant value needs to be given a time zone, not a locale (Java locales do not manipulate time zones in this way).

You can do what you need in Java:

Instant myInstant = Instant.parse("2021-02-17T16:18:00.00Z");
ZonedDateTime myZDT = myInstant.atZone(ZoneId.of("CET"));

Now, you can use the following Thymeleaf snippet:

<div th:text="${#temporals.format(myZDT, 'dd/MM/yyyy HH:mm')}"></div>

This will generate the following in a div:

17/02/2021 17:18

Now the time is displayed as 17:18 instead of 16:18.


UPDATE: Please be sure to read the comment from OleV.V. regarding deprecated abbreviations, including the note:

Three letter time zone abbreviations are deprecated and very often ambiguous, so don’t use ZoneId.of("CET"). Use a time zone ID like Europe/Vienna or Europe/San_Marino, so in the region/city format.

The following question may therefore be useful:

Where is the official list of zone names for java.time?

And a related code snippet:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for (String zone : zoneIds) {
    System.out.println(zone);
}

You need to convert the timestamp to date-time with the specified timezone. Given below are some of the ways to do so.

Using ZonedDateTime#withZoneSameInstant:

import java.time.ZoneId;
import java.time.ZonedDateTime;

class Main {
    public static void main(String[] args) {
        String timestamp = "2021-02-17T16:18:00.00Z";
        ZonedDateTime zdt = ZonedDateTime.parse(timestamp);
        ZonedDateTime zdtAtCet = zdt.withZoneSameInstant(ZoneId.of("Europe/Paris"));
        System.out.println(zdtAtCet);
    }
}

Now, you can use the corresponding object of OffsetDateTime in your code e.g.

<div th:text="${#temporals.format(zdtAtCet, 'dd/MM/yyyy HH:mm')}"></div>

ZonedDateTime has been designed to adjust the changes in the offset from UTC due to daylight savings automatically.

If you are going to deal with a fixed offset from UTC e.g. UTC+01:00, you can also use OffsetDateTime#withOffsetSameInstant e.g.

import java.time.OffsetDateTime;
import java.time.ZoneOffset;

class Main {
    public static void main(String[] args) {
        String timestamp = "2021-02-17T16:18:00.00Z";
        OffsetDateTime odt = OffsetDateTime.parse(timestamp);
        OffsetDateTime odtAtUtcPlus1 = odt.withOffsetSameInstant(ZoneOffset.of("+01:00"));
        OffsetDateTime odtAtUtcPlus2 = odt.withOffsetSameInstant(ZoneOffset.of("+02:00"));
        System.out.println(odtAtUtcPlus1);
        System.out.println(odtAtUtcPlus2);
    }
}

Output:

2021-02-17T17:18+01:00
2021-02-17T18:18+02:00

You can also use Instant#atZone and Instant#atOffset to do first and the second solution, mentioned above, respectively e.g.

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

class Main {
    public static void main(String[] args) {
        String timestamp = "2021-02-17T16:18:00.00Z";
        Instant instant = Instant.parse(timestamp);
        OffsetDateTime odtAtUtcPlus1 = instant.atOffset(ZoneOffset.of("+01:00"));
        ZonedDateTime zdtAtCet = instant.atZone(ZoneId.of("Europe/Paris"));
        System.out.println(odtAtUtcPlus1);
        System.out.println(zdtAtCet);
    }
}

Output:

2021-02-17T17:18+01:00
2021-02-17T17:18+01:00[Europe/Paris]

Learn more about the modern date-time API from Trail: Date Time.

Temporals are created automatically. It takes the system default timezone.

public Temporals(final Locale locale) {
    this(locale, ZoneId.systemDefault());
}

The creation process isn't really designed to be customizable, so the best way seems to be to change the system default ZoneId.

For me, the best way was to set the expected timezone in Docker container. In my case the date was already correct on the server. To propagate it to the container volumes could be used:

-v /etc/timezone:/etc/timezone:ro -v /etc/localtime:/etc/localtime:ro
Related