1

Trying to use JSR 310 to convert milliseconds datetime values between timezones. Dealing with milliseconds values is required, to work with legacy APIs. In my case, it's between local and UTC/GMT, but I would expect it to be the exact same code independent of exactly which source and destination timezones are involved. Here is my little test program. It is configured such that it iterates over the hours right around the last local DST change. The output is wrong, and so I assume UTCtoLocalMillis is wrong, but possibly there is also a problem with my test methodology. What I mean by wrong output, is that for my timezone, hours should be substracted to get UTC, but the code actually adds hours. Secondly, the point at which DST hits is also off by one hour.

I first fetch the local timezone, but then reset it to UTC, such that the Date().toString() will not perform any conversion when creating the output.

What I want is to create a method that takes a time in milliseconds, and a source ZoneID, and a target ZoneID, and returns the converted milliseconds, using the new JSR 310 API.

public class Test {
static int DAYS_OFFSET_TO_BE_BEFORE_DST_CHANGE = -16;

static TimeZone UTC_TZ = TimeZone.getTimeZone("UTC");

static TimeZone LOCAL_TZ = TimeZone.getDefault();

static ZoneId UTC_ID = ZoneId.of(UTC_TZ.getID());

static ZoneId LOCAL_ZONE_ID = ZoneId.of(LOCAL_TZ.getID());

static long UTCtoLocalMillis(final long utcMillis) {
    final Instant instant = Instant.ofEpochMilli(utcMillis);
    final ZonedDateTime before
        = ZonedDateTime.ofInstant(instant, UTC_ID);
    final ZonedDateTime after
        = before.withZoneSameLocal(LOCAL_ZONE_ID);
    return after.toInstant().toEpochMilli();
}

public static void main(final String[] args) {
    TimeZone.setDefault(UTC_TZ);
    System.out.println("LOCAL_TZ: " + LOCAL_TZ.getDisplayName());
    final Calendar cal = Calendar.getInstance(LOCAL_TZ);
    cal.add(Calendar.DATE, DAYS_OFFSET_TO_BE_BEFORE_DST_CHANGE);
    final Date start = cal.getTime();
    // DST Change: Sunday, 31 March 2013 01:59:59 (local time)
    final long oneHour = 3600000L;
    for (int i = 0; i < 12; i++) {
        final Date date = new Date(start.getTime() + i * oneHour);
        System.out.println("UTC: " + date + "   toLocal: "
                + new Date(UTCtoLocalMillis(date.getTime())));
    }
}
}
Perception
  • 79,279
  • 19
  • 185
  • 195
Sebastien Diot
  • 7,183
  • 6
  • 43
  • 85

2 Answers2

3

What I want is to create a method that takes a time in milliseconds, and a source ZoneID, and a target ZoneID, and returns the converted milliseconds, using the new JSR 310 API.

Your requirement seems very confused. The "millis" value generally referred to in time APIs is epoch-millis, the number of milliseconds from 1970-01-01T00:00Z. The millis value is always relative to UTC. While there is a concept of "local millis" it is never really used. In particular, java.util.Date constructor takes in epoch-millis.

Thus, the code new Date(UTCtoLocalMillis(date.getTime())) is basically meaningless.

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
  • Thank you! So it seems I don't 'get' the time API. I must admit I had to deal a lot with dates until now, but neither with time, nor with timezones. But I think one single question can clear it all: If all (correctly configured) computers around the world called System.currentTimeMillis() at the same time, would they then all get the same (approximate) value? The javadoc says "Returns the current time in milliseconds" which tells us neither if it is "local" or "UTC", because "current" does not specifically implies the one or the other, IMO. – Sebastien Diot Apr 16 '13 at 12:37
  • OK. I answered it myself, by asking someone on the other side of the planet to run this one-liner at the same time I did: public class Test { public static void main(String[] args) { System.out.println(System.currentTimeMillis()); } } And the same time came out. Basically, System.currentTimeMillis() (and Date) is always in UTC. I wish this was made clearer in the Javadoc. :( – Sebastien Diot Apr 16 '13 at 17:28
2

tl;dr

Instant                              // Represent a moment in UTC with a resolution of nanoseconds.
.ofEpochMilli( utcMillis )           // Parse a count of milliseconds since epoch of 1970-01-01T00:00:00Z.
.atZone(                             // Adjust from UTC to some time zone.
    ZoneId.of( "Pacific/Auckland" ) 
)

Details

The Answer by JodaStephen is correct. There is no such thing as “LocalMillis”. You seem to have some conceptual misunderstanding. This is understandable as date-time work is surprisingly tricky.

Tip: Learn to think and work in UTC. As a programmer or admin, forget about your own time zone. Think of UTC as The One True Time. The various time zones are but mere variations. Do your logging, tracing, data storage, and data exchange all in UTC. Apply a time zone only for presentation to the user. Flipping back-and-forth between UTC and your own parochial time zone will drive you batty. Place a second clock on your desk labeled and set to UTC; use it while working.

Another tip: Focus conceptually on the timeline. There is only one flow of time. The main way to represent that timeline is UTC. The various time zones are but varied representations of the same moments on the single timeline.

Here is some example java.time code.

takes a time in milliseconds, and a source ZoneID, and a target ZoneID, and returns the converted milliseconds, using the new JSR 310 API.

The part you are not understanding is that "source ZoneID" is always UTC for a count-of-milliseconds-since-epoch. Anyone passing around a count-of-milliseconds that is not in UTC is inexperienced and asking for trouble.

If you have a count of milliseconds since the epoch reference of first moment of 1970 in UTC, 1970-01-01T00:00:00Z, then parse as an Instant. An Instant represents a moment in UTC.

Instant instant = Instant.ofEpochMilli( utcMillis ) ;

Generate text in a String representing the moment in that Instant. By default the java.time classes use ISO 8601 standard formats when parsing/generating strings. The Z on the end means UTC and is pronounced “Zulu”.

String output = instant.toString() ;

2018-01-23T01:23:45.123456789Z

Going the other direction…

long millisecondsFromEpoch = instant.toEpochMilli() ;

With an Instant in hand, you may want to view that moment through the lens of the wall-clock time used by the people of a particular region, a time zone. By “wall-clock time” we mean the time-of-day and date seen by a person who looks up to the clock and calendar hanging on their wall.

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( "Africa/Tunis" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;  // Same moment, same point on the timeline, different wall-clock time.

We can get back to UTC by extracting an Instant.

Instant instant = zdt.toInstant() ;  // Same moment, different wall-clock time.

Last tip: Never use Date, Calendar, SimpleDateFormat or the other related legacy date-time classes. They are endlessly confusing and frustrating. Use only java.time 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.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Hi. While I appreciate long and detailed answers, I find it strange that you would assume that I still now don't understand how this works *5 years* after asking the question: "You seem to have some conceptual misunderstanding..." – Sebastien Diot Oct 02 '18 at 08:37
  • 1
    @SebastienDiot You also seem to misunderstand that **Stack Overflow is not email**. When I post here, I am *not* speaking to you individually. I am speaking to the thousands of other people who are reading this page *today*. While framed as a response to you, I am actually addressing the confusion or misunderstanding or curiosity of those who sought out this page. If I were helping only you individually, I would be charging a consulting fee, not working for free. The pleasure of serving posterity is what makes doing this work free-of-charge worthwhile. – Basil Bourque Oct 02 '18 at 17:01