6

I'm using org.joda.time.LocalDate and LocalDateTime. From an external source I get a Unix timestamp and want to make a LocalDate(Time) out of it. The point is, it is defined in the interface of that external system, that all dates/times are in UTC timezone. So I want to avoid any implicit conversion from that timestamp to any default timezone of the local system which might be different from UTC. There is a constructor of LocalDateTime for such things, so I tried (as an example):

System.out.println(new LocalDateTime(3600000L));
  --> 1970-01-01T02:00:00.000

System.out.println(new LocalDateTime(3600000L, DateTimeZone.UTC));
  --> 1970-01-01T01:00:00.000

The result surprises me a bit. Having a look into the JavaDoc, the first constructor evaluates the timestamp "using ISO chronology in the default zone." By definition, the Unix timestamp is the number of seconds (here milliseconds) from 01-JAN-1970T00:00:00UTC! So if the value 3600000 (= exactly 2 hours in millis) is add to that base, it would come to 01-JAN-1970T02:00:00UTC. My local system is set to timezone Europe/Berlin (CET) which is UTC+1. Precisely, we have daylight saving right now, so it should even be UTC+2, but lets pretend we're at UTC+1 now. So if the timestamp is by definition UTC, then I would expect that the resulting time is either 01:00:00, if it interprets the value of the timestamp to be in CET which is converted to UTC, or 03:00:00 if it correctly expects the timestamp to have a UTC value which is converted to CET. But it actually shows an unconverted timestamp, exactly 2 hours off the base. The second constructor is supposed to evaluate the timestamp "using ISO chronology in the specified zone." (from JavaDoc) So if I specify UTC timezone explicitly, I would not expect any conversion at all, but a time of 02:00:00. A UTC based timestamp which results in a time which itself is declared to be UTC should result in exactly that, but the result is 01:00:00! Just to double-check, I called it with CET explicitly and got the same result as if I don't provide any timezone.

So it looks like, that the timestamp is not considered to be UTC, but to be in the local timezone. Creating a LocalDateTime takes it and applies a conversion from your local timezone to the target one (second parameter of the constructor). First of all I'm wondering, if this is really ok. Secondly I have to guarantee that no such conversion is happening in my code. So I could believe, leaving the second parameter and using the default timezone does the trick, but is that guaranteed? Or might there be a chance that some strange conversion happens if we change from/to daylight saving? Even changing the local timezone must not have any consequence, this is why all times we get as a timestamp from that external system are already converted to UTC.

One evil scenario I observed was, when a timestamp was supposed to be just a date (without time). In this case, the timestamp would be any date with time set to 00:00:00. When I use LocalDate the same way I used LocalDateTime in the example above, it converts the timestamp into date + time (of course) and simply cuts the time off. BUT, if the date was 15-JUL-2014T00:00:00UTC, and the result at my end is shifted the same one hour as in my other example, that turns to 14-JUL-2014T23:00:00 and therewith to the date 14-JUL-2014! This is actually a disaster and must not happen!

So does anyone of you have a clue why LocalDate(Time) behaves like that? Or what is the concept behind I which I might misinterpret. Or how to guarantee that no conversion happens?

BlackDroid
  • 61
  • 1
  • 1
  • 3
  • 5
    I just read the title: "How to *convert*... without *conversion*". I couldn't read through the wall of text. – icza Aug 19 '14 at 13:35
  • 1
    Even Messi can't score through it –  Aug 19 '14 at 13:38
  • 1
    Ok, decide what you want: a super short question without any context where your first request is to provide more information, or a detailed question which gives you the chance to understand my problem, which you call a "wall of text" now. I always thought the latter one is preferred. – BlackDroid Aug 19 '14 at 14:00
  • Regarding the title, you're right, I just realize now that it sounds strange. I meant without timezone conversion/shift. But everyone who reads through it will perfectly understand what I mean. – BlackDroid Aug 19 '14 at 14:05
  • 1
    @BlackDroid Less is usually better to start and we can always ask for more information if needed as long as you start with the minimal amount for us to understand. Also, try to use some formatting and break things up a bit. – codeMagic Aug 19 '14 at 15:30
  • For DST, you need to consider the year 1970, and the country (either West or East Germany), and the particular rules at that time in that country. Whether DST is observed right now in the unified Germany is irrelevant. Computer systems look up a database for that information. – Johan Boulé Nov 09 '19 at 23:54
  • 3600000ms = 1h, not 2h. – Johan Boulé Nov 10 '19 at 00:12

3 Answers3

11

tl;dr

Your Question is confusing, but you seem to claim the number 3_600_000L represents a count of milliseconds since the epoch reference of first moment of 1970 in UTC, 1970-01-01T00:00Z.

So parse as an Instant.

Instant                         // Represent a moment in UTC, an offset of zero hours-minutes-seconds.
.ofEpochMilli( 3_600_000L  )    // Parse a count of milliseconds since 1970-01-01T00:00Z. Returns a `Instant` object.
.toString()                     // Generate text representing this value, using standard ISO 8601 format.

The result is 1 AM on the first day of 1970 as seen in UTC. The Z on the end means UTC.

1970-01-01T01:00:00Z

Get the date portion, as seen in UTC.

Instant                           // Represent a moment in UTC, an offset of zero hours-minutes-seconds.
.ofEpochMilli( 3_600_000L  )      // Parse a count of milliseconds since 1970-01-01T00:00Z.
.atOffset(                        // Convert from `Instant` (always in UTC, an offset of zero) to `OffsetDateTime` which can have any offset.
    ZoneOffset.UTC                // A constant representing an offset of zero hours-minutes-seconds, that is, UTC itself.
)                                 // Returns a `OffsetDateTime` object.
.toLocalDate()                    // Extract the date portion, without the time-of-day and without the offset-from-UTC.
.toString()                       // Generate text representing this value, using standard ISO 8601 format.

1970-01-01

Adjust that moment from UTC to the time zone Europe/Berlin.

Instant                           // Represent a moment in UTC, an offset of zero hours-minutes-seconds.
.ofEpochMilli( 3_600_000L  )      // Parse a count of milliseconds since 1970-01-01T00:00Z.
.atZone(                          // Convert from UTC to a particular time zone.
    ZoneId.of( "Europe/Berlin" )  // A time zone is a history of the past, present, and future changes to the offset-from-UTC used by the people of a particular region. 
)                                 // Returns a `ZonedDateTime` object.
.toString()                       // Generate text representing this value, using standard ISO 8601 format wisely extended to append the name of the time zone in square brackets.

1970-01-01T02:00+01:00[Europe/Berlin]

Notice how that result has a different time-of-day, 2 AM in Berlin area rather than the 1 AM we saw in UTC. The Europe/Berlin time zone was running an hour ahead of UTC at that moment then, so an hour ahead of 1 AM is 2 AM — same moment, same point on the timeline, different wall-clock time.

Get the date-only portion from that moment as seen in Europe/Berlin.

Instant                           // Represent a moment in UTC, an offset of zero hours-minutes-seconds.
.ofEpochMilli( 3_600_000L  )      // Parse a count of milliseconds since 1970-01-01T00:00Z.
.atZone(                          // Convert from UTC to a particular time zone.
    ZoneId.of( "Europe/Berlin" )  // A time zone is a history of the past, present, and future changes to the offset-from-UTC used by the people of a particular region. 
)                                 // Returns a `ZonedDateTime ` object.
.toLocalDate()                    // Extract the date only, without the time-of-day and without the time zone. Returns a `LocalDate` object.
.toString()                       // Generate text representing this value, using standard ISO 8601.

1970-01-01

In this case, the date in Berlin area is the same as in UTC. But in other cases the date may vary. For example, 9 PM (21:00) on the 23rd of January in UTC is simultaneously “tomorrow” the 24th in Tokyo Japan.

java.time

Apparently, you use the term “Unix timestamp” to mean a count of milliseconds since first moment of 1970 UTC, 1970-01-01T00:00Z.

Parse that number into an Instant object. 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.ofEpochMilli( 3_600_000L ) ;

instant.toString(): 1970-01-01T01:00:00Z

So very simple: An Instant is always in UTC, always a moment, a point on the timeline.

when a timestamp was supposed to be just a date (without time).

For this, use the LocalDate class. The LocalDate class represents a date-only value without time-of-day and without time zone.

A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment, so your results may vary. Better to specify your desired/expected time zone explicitly as an argument.

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" ) ;  

Adjust your UTC value (Instant) to another time zone by applying a ZoneId to generate a ZonedDateTime.

ZonedDateTime zdt = instant.atZone( z ) ;  

From there we can extract the date-only portion as a LocalDate object.

LocalDate ld = zdt.toLocalDate() ;

If you want the first moment of the day on that date, you must specify the context of a time zone. For any given moment, the date varies around the globe by time zone. When a new day dawns in India, it is still “yesterday” in France.

Always let java.time determine the first moment of the day. Do not assume 00:00. In some zones on some dates, the day may start at another time such as 01:00 because of anomalies such as Daylight Saving Time (DST).

ZonedDateTime zdtStartOfDay = ld.atStartOfDay( z ) ;

If you want to see that same moment as UTC, simply extract a Instant.

Instant instant = zdtStartOfDay.toInstant() ;

The java.time classes also have a LocalDateTime class. Understand that this class LocalDateTime does not represent a moment! It does not represent a point on the timeline. It has no real meaning until you place it in the context of a time zone. This class is only used for two meanings:

  • The zone/offset is unknown (a bad situation).
  • Every/any zone/offset is intended. For example, "Christmas starts at 00:00 on December 25, 2018“, which means different moments in different places. The first Christmas happens in Kiribati. Then successive Christmases start after each successive midnight moving westward through Asia, then India, and onwards to Europe/Africa, and eventually the Americas. So it takes at least 26 hours for Santa to deliver all the presents.

Hopefully you can see this work is not at all as confusing once you understand the core concepts and use the excellent well-designed java.time classes.


Table of all date-time types in Java, both modern and legacy


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?

Table of which java.time library to use with which version of Java or Android

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
  • The java.time API is an improvement, but still has design issues compared to the best API currently being rolled out in the C++ standard for example. The documentation about the toLocalDate method of the LocalDateTime class is unfortunately too vague or missleading. Given a LocalDateTime which shows as 2019-10-25T00:00, and whose internal fields match those printed values, the toLocalDate method returns a LocalDate that's 2019-10-24 on my system. I'll have to resort to reading the actual source code implementation to have any clue what to do. – Johan Boulé Nov 10 '19 at 00:44
  • @JohanBoulé Please link to the C++ API for date-time handling you mentioned. I am curious. – Basil Bourque Nov 10 '19 at 01:01
  • @JohanBoulé Regarding the [source code for `LocalDateTim` in Java 11](https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/java.base/share/classes/java/time/LocalDateTime.java), the method `toLocalDate` is utterly simple: `return date;` where `date` is a `LocalDate` member field on the class. See example code `LocalDateTime.parse( "2019-10-25T00:00" ).toLocalDate().toString()` [run live at IdeOne.com](https://ideone.com/ztXBKL). So you must have something else going on in your own code to get your surprise 24th rather than 25th. I suggest you post a Question here showing the problem. – Basil Bourque Nov 10 '19 at 01:13
  • My bad. I was debugging with desynchronized source code, and using the debugger's inspector on up-to-date code expression. Happens all the time due to Eclipse's build action having nothing to do with the output read by its tomcat server launcher "integration". – Johan Boulé Nov 10 '19 at 06:54
  • What I don't like in the java.time design is the exaggerated coupling between the various components. For example, the Instant class should really have no `now` method because that's the role of clocks. It should have no method taking anything related to timezones, because those are the roles of zone-related classes. I feel the design has too many factory methods stitched to random classes rather than first class functions. The C++ API made a point to keep each component very cleanly separated and focused on one and only one thing. – Johan Boulé Nov 10 '19 at 07:11
  • According to the image you provided LocalDate has no zone. So, why do we need ZonedDateTime to get LocalDate from timestamp? Could you explain? – Pavel_K May 20 '20 at 20:37
  • @Pavel_K As I said in the Answer, for any given moment, the **date varies around the globe by time zone**. For example, at 10 PM in Toledo Ohio US on the 23rd of January, it is simultaneously “Tomorrow” the 24th of January in Tokyo Japan. At that moment whether you see the date as 23 or 24 depends on the time zone of interest to you. – Basil Bourque May 20 '20 at 21:00
  • Do I understand you right - Unix timestamp includes zone and it is UTC 0? – Pavel_K May 20 '20 at 21:05
  • @Pavel_K There no precise definition for “Unix timestamp”, but usually people mean by that a count of whole seconds or a count of milliseconds since the first moment of 1970 in UTC. By “in UTC”, we mean an offset-from-UTC of zero hours-minutes-seconds. But UTC is not a time zone. A time zone is a history of the past, present, and future to the offset used by the people of a particular region. For example, `Atlantic/Reykjavik` and `Africa/Abidjan` are two of the several time zones that nowadays use an offset of zero. – Basil Bourque May 20 '20 at 21:17
  • Could you say if this is a right solution if we have timestamp without time zone and we want to get LocalDate without time zone - `LocalDate date = LocalDate.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC);` ? – Pavel_K May 20 '20 at 21:50
  • @Pavel_K Do you want the current date as seen in UTC? `LocalDate.now( ZoneOffset.UTC )` And for current date-time as seen in UTC: `OffsetDateTime.now( ZoneOffset.UTC )` – Basil Bourque May 20 '20 at 22:14
  • @Pavel_K I added a "tl;dr" section at the top of this Answer. Those examples may help you. – Basil Bourque May 21 '20 at 00:45
  • @BasilBourque Yes, that is what I was talking about. Thank you very much. – Pavel_K May 21 '20 at 08:03
3

Why don't you:

timeStamp.toLocalDateTime().toLocalDate(); // JAVA 8
  • 1
    This defaults to local timezone. Not what you would want all the time. – zengr Apr 18 '16 at 21:11
  • You could temporarily change the local date beforehand: TimeZone.setDefault(TimeZone.getTimeZone("UTC")); –  Apr 28 '17 at 15:28
  • 2
    Calling `TimeZone.setDefault` should only be done as a desperate last-ditch choice. It immediately affects all code in all threads of all apps within the JVM. – Basil Bourque Apr 06 '18 at 01:42
2

new LocalDateTime(3600000L, DateTimeZone.UTC) doesn't really make sense: LocalDateTime is, by definition, in your time zone. So what this does is: It assumes the timestamp was taken using your local timezone and you want to know what this timestamp would be in the UTC timezone. This is exactly the opposite conversion that you want to do.

Try new DateTime(3600000L).toLocalDateTime() to convert a UTC timestamp to a local time.

[EDIT] You're right, my suggestion above is misleading. The docs say:

LocalDateTime is an unmodifiable datetime class representing a datetime without a time zone.

So this thing is local to the "current time zone" - whatever that may be. When you create a formatter, it implicitly gets a time zone (the default one). So when you format such a local time, it will "move" into your time zone since it doesn't have one itself.

You can use this type to represent the concept of "12:00" without a time zone. If you add to a date with Singapore, it will inherit the time zone of Singapore. So you can use this for date calculations like "I want to get a DateTime for "9:00" in various cities in the world."

DateTime, on the other hand, has a fixed time zone which doesn't change depending on the context. If you don't give it one, the current time zone of the Java VM will be the default.

With that knowledge, new DateTime(3600000L, DateTimeZone.UTC).toLocalDateTime() obviously has to result in 1970-01-01T01:00:00.000.

First, we create a DateTime with a fixed time zone (UTC). When you format this alone, the formatter sees "oh, this has a time zone, so I can use that." Now you convert it into a local time which strips the time zone info and the formatter will use the default.

To solve your problem, use this code:

new DateTime(3600000L, DateTimeZone.UTC).withTimeZone(DateTimeZone.getDefault())

which should be the same as:

new DateTime(3600000L)

since all time stamps are relative to 1970-01-01T00:00:00Z (Z == UTC time zone).

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • Exactly, it assumes the timestamp was in my local timezone, but the timestamp is by definition UTC! So UTC timestamp converted to UTC should mean, there is no conversion at all, but there is! :-) – BlackDroid Aug 19 '14 at 14:19
  • And I tried your recommendation, and it surprises me again: new DateTime(3600000L, DateTimeZone.UTC).toLocalDateTime() results _again_ in 1970-01-01T01:00:00.000 ;-P – BlackDroid Aug 19 '14 at 14:20
  • 2
    By the way, is it really "by definition" that LocalDateTime is always bound to _your_ local timezone? The JavaDoc says, it is a date/time without any timezone. That makes sense, so it is "somewhere local", not necessarily in your time zone, right? Even more, you can provide a timezone explicitly only in methods/constructors, which take any timezone interpreted time and make it a LocalDateTime. After that, it really has no timezone anylonger. – BlackDroid Aug 19 '14 at 14:26
  • You're right, my explanation was wrong/confusing. See my edits. – Aaron Digulla Aug 19 '14 at 14:45
  • Hey, thanks a lot for all the effort you spend to help me! Still I have the feeling, that the timestamp is not interpreted as the number of millis from 1970-01-01T00:00:00Z (= UTC). When I do: new DateTime(3600000L, DateTimeZone.UTC), then I have everything in UTC, right? The base is UTC, my DateTime is in UTC, but the result is: 1970-01-01T01:00:00.000Z. Still I loose this one hour somewhere even without converting the DateTime into a LocalDateTime. I don't get it! – BlackDroid Aug 19 '14 at 15:11
  • 60*60 = 3600 = one hour. Or try it with `new DateTime(0L)` to make sure there is no time zone or other offset. – Aaron Digulla Aug 19 '14 at 15:19