Reputation: 51
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?
Upvotes: 5
Views: 27782
Reputation: 338624
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.
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:
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.
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.
Upvotes: 12
Reputation: 328604
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).
Upvotes: 2
Reputation:
Why don't you:
timeStamp.toLocalDateTime().toLocalDate(); // JAVA 8
Upvotes: 3