Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SimpleDateFormat with Timezone set gets correct value but wrong zone

I have a simple test in a Spring application, which has default timezone set to UTC:

@SpringBootApplication
public class MemberIntegrationApp {

    @Autowired
    private TimeZoneProperties timeZoneProperties;

    @PostConstruct
    void started() {
        TimeZone.setDefault(TimeZone.getTimeZone(timeZoneProperties.getAppDefault()));  // which is UTC
    }

    public static void main(String[] args) {
        SpringApplication.run(MemberIntegrationApp.class, args);
    }

}

And, this simple test: (The test class is annotated with @SpringBootTest to load the configuration in main class and @SpringRunner is applied, too)

/**
 * Test the effect of setting timezone
 */
@Test
public void testTimezoneSettingOnSimpleDateFormat() throws ParseException {
    SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String d = "2018-08-08 12:34:56";
    log.info("Trying to parse the date string: {}", d);
    Date result = f.parse(d);
    log.info("The result should be 12:34 UTC: {}", result);

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    result = f.parse(d);
    log.info("The result should be 12:34 UTC: {}", result);

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 CEST: {}", result);
    log.info("Now the offset(depre): {}", result.getTimezoneOffset());
}

I have output:

Trying to parse the date string: 2018-08-08 12:34:56
The result should be 12:34 UTC: Wed Aug 08 12:34:56 UTC 2018
The result should be 12:34 UTC: Wed Aug 08 12:34:56 UTC 2018
The result should be 10:34 CEST: Wed Aug 08 10:34:56 UTC 2018
Now the offset(depre): 0

Now, why the fourth line has the value correct, but the timezone is wrong? It should be Europe/Madrid. And the offset(which is deprecated in Java 8, OK I can forgive it), it should be +0200, not 0.

It is UTC because when converting to string in log.info(), slf4j is interferring???? Or what? I don't think so because System.out.println() gives me UTC too.

I know I should use OffsetDateTime, but it is legacy and we cannot change all fields of date to that, for now. I want to know why Java parsed it wrongly.

What is the effect of Timezone.getDefault() when parsing with SimpleDateFormat? And what is that of f.getTimezone()? They seem to act in different part of the process.....

I ask this question, because internally Jackson uses SimpleDateFormat to process date string/formatting dates. Does the config on an ObjectMapper affect the SimpleDateFormat that the mapper uses?

like image 296
WesternGun Avatar asked Nov 15 '18 15:11

WesternGun


People also ask

What time zone does SimpleDateFormat use?

Since it does not hold any timezone information, its toString function applies the JVM's timezone to return a String in the format, EEE MMM dd HH:mm:ss zzz yyyy , derived from this milliseconds value.

How to set timezone in SimpleDateFormat?

You can make use of the following DateFormat. SimpleDateFormat myDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); myDate. setTimeZone(TimeZone. getTimeZone("UTC")); Date newDate = myDate.

How does Java handle different time zones?

If you cannot change the OS or the JVM timezone, you can still convert a Java Date/Time or Timestamp to a specific time zone using the following two JDBC methods: PreparedStatement#setTimestamp(int parameterIndex, Timestamp x, Calendar cal) – to convert the timestamp that goes to the database.

What are the best practices for handling timezone in database transactions?

The widely-recommended solution for storing dates and times is to store the date and time in UTC. This means, whenever you have a user that inserts or updates a datetime value in the database, convert it to UTC and store the UTC value in the database column. Your data will be consistent.


3 Answers

I don't think it's a bug, but rather a misinterpretation of the lines:

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 CEST: {}", result);

What does it mean ?

You first set a time zone, telling the parser you are about to parse a time in Europe/Madrid zone.

Then you display it. It cannot guess in which time zone you want it, so it displays it in the default time zone, UTC in your case.


Note that:

  • it's actually 10:34 in UTC while it's 12:34 in Madrid, and not the other way round.
  • Date.getTimezoneOffset() is the offset between UTC and the default timezone (thus 0 in your case), nothing to do with the time zone you used to configure the parser. Moreover it is deprecated since java 1.1, you should not really use it anymore.

To display a date value in different time zone, SimpleDateFormat.format() can be used, for instance:

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    log.info("UTC {}", f.format(new Date()));
    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    log.info("Europe/Madrid {}", f.format(new Date()));
like image 101
Benoit Avatar answered Nov 06 '22 18:11

Benoit


Thanks for the reply; in the OP I was thinking of this line but put it wrong:

 log.info("The result should be 14:34 CEST: {}", result);

I thought it is like "So I want it to be Madrid so the output is Madrid timezone", but is the opposite:

The timezone of the formatter will be the input date/string's timezone, while the default timezone(if not changed, that of the JVM, if changed, the value of Timezone.getDefault(), will be the output result(date/string)'s timezone. Based on these two, the formatter will do the conversion.

And, Spring/Jackson internally uses SimpleDateFormat to do JSON/Object serialization/deserialization, so it will be the rule for Spring, too

And, as I test, spring.jackson.time-zone and mapper.setTimezone() will be overridden by JsonFormat(timezone = "xxx") on the fields. That is to say, the spring.jackson.time-zone is more general and applies to all fields of Date who needs an "input" timezone, and JsonFormat(timezone = "xxx") is more specific and overrides the former one. I guess spring.jackson.dateformat and @JsonFormat(pattern = "xx") have the same relationship but I haven't tested.

Graphically:

enter image description here

I write this test to demonstrate this:

/**
 * Test the effect of setting timezone on a {@link SimpleDateFormat}. Apparently,
 * <code>f.setTimezone()</code> sets the input timezone, and default timezone sets
 * the output timezone.
 *
 */
@Test
public void testTimezoneSettingOnSimpleDateFormat() throws ParseException {
    /* *********** test parsing *********** */
    log.info("********** test parsing **********");
    SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String d = "2018-08-08 12:34:56";

    log.info("Trying to parse the date string: {}", d);
    Date result = f.parse(d);
    log.info("The result should be 12:34 UTC: {}", result);

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    result = f.parse(d);
    log.info("The result should be 12:34 UTC: {}", result);

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 UTC: {}", result);

    /* ********** test formatting ********** */
    log.info("********** test formatting **********");
    // given
    SimpleDateFormat f2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
    // construct a date to represent this moment
    OffsetDateTime now = OffsetDateTime.of(2018, 11, 16, 10, 22, 22, 0, ZoneOffset.of("+0100"));
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); // GMT+8, so Madrid+7
    // when you construct a date without timezone, it will be the timezone of system/default!
    Date nowDate = new Date(now.toEpochSecond() * 1000);
    log.info("The constructed date is: {}", nowDate); // Fri Nov 16 17:22:22 CST 2018

    // now formatter timezone is Madrid
    f2.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    // now default timezone is Asia/Shanghai

    // when
    String result2 = f2.format(nowDate);

    // then
    log.info("The result should be 10:22: {}", result2); // 2018-11-16T10:22:22+01:00


    log.info("Conclusion: the formatter's timezone sets the timezone of input; the application/default " +
            "timezone sets the timezone of output. ");
}
like image 28
WesternGun Avatar answered Nov 06 '22 18:11

WesternGun


public static Instant getInstantNow() {
        Clock utcClock = Clock.systemUTC();
        //ZoneId myTZ = ZoneId.of("Brazil/East");       
        return Instant.now(utcClock).minusSeconds(10800);   
        //Instant in = Instant.now(utcClock);
        //return in.atZone(myTZ);   
    }

    public static LocalDateTime getLocalDateTimeNow() {
        ZonedDateTime nowBrasil = ZonedDateTime.now(ZoneId.of("Brazil/East"));
        return LocalDateTime.from(nowBrasil);
    }

    public static LocalDate getLocalDateNow() {
        ZonedDateTime nowBrasil = ZonedDateTime.now(ZoneId.of("Brazil/East"));
        return LocalDate.from(nowBrasil);
    }
like image 1
Michel Fernandes Avatar answered Nov 06 '22 20:11

Michel Fernandes