This question already has answers here:
What Date time format can be used to handle DST in Java
(2 answers)
Closed 12 months ago.
We are sent two dates from an external source as Strings. We then calculate the difference between the dates to see how many hours they worked.
Two times a year due to the time change - Our calculations get wrong. We use Java.
How do you solve a problem like this?
The two dates come in a file as String and have the following format.
"2019-10-07 11:07 AM"
It really depends on what your input data is and precisely what you're trying to determine.
If you're just receiving the dates, represent them as LocalDate values - that's what LocalDate represents; a date without any associated time zone. You can then use Period.between to find out the difference between LocalDate values, for example.
However, if you want to actually work out an elapsed time between two instants in time, then you do need to take time zones into account - in which case you might parse into an OffsetDateTime or a ZonedDateTime (or perhaps directly Instant - it depends on your input data). You can find the elapsed Duration between any two Temporal values (such as OffsetDateTime and ZonedDateTime, or Instant) using Duration.between.
It's worth noting that if your input data specifies a time zone (e.g. Europe/London) and a local date/time, you will need to consider ambiguous local date/time values. For example, suppose the file is something like:
Time zone: Europe/London
Shift 1 start: 2022-10-31T01:30
Shift 1 end: 2022-10-31T02:30
Should that be one hour, or two? It could be either, because 1:30am happened twice in the Europe/London time zone on that day, as the clocks went back from 2am to 1am. You could even end up with data that seems implausible at first glance:
Time zone: Europe/London
Shift 1 start: 2022-10-31T01:30
Shift 1 end: 2022-10-31T01:15
That's entirely valid as a 45 minute shift, if it started at the first occurrence of 1:30, and ended at the second occurrence of 1:15.
A good way to represent correct points in time that, once constructed, allow calculations independent of any assumed time zone is ZonedDateTime. Whats nice about this class is that you can create instances of it easily using your own idea of local time. You just need to tell it what time zone you're in, and it will do the right thing for daylight savings time as defined for your time zone. Once constructed, you can do date/time arithmetic on these objects without concern for time zone or DST state.
Here are the two simple cases for creating and subtracting two ZonedDateTime objects representing an 8 hour timespan, one that spans a DST change, and one that doesn't. There is likely a way to do this with Java's date functions such that you never have to see and deal with the individual numbers.
Case 1: Worker works 8 hours that don't cross a DST point:
ZonedDateTime zdt = ZonedDateTime.of(
2022, 1, 13, 1, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
ZonedDateTime zdt2 = ZonedDateTime.of(
2022, 1, 13, 9, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
long hours = ChronoUnit.HOURS.between(zdt, zdt2);
System.out.println(hours);
Result:
8
Case 2: Worker works 8 hours that cross a DST point:
ZonedDateTime zdt = ZonedDateTime.of(
2022, 3, 13, 1, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
ZonedDateTime zdt2 = ZonedDateTime.of(
2022, 3, 13, 10, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
long hours = ChronoUnit.HOURS.between(zdt, zdt2);
System.out.println(hours);
Result:
8
Notice that if you forgot about DST, you'd think that for the second shift, the worker worked an extra hour. But given DST, the two shifts were the same number of hours.
NOTE: I started from numeric representations of date/times because the question does not give examples of or otherwise specify the formats of the input strings involved. A "correct" example that involved string parsing would need to assume a format that might be different than what the OP is starting with. The primary question here would be if and how time zone is represented in those strings. Parsing a string to come up with a set of numbers and a time zone is a separate problem that has nothing to do with DST, so I leave that for the OP to figure out. It is likely that you could use date/time parsing logic in the Java date/time library to directly arrive at a ZonedDateTime, again, dependent on the format of the strings.
Related
I want to get a difference in hours between a current time in a specific timezone and UTC time. I tried this:
LocalTime time = LocalTime.now();
System.out.println(time); //21:05:42:764
LocalTime utcTime = LocalTime.now(ZoneId.of("UTC"));
System.out.println(utcTime); //18:05:42:769
System.out.println(Duration.between(utcTime, time).getSeconds()/3600); //2
System.out.println(Duration.between(time, utcTime).getSeconds()/3600); //-3
Why is the difference between the last two results and are there better ways to do it? I need to get the number 3.
Why is the difference between the last two results
The reason that you're getting different results for the two computed durations is a combination of the fact that there is some tiny amount of time elapsed between the two recordings and the fact that the duration start time is included in the range but the duration end time is not.
Consider these times instead: 6:00:00:001 vs 8:00:00:000. Here it is very obvious that we're only exactly one millisecond off of two hours, but when we think about seconds we're either going to get 7199 or -7200. When we then do integer math (i.e. divide by 3600), we're going to get 1 or -2.
If it weren't for the one extra millisecond on the first timestamp, the absolute value of the two would be identical.
Duration is the wrong class. There is zero duration between "now" in one time zone and "now" in another. For a fun but memorable way to think about this, see here.
You appear to be seeking to know the current offset from UTC for a given time zone. You can use the ZonedDateTime class for that:
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Kolkata"));
ZoneOffset offset = zdt.getOffset();
int offsetMinutes = offset.getTotalSeconds() / 60;
double offsetHours = ((double) offsetMinutes) / 60;
System.out.println(offsetHours); // 5.5
You could also just use ZonedDateTime.now() on the first line, if you want to use the computer's current time zone.
With regard to LocalTime - that is just the time portion (hours, minutes, seconds, and smaller). Since there is no date associated, you can't necessarily determine which time zone offset it belongs to. There is more than one date that "today" going on at any given moment. Time zone offsets range from UTC-12 to UTC+14, so there are indeed values where the same time of day is happening on two different dates somewhere on the planet.
As an example, 08:00:00 in Hawaii (Pacific/Honolulu) on 2019-01-01 is also 08:00:00 in Kiribati (Pacific/Kiritimati), but on 2019-01-02 - the following date! (Reference here.) Thus, if you had a LocalTime object with 08:00:00 and it was 08:00:00 in one of those two zones, you'd not be able to tell which one it was, or what the corresponding UTC offset should be.
Also, keep in mind that time zone offsets are not limited to whole hours. There are present-day time zones with half-hour and 45-minute offset. Historically, there have been others.
Lastly, keep in mind that an offset is not necessarily enough to identify a time zone. Many time zones share offsets at some points in time, but differ in others. See "Time Zone != Offset" in the timezone tag wiki.
Oh, and about your results getting 2 in one direction and -3 in the other - this is a rounding error due to your integer division. If you print out the seconds value, you'll notice they are one second apart (10799, vs -10800). Dig closer and you'll find that "now" included fractional seconds that were truncated with the getSeconds call. (You called .now() twice, so they were at slightly different times.)
The behaviour that I see is very strange - sometimes LocalDateTime would be equal to ZonedDateTime, other times it will differ by 1 hour or 2 and sometimes it's 30 minutes. All these strange differences depend on the year that I subtract. Can someone explain what's happening? Tried jdk1.8.0_65 and jdk1.8.0_91, MacOS 10.11.5. I work with UTC:
ZoneOffset offset = ZoneOffset.UTC;
Here are some experiments. For 1919 values may differ by nano or milliseconds, which is expected:
assertEquals(
LocalDateTime.now(offset).minusYears(85).toInstant(offset),
ZonedDateTime.now().minusYears(85).withZoneSameInstant(offset).toInstant());
For 1919 it's 1 hour difference:
assertEquals(
LocalDateTime.now(offset).minusYears(86).toInstant(offset),
ZonedDateTime.now().minusYears(86).withZoneSameInstant(offset).toInstant());
Expected :<1930-05-28T20:19:10.383Z>
Actual :<1930-05-28T21:19:10.383Z>
For 1920 it's 2 hours difference:
assertEquals(
LocalDateTime.now(offset).minusYears(95).toInstant(offset),
ZonedDateTime.now().minusYears(95).withZoneSameInstant(offset).toInstant());
Expected :<1921-05-28T20:21:45.094Z>
Actual :<1921-05-28T18:21:45.094Z>
For 1921 again milli or nano seconds difference:
assertEquals(
LocalDateTime.now(offset).minusYears(96).toInstant(offset),
ZonedDateTime.now().minusYears(96).withZoneSameInstant(offset).toInstant());
And the weirdest of all - 1930 year with 30 mins difference:
assertEquals(
LocalDateTime.now(offset).minusYears(97).toInstant(offset),
ZonedDateTime.now().minusYears(97).withZoneSameInstant(offset).toInstant());
Expected :<1919-05-28T20:24:27.345Z>
Actual :<1919-05-28T19:53:08.346Z>
Update
As #Tunaki pointed I had to specify the offset for the ZonedDateTime:
assertEquals(
LocalDateTime.now(offset).minusYears(95).toInstant(offset),
ZonedDateTime.now(offset).minusYears(95).withZoneSameInstant(offset).toInstant());
The problem is that this doesn't know about Time Zones: LocalDateTime.now(offset).minusYears(97).toInstant(offset). There is only offset present. But this knows about the time zone: ZonedDateTime.now().minusYears(97).toInstant()
ZoneId contains information about the place and the time difference at that place. It knows that N years ago in that particular time zone the offset was 2 hours, not 3 as it is now.
ZoneOffset keeps track only about the hours/minutes shift. It doesn't know the history of time changes in a particular country. It just "adds hours".
The suggested (and a little bit incorrect) solution is to let ZonedDateTime to forget about zones and use offset instead: ZonedDateTime.now(offset).minusYears(97). This now would agree with LocalDateTime with the same offset - both would show same incorrect information. But they would agree since both "just add hours" instead of understanding the time difference at that point of history for that place:
ZoneOffset offset = ZoneId.of("Europe/Moscow").getRules().getOffset(Instant.now());
assertEquals(
LocalDateTime.now(offset).minusYears(97).toInstant(offset),
ZonedDateTime.now(offset).minusYears(97).toInstant());
Alternatively we can set the LocalDateTime to show the correct value for that time for that place:
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Europe/Moscow")).minusYears(97);
ZoneOffset offset = zoned.getOffset();//agrees with history
assertEquals(
LocalDateTime.now().minusYears(97).toInstant(offset),
zoned.toInstant());
PS: This all shows that ZonedDateTime can work differently in different situations - sometimes it knows about time zones, other times it just "adds hours" as you would do with LocalDateTime by setting the offset manually. To me this is a weird implementation. JodaTime probably is still the best Java implementation. At least you don't have to learn it for several days to understand it.
Since no one is answering this...
You have to send in offset when creating the ZonedDateTime as well. Otherwise it will use Clock.systemDefaultZone() and you get a difference in timezones.
ZonedDateTime.now(offset).minusYears(95).withZoneSameInstant(offset).toInstant()
Recently I stumbled over the hint to use Years.between() as JODA best practice to calculate someones age. It does not work in general as the following example demonstrates.
DateTime y0000 = new DateTime(0000, 1, 1, 0, 0, 0, 0);
DateTime y2000 = new DateTime(2000, 1, 1, 0, 0, 0, 0);
assertEquals(2000, new Period(y0000, y2000).getYears());
assertEquals(1999, Years.yearsBetween(y0000.toInstant(), y2000.toInstant()).getYears());
assertEquals(1999, new Period(y0000.toInstant(), y2000.toInstant()).getYears());
assertEquals(2000, new Period(new DateTime(y0000),new DateTime(y2000)).getYears());
Does JODA work here as designed or is this a JODA defect?
UPDATE:
JODA works as designed. If you really want to use Years.between() then either work with LocalDateTime or make sure to work with DateTimeZone.UTC.
From my comment (now transferred into this answer):
The result of 1999 for instant-conversion can be explained by the fact that timezone data are not well defined in far past. Joda-Time uses estimated zone offsets with second parts (so called LMT entries in the TZDB-database). This can result in different offsets for year 0 and year 2000 and hence decremented year delta. It is also important to understand that the timezone idea is an idea of the 19th century! So your code is meaningless ;-) The dark side by design here is also that Joda-Time enables code without making the timezone effect visible and explicit.
I have now investigated the offsets in my local timezone using this code:
System.out.println(DateTimeZone.getDefault().getOffset(y0000.toInstant()) / 1000); // 3208 s
System.out.println(DateTimeZone.getDefault().getOffset(y2000.toInstant()) / 1000); // 3600 s
The consequence is: You have to subtract the first smaller offset from y0000 and to subtract the bigger offset from y2000 so the delta in seconds between both instants cannot be equal to 2000 years expressed in seconds. Therefore the year delta is decremented.
In order to avoid such unexpected deltas (where you do obviously not think about any timezone effects) I strongly recommend to use types like LocalDateTime. This type has no timezone in contrast to DateTime and will do what you expect (of course without conversion to Instant). Otherwise, if you care about "exact" physical time differences (either by different summer/winter-time or historical estimation of offsets) then the expected year difference is not really correct i.e. should take into account the timezone offsets.
I'm trying to create a date with absolute values for the fields through JODA DateTime, for example (the example is in the epoch for easier examples):
new DateTime(1970, 1, 1, 1, 0, 0, 0, DateTimeZone.forId("GMT+0").toDate()
The JODA DateTime object correctly represents the hour as 1AM, if I inspect it all the absolute values are precise and correct (1 hour, 3600 seconds, 3600000 millis since the epoch, etc.) but as soon as toDate() and java.util.Date kicks in it becomes 2AM because he's adding the DST of my system's current time, even though I explicitly created the DateTime object with GMT+0 offset.
How can I ignore DST offset and just tell Java to create me a java.util.Date with the absolute values I want? Unfortunately I'm dealing with legacy code and I'm stuck with using java.util.Date so switching everything to Joda is not an option in the short term.
Thanks.
java.util.Date does not store any time zone or DST-offset itself because it just stores the elapsed milliseconds since UNIX epoch, not counting leap seconds.
What you observe is rather the output of the toString()-method of j.u.Date which indeed is dependent on your local (JVM) time zone! So you have no error, only that j.u.Date has confusing behaviour.
Added: If you want to have a "correct" display of your Date-object then you need a format tool like SimpleDateFormat which you can use for any formatted output you wish.
I would like to know exactly how many months and days(possibly years) some older date is from today. Is there a method to do that?
I know how to get the difference of the months, I know how to get the difference in days. But I am unable to get the months and the days.
Ex:
old = '2013-03-04'
now = '2013-04-17'
so the result im looking for is something like 1 month(s) and 13* day(s)
*maybe its 12 im not every sure.
This can be done by using Period in JodaTime.
For example,
LocalDate old = new LocalDate(2013, 3, 4);
LocalDate now = new LocalDate(2013, 4, 17);
Period p = new Period(old, now, PeriodType.yearMonthDay());
To get the months, use p.getMonths(), to get the days p.getDays().
The result of the example is 1 month, 13 days.
Yes, see the documentation of intervals:
Intervals
An interval in Joda-Time represents an interval of time from one
instant to another instant. Both instants are fully specified instants
in the datetime continuum, complete with time zone.
Intervals are implemented as half-open, which is to say that the start
instant is inclusive but the end instant is exclusive. The end is
always greater than or equal to the start. Both end-points are
restricted to having the same chronology and the same time zone.
Two implementations are provided, Interval and MutableInterval, both
are specializations of ReadableInterval.