Java Convert GMT/UTC to Local time doesn't work as expected

119,176

Solution 1

I also recommend using Joda as mentioned before.

Solving your problem using standard Java Date objects only can be done as follows:

    // **** YOUR CODE **** BEGIN ****
    long ts = System.currentTimeMillis();
    Date localTime = new Date(ts);
    String format = "yyyy/MM/dd HH:mm:ss";
    SimpleDateFormat sdf = new SimpleDateFormat(format);

    // Convert Local Time to UTC (Works Fine)
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    Date gmtTime = new Date(sdf.format(localTime));
    System.out.println("Local:" + localTime.toString() + "," + localTime.getTime() + " --> UTC time:"
            + gmtTime.toString() + "," + gmtTime.getTime());

    // **** YOUR CODE **** END ****

    // Convert UTC to Local Time
    Date fromGmt = new Date(gmtTime.getTime() + TimeZone.getDefault().getOffset(localTime.getTime()));
    System.out.println("UTC time:" + gmtTime.toString() + "," + gmtTime.getTime() + " --> Local:"
            + fromGmt.toString() + "-" + fromGmt.getTime());

Output:

Local:Tue Oct 15 12:19:40 CEST 2013,1381832380522 --> UTC time:Tue Oct 15 10:19:40 CEST 2013,1381825180000
UTC time:Tue Oct 15 10:19:40 CEST 2013,1381825180000 --> Local:Tue Oct 15 12:19:40 CEST 2013-1381832380000

Solution 2

Joda-Time


UPDATE: The Joda-Time project is now in maintenance mode, with the team advising migration to the java.time classes. See Tutorial by Oracle.

See my other Answer using the industry-leading java.time classes.


Normally we consider it bad form on StackOverflow.com to answer a specific question by suggesting an alternate technology. But in the case of the date, time, and calendar classes bundled with Java 7 and earlier, those classes are so notoriously bad in both design and execution that I am compelled to suggest using a 3rd-party library instead: Joda-Time.

Joda-Time works by creating immutable objects. So rather than alter the time zone of a DateTime object, we simply instantiate a new DateTime with a different time zone assigned.

Your central concern of using both local and UTC time is so very simple in Joda-Time, taking just 3 lines of code.

    org.joda.time.DateTime now = new org.joda.time.DateTime();
    System.out.println( "Local time in ISO 8601 format: " + now + " in zone: " + now.getZone() );
    System.out.println( "UTC (Zulu) time zone: " + now.toDateTime( org.joda.time.DateTimeZone.UTC ) );

Output when run on the west coast of North America might be:

Local time in ISO 8601 format: 2013-10-15T02:45:30.801-07:00

UTC (Zulu) time zone: 2013-10-15T09:45:30.801Z

Here is a class with several examples and further comments. Using Joda-Time 2.5.

/**
 * Created by Basil Bourque on 2013-10-15.
 * © Basil Bourque 2013
 * This source code may be used freely forever by anyone taking full responsibility for doing so.
 */
public class TimeExample {
    public static void main(String[] args) {
        // Joda-Time - The popular alternative to Sun/Oracle's notoriously bad date, time, and calendar classes bundled with Java 8 and earlier.
        // http://www.joda.org/joda-time/

        // Joda-Time will become outmoded by the JSR 310 Date and Time API introduced in Java 8.
        // JSR 310 was inspired by Joda-Time but is not directly based on it.
        // http://jcp.org/en/jsr/detail?id=310

        // By default, Joda-Time produces strings in the standard ISO 8601 format.
        // https://en.wikipedia.org/wiki/ISO_8601
        // You may output to strings in other formats.

        // Capture one moment in time, to be used in all the examples to follow.
        org.joda.time.DateTime now = new org.joda.time.DateTime();

        System.out.println( "Local time in ISO 8601 format: " + now + " in zone: " + now.getZone() );
        System.out.println( "UTC (Zulu) time zone: " + now.toDateTime( org.joda.time.DateTimeZone.UTC ) );

        // You may specify a time zone in either of two ways:
        // • Using identifiers bundled with Joda-Time
        // • Using identifiers bundled with Java via its TimeZone class

        // ----|  Joda-Time Zones  |---------------------------------

        // Time zone identifiers defined by Joda-Time…
        System.out.println( "Time zones defined in Joda-Time : " + java.util.Arrays.toString( org.joda.time.DateTimeZone.getAvailableIDs().toArray() ) );

        // Specify a time zone using DateTimeZone objects from Joda-Time.
        // http://joda-time.sourceforge.net/apidocs/org/joda/time/DateTimeZone.html
        org.joda.time.DateTimeZone parisDateTimeZone = org.joda.time.DateTimeZone.forID( "Europe/Paris" );
        System.out.println( "Paris France (Joda-Time zone): " + now.toDateTime( parisDateTimeZone ) );

        // ----|  Java Zones  |---------------------------------

        // Time zone identifiers defined by Java…
        System.out.println( "Time zones defined within Java : " + java.util.Arrays.toString( java.util.TimeZone.getAvailableIDs() ) );

        // Specify a time zone using TimeZone objects built into Java.
        // http://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html
        java.util.TimeZone parisTimeZone = java.util.TimeZone.getTimeZone( "Europe/Paris" );
        System.out.println( "Paris France (Java zone): " + now.toDateTime(org.joda.time.DateTimeZone.forTimeZone( parisTimeZone ) ) );

    }
}

Solution 3

You have a date with a known timezone (Here Europe/Madrid), and a target timezone (UTC)

You just need two SimpleDateFormats:

        long ts = System.currentTimeMillis();
        Date localTime = new Date(ts);

        SimpleDateFormat sdfLocal = new SimpleDateFormat ("yyyy/MM/dd HH:mm:ss");
        sdfLocal.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));

        SimpleDateFormat sdfUTC = new SimpleDateFormat ("yyyy/MM/dd HH:mm:ss");
        sdfUTC.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Convert Local Time to UTC
        Date utcTime = sdfLocal.parse(sdfUTC.format(localTime));
        System.out.println("Local:" + localTime.toString() + "," + localTime.getTime() + " --> UTC time:" + utcTime.toString() + "-" + utcTime.getTime());

        // Reverse Convert UTC Time to Locale time
        localTime = sdfUTC.parse(sdfLocal.format(utcTime));
        System.out.println("UTC:" + utcTime.toString() + "," + utcTime.getTime() + " --> Local time:" + localTime.toString() + "-" + localTime.getTime());

So after see it working you can add this method to your utils:

    public Date convertDate(Date dateFrom, String fromTimeZone, String toTimeZone) throws ParseException {
        String pattern = "yyyy/MM/dd HH:mm:ss";
        SimpleDateFormat sdfFrom = new SimpleDateFormat (pattern);
        sdfFrom.setTimeZone(TimeZone.getTimeZone(fromTimeZone));

        SimpleDateFormat sdfTo = new SimpleDateFormat (pattern);
        sdfTo.setTimeZone(TimeZone.getTimeZone(toTimeZone));

        Date dateTo = sdfFrom.parse(sdfTo.format(dateFrom));
        return dateTo;
    }

Solution 4

tl;dr

Instant.now()                           // Capture the current moment in UTC.
.atZone( ZoneId.systemDefault() )       // Adjust into the JVM's current default time zone. Same moment, different wall-clock time. Produces a `ZonedDateTime` object.
.toInstant()                            // Extract a `Instant` (always in UTC) object from the `ZonedDateTime` object.
.atZone( ZoneId.of( "Europe/Paris" ) )  // Adjust the `Instant` into a specific time zone. Renders a `ZonedDateTime` object. Same moment, different wall-clock time.
.toInstant()                            // And back to UTC again.

java.time

The modern approach uses the java.time classes that supplanted the troublesome old legacy date-time classes (Date, Calendar, etc.).

Your use of the word "local" contradicts the usage in the java.time class. In java.time, "local" means any locality or all localities, but not any one particular locality. The java.time classes with names starting with "Local…" all lack any concept of time zone or offset-from-UTC. So they do not represent a specific moment, they are not a point on the timeline, whereas your Question is all about moments, points on the timeline viewed through various wall-clock times.

Get the current system time (local time)

If you want to capture the current moment in UTC, use Instant. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Instant instant = Instant.now() ;  // Capture the current moment in UTC.

Adjust into a time zone by applying a ZoneId to get a ZonedDateTime. Same moment, same point on the timeline, different wall-clock time.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;  // Same moment, different wall-clock time.

As a shortcut, you can skip the usage of Instant to get a ZonedDateTime.

ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = ZonedDateTime.now( z ) ;

Convert Local time to UTC // Works Fine Till here

You can adjust from the zoned date-time to UTC by extracting an Instant from a ZonedDateTime.

ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = ZonedDateTime.now( z ) ;
Instant instant = zdt.toInstant() ;

Reverse the UTC time, back to local time.

As shown above, apply a ZoneId to adjust the same moment into another wall-clock time used by the people of a certain region (a time zone).

Instant instant = Instant.now() ;  // Capture current moment in UTC.

ZoneId zDefault = ZoneId.systemDefault() ;  // The JVM's current default time zone.
ZonedDateTime zdtDefault = instant.atZone( zDefault ) ;

ZoneId zTunis = ZoneId.of( "Africa/Tunis" ) ;  // The JVM's current default time zone.
ZonedDateTime zdtTunis = instant.atZone( zTunis ) ;

ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;  // The JVM's current default time zone.
ZonedDateTime zdtAuckland = instant.atZone( zAuckland ) ;

Going back to UTC from a zoned date-time, call ZonedDateTime::toInstant. Think of it conceptually as: ZonedDateTime = Instant + ZoneId.

Instant instant = zdtAuckland.toInstant() ;

All of these objects, the Instant and the three ZonedDateTime objects all represent the very same simultaneous moment, the same point in history.

Followed 3 different approaches (listed below) but all the 3 approaches retains the time in UTC only.

Forget about trying to fix code using those awful Date, Calendar, and GregorianCalendar classes. They are a wretched mess of bad design and flaws. You need never touch them again. If you must interface with old code not yet updated to java.time, you can convert back-and-forth via new conversion methods added to the old classes.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Solution 5

I am joining the choir recommending that you skip the now long outdated classes Date, Calendar, SimpleDateFormat and friends. In particular I would warn against using the deprecated methods and constructors of the Date class, like the Date(String) constructor you used. They were deprecated because they don’t work reliably across time zones, so don’t use them. And yes, most of the constructors and methods of that class are deprecated.

While at the time you asked the question, Joda-Time was (from all I know) a clearly better alternative, time has moved on again. Today Joda-Time is a largely finished project, and its developers recommend you use java.time, the modern Java date and time API, instead. I will show you how.

    ZonedDateTime localTime = ZonedDateTime.now(ZoneId.systemDefault());

    // Convert Local Time to UTC 
    OffsetDateTime gmtTime
            = localTime.toOffsetDateTime().withOffsetSameInstant(ZoneOffset.UTC);
    System.out.println("Local:" + localTime.toString() 
            + " --> UTC time:" + gmtTime.toString());

    // Reverse Convert UTC Time to Local time
    localTime = gmtTime.atZoneSameInstant(ZoneId.systemDefault());
    System.out.println("Local Time " + localTime.toString());

For starters, note that not only is the code only half as long as yours, it is also clearer to read.

On my computer the code prints:

Local:2017-09-02T07:25:46.211+02:00[Europe/Berlin] --> UTC time:2017-09-02T05:25:46.211Z
Local Time 2017-09-02T07:25:46.211+02:00[Europe/Berlin]

I left out the milliseconds from the epoch. You can always get them from System.currentTimeMillis(); as in your question, and they are independent of time zone, so I didn’t find them intersting here.

I hesitatingly kept your variable name localTime. I think it’s a good name. The modern API has a class called LocalTime, so using that name, only not capitalized, for an object that hasn’t got type LocalTime might confuse some (a LocalTime doesn’t hold time zone information, which we need to keep here to be able to make the right conversion; it also only holds the time-of-day, not the date).

Your conversion from local time to UTC was incorrect and impossible

The outdated Date class doesn’t hold any time zone information (you may say that internally it always uses UTC), so there is no such thing as converting a Date from one time zone to another. When I just ran your code on my computer, the first line it printed, was:

Local:Sat Sep 02 07:25:45 CEST 2017,1504329945967 --> UTC time:Sat Sep 02 05:25:45 CEST 2017-1504322745000

07:25:45 CEST is correct, of course. The correct UTC time would have been 05:25:45 UTC, but it says CEST again, which is incorrect.

Now you will never need the Date class again, :-) but if you were ever going to, the must-read would be All about java.util.Date on Jon Skeet’s coding blog.

Question: Can I use the modern API with my Java version?

If using at least Java 6, you can.

  • In Java 8 and later the new API comes built-in.
  • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (that’s ThreeTen for JSR-310, where the modern API was first defined).
  • On Android, use the Android edition of ThreeTen Backport. It’s called ThreeTenABP, and I think that there’s a wonderful explanation in this question: How to use ThreeTenABP in Android Project.
Share:
119,176
Vinod Jayachandran
Author by

Vinod Jayachandran

Updated on June 17, 2020

Comments

  • Vinod Jayachandran
    Vinod Jayachandran almost 4 years

    In Order to show a reproducible scenario, I am doing the following

    1. Get the current system time (local time)

    2. Convert Local time to UTC // Works Fine Till here

    3. Reverse the UTC time, back to local time. Followed 3 different approaches (listed below) but all the 3 approaches retains the time in UTC only.

      {

      long ts = System.currentTimeMillis();
      Date localTime = new Date(ts);
      String format = "yyyy/MM/dd HH:mm:ss";
      SimpleDateFormat sdf = new SimpleDateFormat (format);
      
      // Convert Local Time to UTC (Works Fine) 
      sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
      Date gmtTime = new Date(sdf.format(localTime));
      System.out.println("Local:" + localTime.toString() + "," + localTime.getTime() + " --> UTC time:" + gmtTime.toString() + "-" + gmtTime.getTime());
      
      // Reverse Convert UTC Time to Locale time (Doesn't work) Approach 1
      sdf.setTimeZone(TimeZone.getDefault());        
      localTime = new Date(sdf.format(gmtTime));
      System.out.println("Local:" + localTime.toString() + "," + localTime.getTime() + " --> UTC time:" + gmtTime.toString() + "-" + gmtTime.getTime());
      
      // Reverse Convert UTC Time to Locale time (Doesn't work) Approach 2 using DateFormat
      DateFormat df = new SimpleDateFormat (format);
      df.setTimeZone(TimeZone.getDefault());
      localTime = df.parse((df.format(gmtTime)));
      System.out.println("Local:" + localTime.toString() + "," + localTime.getTime() + " --> UTC time:" + gmtTime.toString() + "-" + gmtTime.getTime());
      
      // Approach 3
      Calendar c = new GregorianCalendar(TimeZone.getDefault());
      c.setTimeInMillis(gmtTime.getTime());
      System.out.println("Local Time " + c.toString());
      

      }

  • Vinod Jayachandran
    Vinod Jayachandran over 10 years
    Can you provide an example to achieve the same using #joda ?
  • Vinod Jayachandran
    Vinod Jayachandran over 10 years
    I don't mind using joda, yet would like to understand why the code isn't behaving as expected.
  • Dime
    Dime almost 10 years
    A good answer! However instead of TimeZone.getDefault().getOffset(localTime.getTime()), there are should be TimeZone.getDefault().getOffset(gmtTime.getTimeInMillis()). Otherwise you may run into problems with DaylightSaving if your UTC Time is in winter, but the localtime is in summer.
  • Bugs Happen
    Bugs Happen over 8 years
    new Date(String string) deprecated :(
  • Andrii Plotnikov
    Andrii Plotnikov over 7 years
    you're a life saver T_T this is the only way to solve conflict of java.util.date(@temporal) and timestamp(postgresql) of GMT vs UTC
  • Basil Bourque
    Basil Bourque almost 6 years
    FYI, the troublesome old date-time classes such as java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat are now legacy, supplanted by the java.time classes built into Java 8 and later. See Tutorial by Oracle.