Iterating through ZonedDateTime interval in Duration steps - java

Problem
Given a start and end timestamp and a duration I'd like to iterate through that time interval in steps of the duration. The duration should be specified in ISO 8601 notation. Daylight saving time should be considered depending on the timezone.
Example code:
// start/end at switch from summer to winter time
ZonedDateTime startTimestamp = ZonedDateTime.of( LocalDateTime.of(2018, 10, 28, 0, 0), ZoneId.of("CET"));
ZonedDateTime endTimestamp = startTimestamp.plusHours(5);
Duration duration = Duration.parse( "PT1H");
while( startTimestamp.isBefore(endTimestamp)) {
System.out.println( startTimestamp);
startTimestamp = startTimestamp.plus( duration);
}
Which results in:
2018-10-28T00:00+02:00[CET]
2018-10-28T01:00+02:00[CET]
2018-10-28T02:00+02:00[CET]
2018-10-28T02:00+01:00[CET]
2018-10-28T03:00+01:00[CET]
The problem is that this works as long as the duration is days at max. From the Duration parser documentation:
There are then four sections, each consisting of a number and a suffix. The sections have suffixes in ASCII of "D", "H", "M" and "S" for days, hours, minutes and seconds, accepted in upper or lower case.
But the ISO 8601 standard specifies that a duration might also be in months and years.
Durations define the amount of intervening time in a time interval and
are represented by the format P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
Question
How do you properly iterate in ISO 8601 duration steps through a ZonedDateTime interval considering calendar elements of Week, Month, Year?
Example for Month:
Start: 01.01.2018
End: 01.01.2019
I'd like to get every 1st of each month. Specifying P1M as duration throws of course this exception:
Exception in thread "main" java.time.format.DateTimeParseException:
Text cannot be parsed to a Duration

To work with date related fields (years, months and days), you must use a java.time.Period. Example:
ZonedDateTime startTimestamp = ZonedDateTime.of(LocalDateTime.of(2018, 1, 1, 0, 0), ZoneId.of("CET"));
ZonedDateTime endTimestamp = startTimestamp.plusMonths(5);
Period period = Period.parse("P1M");
while (startTimestamp.isBefore(endTimestamp)) {
System.out.println(startTimestamp);
startTimestamp = startTimestamp.plus(period);
}
This prints:
2018-01-01T00:00+01:00[CET]
2018-02-01T00:00+01:00[CET]
2018-03-01T00:00+01:00[CET]
2018-04-01T00:00+02:00[CET]
2018-05-01T00:00+02:00[CET]
Unfortunately, java.time has divided ISO8601 durations in 2 classes, where Period works with date-based fields, while Duration works with time-based fields.
Alternative
If you don't mind adding a dependency to your application, you can use the threeten extra lib: http://www.threeten.org/threeten-extra/
It contains the class PeriodDuration, that encapsulates both a Period and a Duration, so both "P1M" or "PT1H" will work:
// this works
PeriodDuration period = PeriodDuration.parse("P1M");
// this too
PeriodDuration period = PeriodDuration.parse("PT1H");
And the plus method can receive a PeriodDuration, because it also implements TemporalAmount.

Try with a do while loop.
ZonedDateTime startTimestamp = ZonedDateTime.of(LocalDateTime.of(2018, 4, 20, 12, 10), ZoneId.of("CET"));
ZonedDateTime endTimestamp = startTimestamp.plusHours(5);
Duration duration = Duration.parse("PT1H");
do {
System.out.println(startTimestamp.toLocalTime());
startTimestamp = endTimestamp.plus(duration);
} while (startTimestamp.isBefore(endTimestamp));
You have to re-initialize the startTimeStamp with adding duration in to the end timestamp.

Related

How would I convert the exact amount of time (years, months, days, hours, minutes, seconds) between two Calendar objects?

I am trying to covert the exact amount of time between two Calendar objects in Java.
This is the code that I currently have...
public static Map<TimeUnit, Long> computeDifference(Calendar date1, Calendar date2) {
long diffInMillies = date2.getTimeInMillis() - date1.getTimeInMillis();
//create the list
List<TimeUnit> units = new ArrayList<TimeUnit>();
units.add(TimeUnit.SECONDS);
units.add(TimeUnit.MINUTES);
units.add(TimeUnit.HOURS);
units.add(TimeUnit.DAYS);
Collections.reverse(units);
//create the result map of TimeUnit and difference
Map<TimeUnit,Long> result = new LinkedHashMap<TimeUnit,Long>();
long milliesRest = diffInMillies;
for ( TimeUnit unit : units ) {
//calculate difference in millisecond
long diff = unit.convert(milliesRest,TimeUnit.MILLISECONDS);
long diffInMilliesForUnit = unit.toMillis(diff);
milliesRest = milliesRest - diffInMilliesForUnit;
//put the result in the map
result.put(unit,diff);
}
return result;
}
When printed, the output looks like this {DAYS=1, HOURS=10, MINUTES=30, SECONDS=45} for input date1 = 19 August 2019 02:00:00 and date2 = 20 August 2019 12:30:45
The largest time unit available in this method is DAYS, but I want to find something that includes both months and years. I realize that TimeUnit doesn't really have anything to do with specific calendar dates (rather a 24-hour interval), which is why I was wondering if there is any way to make this conversion using the Calendar class or something similar. I've also looked into ChronoUnit as a substitute for TimeUnit, but that won't work for the same reason TimeUnit doesn't.
Would love any suggestions for how to incorporate larger time units. Thank you!
You shouldn't use the Calendar class, since it's obsolete. You should use classes from the java.time package instead.
In order to get the desired result, you could use the Period class.
You first need to convert both Calendar instances to LocalDate instances.
Then you could use Period.between(startDate, endDate) to get a Period instance, which makes the getDays(), getMonths() and getYears() methods available to you.
If you also want to include time components (hours, minutes and seconds), then you could use Duration in combination with Period. But then first read the post linked to by Sweeper.
Something like this would probably work:
LocalDateTime start = LocalDateTime.of(2019, 1, 1, 12, 0, 0);
LocalDateTime end = LocalDateTime.of(2021, 4, 26, 5, 56, 40);
Duration d = Duration.between(start.toLocalTime(), end.toLocalTime());
Period p = Period.between(start.toLocalDate(), end.toLocalDate());
// If the startdate's time component lies behind the enddate's time component,
// then we need to correct both the Period and Duration
if (d.isNegative()) {
p = p.minusDays(1);
d = d.plusDays(1);
}
System.out.printf("y %s m %s d %s h %s m %s s %s%n",
p.getYears(),
p.getMonths(),
p.getDays(),
d.toHours() % 24,
d.toMinutes() % 60,
d.getSeconds() % 60);
Note that Java 9 comes with to…Part methods, so you don't have to use the modulo operator anymore.
Be advised: this code does not take into account clock adjustments due to daylight savings time.
tl;dr
Period
.between(
( ( GregorianCalendar ) myCalStart ).toZonedDateTime().toLocalDate() ,
( ( GregorianCalendar ) myCalStop ).toZonedDateTime().toLocalDate()
)
…or…
Duration
.between(
( ( GregorianCalendar ) myCalStart ).toInstant() ,
( ( GregorianCalendar ) myCalStop ).toInstant()
)
java.time
You are using terrible date-time classes that were supplanted years ago by the modern java.time classes defined in JSR 310.
Never use Calendar, GregorianCalendar, Date, SimpleDateFormat, and such. Use only the classes found in the java.time packages.
ZonedDateTime
Assuming both your Calendar objects are actually GregorianCalendar objects underneath, convert. To convert, call new to…/from… methods added to the old classes.
// start
GregorianCalendar gcStart = ( GregorianCalendar ) myCalStart ;
ZonedDateTime zdtStart = gcStart.toZonedDateTime() ;
// stop
GregorianCalendar gcStop = ( GregorianCalendar ) myCalStop ;
ZonedDateTime zdtStop = gcStop.toZonedDateTime() ;
Both GregorianCalendar and ZonedDateTime represent a date with a time-of-day placed in the context of a time zone, combined to determine a moment (a specific point on the timeline). ZonedDateTime resolves to a finer level of nanoseconds rather than milliseconds.
Period
If you care about elapsed time in terms of years-months-days, use Period with LocalDate objects. LocalDate represents a date without a time-of-day and without a time zone. We can extract the date portion from our ZonedDateTime objects.
LocalDate ldStart = zdtStart.toLocalDate() ;
LocalDate ldStop = zdtStop.toLocalDate() ;
Period p = Period.between( ldStart , ldStop ) ;
Generate a string in standard ISO 8601 format.
String output = p.toString() ;
Interrogate for a count of years, months, days.
int years = p.getYears() ;
int months = p.getMonths() ;
int days = p.getDays() ;
Duration
If you cane about elapsed time in terms of hours-minutes-seconds, use Duration. This class represent a pair of moments in UTC. So we extract Instant objects from our pair of ZonedDateTime objects, which internally keep a count of whole seconds since the epoch reference of first moment of 1970 in UTC, plus a fractional second as a count of nanoseconds.
Instant instantStart = zdtStart.toInstant() ;
Instant instantStop = zdtStop.toInstant() ;
Duration d = Duration.between( instantStart , instantStop ) ;
Generate a string in standard ISO 8601 format.
String output = d.toString() ;
Interrogate for a count of days (as 24-hour chunks of time unrelated to the calendar), hours, minutes, seconds.
long days = d.toDaysPart() ;
int hours = d.toHoursPart() ;
int minutes = d.toMinutesPart() ;
int seconds = d.toSecondsPart() ;
int nanos = d.toNanosPart() ;
PeriodDuration
If you think about it, you will see that it does not make sense to combine years/months/days with 24-hour-days/hours/minutes/seconds. See this Stack Overflow page as food for thought.
But if you insist on combining these two different concepts, see the PeriodDuration class found in the ThreeTen-Extra library.

converting epoch to ZonedDateTime in Java

How to convert epoch like 1413225446.92000 to ZonedDateTime in java?
The code given expects long value hence this will throw NumberFormatException for the value given above.
ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(dateInMillis)), ZoneId.of(TIME_ZONE_PST));
java.time can directly parse your string
Edit: If your millisecond value is always non-negative, the following DateTimeFormatter can parse it.
private static final String TIME_ZONE_PST = "America/Los_Angeles";
private static final DateTimeFormatter epochFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
.optionalStart()
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.optionalEnd()
.toFormatter()
.withZone(ZoneId.of(TIME_ZONE_PST));
Now parsing into a ZonedDateTime is just one method call:
ZonedDateTime zdt = ZonedDateTime.parse(dateInMillis, epochFormatter);
System.out.println(zdt);
Output is:
2014-10-13T11:37:26.920-07:00[America/Los_Angeles]
It will not work correctly with a negative value: the fraction would still be parsed as positive, which I am assuming would be incorrect. To be sure to be notified in case of a negative value I have specified in the formatter that the number cannot be signed.
A more general solution: use BigDecimal
If you need a more general solution, for example including negative numbers, I think it’s best to let BigDecinmal parse the number and do the math.
BigDecimal bd = new BigDecimal(dateInMillis);
BigDecimal[] wholeAndFractional = bd.divideAndRemainder(BigDecimal.ONE);
long seconds = wholeAndFractional[0].longValueExact();
int nanos = wholeAndFractional[1].movePointRight(9).intValue();
ZonedDateTime zdt = Instant.ofEpochSecond(seconds, nanos)
.atZone(ZoneId.of(TIME_ZONE_PST));
Output is the same as before. Only now we can also handle negative numbers according to expectations:
String dateInMillis = "-1.5";
1969-12-31T15:59:58.500-08:00[America/Los_Angeles]
Even scientific notation is accepted:
String dateInMillis = "1.41322544692E9";
2014-10-13T11:37:26.920-07:00[America/Los_Angeles]
If finer precision than nanoseconds is possible in the string, consider how you want to truncate or round, and instruct BigDecimal accordingly, there are a number of options.
Original answer
Basil Bourque’s answer is a good one. Taking out the nanoseconds from the fractional part into an integer for nanoseconds may entail a pitfall or two. I suggest:
String dateInMillis = "1413225446.92000";
String[] secondsAndFraction = dateInMillis.split("\\.");
int nanos = 0;
if (secondsAndFraction.length > 1) { // there’s a fractional part
// extend fractional part to 9 digits to obtain nanoseconds
String nanosecondsString
= (secondsAndFraction[1] + "000000000").substring(0, 9);
nanos = Integer.parseInt(nanosecondsString);
// if the double number was negative, the nanos must be too
if (dateInMillis.startsWith("-")) {
nanos = -nanos;
}
}
ZonedDateTime zdt = Instant
.ofEpochSecond(Long.parseLong(secondsAndFraction[0]), nanos)
.atZone(ZoneId.of("Asia/Manila"));
System.out.println(zdt);
This prints
2014-10-14T02:37:26.920+08:00[Asia/Manila]
We don’t need 64 bits for the nanoseconds, so I am just using an int.
Assumption: I have assumed that your string contains a floating-point number and that it may be signed, for example -1.50 would mean one and a half seconds before the epoch. If one day your epoch time comes in scientific notation (1.41322544692E9), the above will not work.
Please substitute your desired time zone in the region/city format if it didn’t happen to be Asia/Manila, for example America/Vancouver, America/Los_Angeles or Pacific/Pitcairn. Avoid three letter abbreviations like PST, they are ambiguous and often not true time zones.
Split the number into a pair of 64-bit long integers:
Number of whole seconds since the epoch reference date of first moment of 1970 in UTC
A number of nanoseconds for the fractional second
Pass those numbers to the factory method Instant.ofEpochSecond​(long epochSecond, long nanoAdjustment)
With an Instant in hand, proceed to assign a time zone to get a ZonedDateTime.
ZoneId z = ZoneId.of( "America/Los_Angeles" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;
Expanding on Basil's and Ole's answers here, for the special case of a negative timestamp i.e. before epoch. Is that even possible? Here's what Jon Skeet writes in "All about java.util.Date":
The Date class uses “milliseconds since the Unix epoch” – that’s the
value returned by getTime(), and set by either the Date(long)
constructor or the setTime() method. As the moon walk occurred before
the Unix epoch, the value is negative: it’s actually -14159020000.
The only real difference between Ole's answer (besides a few extra asserts) is that here, we do not reverse the sign on nanos if the date string starts with a negative sign. The reason for this is, when passing the nanos to the Instant constructor, that is an adjustment, so if we send the nanos as a negative, it will actually adjust the seconds back, and thus the entire ZonedDateTime value is off by the nanos.
This is from the JavaDoc, note the interesting behavior:
This method allows an arbitrary number of nanoseconds to be passed in.
The factory will alter the values of the second and nanosecond in
order to ensure that the stored nanosecond is in the range 0 to
999,999,999. For example, the following will result in the exactly the
same instant:
Instant.ofEpochSecond(3, 1);
Instant.ofEpochSecond(4,-999_999_999);
Instant.ofEpochSecond(2, 1000_000_001);
So the 2nd argument, nanos, we are not setting the value, it is an adjustment. So just the same as for a positive timestamp (after epoch), we want to send in the actual nanos.
Taking Ole's code as a base and adding the above mentioned changes:
String strDateZoned = "Jul 20 1969 21:56:20.599 CDT"; // yes, should use America/Chicago here as Ole points out
DateTimeFormatter dtfFormatter = DateTimeFormatter.ofPattern("MMM dd yyyy HH:mm:ss.SSS zzz");
ZonedDateTime originalZoned = ZonedDateTime.parse(strDateZoned, dtfFormatter);
long epochSeconds = originalZoned.toInstant().getEpochSecond();
int nanoSeconds = originalZoned.toInstant().getNano();
String dateInMillis = epochSeconds + "." + nanoSeconds;
String[] secondsAndFraction = dateInMillis.split("\\.");
int nanos = 0;
if (secondsAndFraction.length > 1) { // there’s a fractional part
// extend fractional part to 9 digits to obtain nanoseconds
String nanosecondsString
= (secondsAndFraction[1] + "000000000").substring(0, 9);
nanos = Integer.parseInt(nanosecondsString);
}
ZonedDateTime zdt = Instant
.ofEpochSecond(Long.parseLong(secondsAndFraction[0]), nanos)
.atZone(ZoneId.of("America/Chicago"));
String formattedZdt = dtfFormatter.format(zdt);
System.out.println("zoneDateTime expected = " + strDateZoned);
System.out.println("zoneDateTime from millis = " + formattedZdt);
assertEquals("date in millis is wrong", "-14159020.599000000", dateInMillis);
assertEquals("date doesn't match expected",strDateZoned, dtfFormatter.format(zdt));
Output from code:
zoneDateTime expected = Jul 20 1969 21:56:20.599 CDT
zoneDateTime from millis = Jul 20 1969 21:56:20.599 CDT
If we reverse the sign on nanos for the case where the seconds part is negative, we can see the difference in the formatted ZonedDateTime:
org.junit.ComparisonFailure: date doesn't match expected
Expected :Jul 20 1969 21:56:20.599 CDT
Actual :Jul 20 1969 21:56:19.401 CDT
P.S. A few more thoughts from the 'All About Dates' post on what Jon Skeet calls "leniency", and elsewhere I have seen called 'normalization' which is perhaps due to POSIX influences:
It’s lenient for no obvious reason: “In all cases, arguments given to
methods for these purposes need not fall within the indicated ranges;
for example, a date may be specified as January 32 and is interpreted
as meaning February 1.” How often is that useful?

Issues Converting String dates into Long values and performing calculations

I have a map of string values which represent down times for different components.
dependencyMap.put ("sut", "14:26:12,14:27:19,00:01:07;15:01:54,15:02:54,00:01:00;15:44:30,15:46:30,00:02:00;16:10:30,16:11:30,00:01:00");
dependencyMap.put ("jms", "14:26:12,14:28:12,00:02:00;15:10:50,15:12:55,00:02:05;15:42:30,15:43:30,00:01:00;16:25:30,16:27:30,00:02:00");
The strings represent the start, end and duration of down times.
(start)14:26:12,(end)14:27:19,(duration)00:01:07
I read the values in, then add them to a list of DependencyDownTime objects which hold the Long values startTime, endTime and duration.
jArray.forEach (dependency ->{
String downTimeValues = knownDowntimesMap.get(dependency);
final String[] downtime = downTimeValues.split (";");
for (final String str : downtime) {
final DependencyDownTime depDownTime = new DependencyDownTime ();
final String[] strings = str.split (",");
if (strings.length == 3) {
final DateFormat dateFormat = new SimpleDateFormat ("HH:mm:ss");
try {
depDownTime.setStartTime(dateFormat.parse (strings[0]).getTime ());
depDownTime.setEndTime (dateFormat.parse (strings[1]).getTime ());
depDownTime.setDuration (dateFormat.parse (strings[2]).getTime ());
downTimes.add (depDownTime);
} catch (final ParseException e) {
//logger.warn (e.getMessage (), e);
}
} else {
//logger.warn ("");
}
}
I then perform simple arithmetic on the values, which calculates the total down time for each component.
// sort the list by start time
Collections.sort(downTimes, Comparator.comparing (DependencyDownTime::getStartTime));
int i = 1;
Long duration = 0L;
for(DependencyDownTime dts: downTimes){
Long curStart = dts.getStartTime ();
Long curEnd = dts.getEndTime();
Long nextStart = downTimes.get(i).getStartTime ();
Long nextEnd = downTimes.get(i).getEndTime ();
if(duration == 0){
duration = dts.getDuration();
}
if(curStart.equals(nextStart) && curEnd < nextEnd){
duration += (nextEnd - curEnd);
}
else if(nextStart > curEnd){
duration += downTimes.get(i).getDuration();
}
else if( curStart < nextStart && curEnd > nextStart){
duration += (nextEnd - curEnd);
}
else if(curEnd == nextStart){
duration += downTimes.get(i).getDuration();
}
i++;
if(i == downTimes.size ()){
componentDTimeMap.put (application, duration);
return;
}
The expected values should be something like 1970-01-01T 00:14:35 .000+0100, a matter of minutes. The actual result is usually extremely high off by a matter of hours in the difference 1969-12-31T 15:13:35 .000+0100
I have 2 questions.
Am I parsing the values correctly?
If my calculations are a little off when adding and subtracting the long values. When I convert the values back to Date format will there be a drastic difference in the expected value?
As explained in your other question, don't mistake those 2 different concepts:
a time of the day: it represents a specific point of a day, such as 10 AM or 14:45:50
a duration: it represents an amount of time, such as "1 hour and 10 minutes" or "2 years, 3 months and 4 days". The duration doesn't tell you when it starts or ends ("1 hour and 10 minutes" relative to what?), it's not attached to a chronology, it doesn't correspond to a specific point in the timeline. It's just the amount of time, by itself.
In your input, you have:
(start)14:26:12,(end)14:27:19,(duration)00:01:07
The start and end represents times of the day, and the duration represents the amount of time. SimpleDateFormat is designed to work with dates and times of the day, but not with durations. Treating the duration as a time of the day might work, but it's a hack as explained in this answer.
Another problem is that when SimpleDateFormat parses only a time, it defaults the day to January 1st 1970 at the JVM default timezone, leading to all the strange results you see. Unfortunately there's no way to avoid that, as java.util.Date works with full timestamps. A better alternative is to use the new date/time API.
As in your other question you're using Java 8, I'm assuming you can also use it here (but if you're using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport (or Android's ThreeTenABP) is org.threeten.bp), but the classes and methods names are the same).
As you're working only with times, there's no need to consider date fields (day/month/year), we can use a LocalTime instead. You can parse the strings directly, because they are in ISO861 compliant format:
LocalTime start = LocalTime.parse("14:26:12");
LocalTime end = LocalTime.parse("14:27:19");
Unfortunately there are no built-in parsers for a duration, so you'll have to parse it manually:
// parse the duration manually
String[] parts = "00:01:07".split(":");
Duration d = Duration
// get hours
.ofHours(Long.parseLong(parts[0]))
// plus minutes
.plusMinutes(Long.parseLong(parts[1]))
// plus seconds
.plusSeconds(Long.parseLong(parts[2]));
Another alternative is to remove the durations from your input (or ignore them) and calculate it using the start and end:
Duration d = Duration.between(start, end);
Both will give you a duration of 1 minute and 7 seconds.
My suggestion is to change the DependencyDownTime to store start and end as LocalTime objects, and the duration as a Duration object. With this, your algorithm would be like this:
Duration total = Duration.ZERO;
for (...) {
LocalTime curStart = ...
LocalTime curEnd = ...
LocalTime nextStart = ...
LocalTime nextEnd = ...
if (total.toMillis() == 0) {
duration = dts.getDuration();
}
if (curStart.equals(nextStart) && curEnd.isBefore(nextEnd)) {
total = total.plus(Duration.between(curEnd, nextEnd));
} else if (nextStart.isAfter(curEnd)) {
total = total.plus(downTimes.get(i).getDuration());
} else if (curStart.isBefore(nextStart) && curEnd.isAfter(nextStart)) {
total = total.plus(Duration.between(curEnd, nextEnd));
} else if (curEnd.equals(nextStart)) {
total = total.plus(downTimes.get(i).getDuration());
}
i++;
if (i == downTimes.size()) {
// assuming you want the duration as a total of milliseconds
componentDTimeMap.put(application, total.toMillis());
return;
}
}
You can either store the Duration object, or the respective value of milliseconds. Don't try to transform it to a Date, because a date is not designed nor supposed to work with durations. You can adapt this code to format a duration if you want (unfortunately there are no native formatters for durations).
Limitations
The code above assumes that all start and end times are in the same day. But if you have start at 23:50 and end at 00:10, should the duration be 20 minutes?
If that's the case, it's a little bit trickier, because LocalTime is not aware of the date (so it considers 23:50 > 00:10 and the duration between them is "minus 23 hours and 40 minutes").
In this case, you could do a trick and assume the dates are all at the current date, but when start is greater than end, it means that end time is in the next day:
LocalTime start = LocalTime.parse("23:50");
LocalTime end = LocalTime.parse("00:10");
// calculate duration
Duration d;
if (start.isAfter(end)) {
// start is after end, it means end is in the next day
// current date
LocalDate now = LocalDate.now();
// start is at the current day
LocalDateTime startDt = now.atTime(start);
// end is at the next day
LocalDateTime endDt = now.plusDays(1).atTime(end);
d = Duration.between(startDt, endDt);
} else {
// both start and end are in the same day
// just calculate the duration in the usual way
d = Duration.between(start, end);
}
In the code above, the result will be a Duration of 20 minutes.
Don't format dates as durations
Here are some examples of why SimpleDateFormat and Date aren't good to handle durations of time.
Suppose I have a duration of 10 seconds. If I try to transform it to a java.util.Date using the value 10 to a date (AKA treating a duration as a date):
// a 10 second duration (10000 milliseconds), treated as a date
Date date = new Date(10 * 1000);
System.out.println(date);
This will get a date that corresponds to "10000 milliseconds after unix epoch (1970-01-01T00:00Z)", which is 1970-01-01T00:00:10Z. But when I print the date object, the toString() method is implicity called (as explained here). And this method converts this millis value to the JVM default timezone.
In the JVM I'm using, the default timezone is America/Sao_Paulo, so the code above outputs:
Wed Dec 31 21:00:10 BRT 1969
Which is not what is expected: the UTC instant 1970-01-01T00:00:10Z corresponds to December 31st 1969 at 9 PM in São Paulo timezone.
This happens because I'm erroneously treating the duration as a date (and the output will be different, depending on the default timezone configured in the JVM).
A java.util.Date can't (must not) be used to work with durations. Actually, now that we have better API's, it should be avoided whenever possible. There are too many problems and design issues with this, just don't use it if you can.
SimpleDateFormat also won't work properly if you handle the durations as dates. In this code:
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
Date d = dateFormat.parse("10:00:00");
The input has only time fields (hour, minute and second), so SimpleDateFormat sets the date to January 1st 1970 at the JVM default timezone. If I System.out.println this date, the result will be:
Thu Jan 01 10:00:00 BRT 1970
That's January 1st 1970 at 10 AM in São Paulo timezone, which in UTC is equivalent to 1970-01-01T13:00:00Z - so d.getTime() returns 46800000.
If I change the JVM default timezone to Europe/London, it will create a date that corresponds to January 1st 1970 at 10 AM in London (or UTC 1970-01-01T09:00:00Z) - and d.getTime() now returns 32400000 (because 10 AM in London and 10 AM in São Paulo happened at different instants).
SimpleDateFormat isn't the right tool to work with durations - it isn't even the best tool to work with dates, actually.

How to Convert Modified Julian Day Numbers with the Java 8 DateTime API

I have a database that stores Dates and DateTimes (as INTEGERs and DOUBLEs, respectively) as Modified Julian Day Numbers (MJD). Modified Julian Day Numbers are a consecutive count of days from midnight UTC, 17 November 1858. By definition they are always reckoned in UTC, have an offset of +0:00 from GMT, and do not adjust for daylight savings. These properties simplify certain operations with DateTimes such as precedence and date arithmetic.
The downside is that MJDs must be relocalized from UTC and delocalized back to UTC before and after use, particularly for applications for which day boundaries are critically important (Medicare, for example, recognizes a billable date boundary as midnight in -local- time).
Consider the following static factory method whose purpose is to delocalize into an MJD (in UTC) a "regional day number" (basically, an MJD that has had the appropriate offset added to it so that it represents a local DateTime):
public static MJD ofDayNumberInZone(double regDN, ZoneId zone) {
:
:
}
It seems intuitively obvious that if you have a local date and time, and you know the local time zone, that you should have all the information you need in order to offset regDN back to UTC (as required by an MJD).
In fact, this function is fairly simple to write using the previous Java Calendar API. The regDN is easily converted to a Date which is used to set a GregorianCalendar instance. Knowing the "local time zone" the calendar reports ZONE_OFFSET and DST_OFFSET values that can then be used to adjust the day number into an MJD.
This is my attempt to write a similar algorithm in the Java 8 DateTime API:
public static MJD ofDayNumberInZone(double zonedMJD, ZoneId zone) {
double epochSec = ((zonedMJD - MJD.POSIX_EPOCH_AS_MJD) * 86400.0);
LocalDateTime dt = LocalDateTime
.ofEpochSecond(
(long) epochSec,
(int) (epochSec - Math.floor(epochSec) * 1000000000.0),
---> zone.getRules().getOffset( <Instant> )
);
}
The problem is indicated at the arrow. Constructing a LocalDateTime instance using the ofEpochSecond method seems to require that you know the offsets in advance, which seems counterintuitive (I have the local time and the time zone already, it's the offset I want).
I haven't been successful in finding a simple way to obtain the offsets from local time back to UTC using the Java 8 API. While I could continue to use the old Calendar API, the new DateTime libraries offer compelling advantages ... so I'd like to try and figure this out. What am I missing?
EDIT: Here is an example, using the old Java Calendar API, of how a count of days and fractional days in an arbitrary time zone is "deregionalized" into UTC. This method takes a double which is the "regionalized day number" and a time zone object. It uses a GregorianCalendar to convert the parameters into a UTC count of milliseconds from the Epoch:
private static final Object lockCal = new Object();
private static final SimpleDateFormat SDF = new SimpleDateFormat();
private static final GregorianCalendar CAL = new
GregorianCalendar(TimeZone.getTimeZone(HECTOR_ZONE));
:
:
public static MJD ofDayNumberInZone(double rdn, TimeZone tz) {
Date dat = new Date((long) ((rdn - MJD.POSIX_EPOCH_AS_MJD) *
(86400.0 * 1000.0)));
return MJD.ofDateInZone(dat, tz);
}
public static MJD ofDateInZone(Date dat, TimeZone tz) {
long utcMillisFromEpoch;
synchronized(lockCal) {
CAL.setTimeZone(tz);
CAL.setTime(dat);
utcMillisFromEpoch = CAL.getTimeInMillis();
}
return MJD.ofEpochMillisInUTC(utcMillisFromEpoch);
}
public static MJD ofEpochMillisInUTC(long millis)
{ return new MJD((millis / (86400.0 * 1000.0)) + POSIX_EPOCH_AS_MJD); }
Per your comments, your core issue seems to be about the ambiguity of converting a date-time without time zone (a LocalDateTime) into a zoned moment (a ZonedDateTime). You explain that anomalies such as Daylight Saving Time (DST) can result in invalid values.
ZonedDateTime zdt = myLocalDateTime.atZone( myZoneId );
This is true. There is no perfect solution when landing in the DST “Spring-forward” or ”Fall-back” cutovers. However, the java.time classes do resolve the ambiguity by adopting a certain policy. You may or may not agree with that policy. But if you do agree, then you can rely on java.time to determine a result.
To quote the documentation for ZonedDateTime.ofLocal:
In the case of an overlap, where clocks are set back, there are two valid offsets. If the preferred offset is one of the valid offsets then it is used. Otherwise the earlier valid offset is used, typically corresponding to "summer".
In the case of a gap, where clocks jump forward, there is no valid offset. Instead, the local date-time is adjusted to be later by the length of the gap. For a typical one hour daylight savings change, the local date-time will be moved one hour later into the offset typically corresponding to "summer".
LocalDate modifiedJulianEpoch = LocalDate.of( 1858 , 11 , 17 );
LocalDate today = LocalDate.now( ZoneOffset.UTC );
long days = ChronoUnit.DAYS.between ( modifiedJulianEpoch , today );
today: 2017-03-19
days: 57831
I do not quite understand your issues. But it seems to me that the point of MJD (Modified Julian Days) is to have a way to track a “One True Time” to avoid all the confusion of time zones. In standard ISO 8601 calendar system, UTC plays than role of “One True Time”. So I suggest sticking to UTC.
When you need to consider a region’s wall-clock time, such as your Medicare example of the region’s end-of-day, determine the regional wall-clock time and then convert to UTC. The Instant class in java.time is always in UTC by definition.
ZoneId z = ZoneId.of( "America/Los_Angeles" );
LocalDate localDate = LocalDate.now( z );
ZonedDateTime firstMomentNextDay = localDate.plusDays( 1 ).atStartOfDay( z );
Instant medicareExpiration = firstMomentNextDay.toInstant(); // UTC
BigDecimal modJulDays = this.convertInstantToModifiedJulianDays( medicareExpiration ) ;
Use BigDecimal when working with fractional decimals where accuracy matters. Using double, Double, float, or Float means using Floating-Point technology that trades away accuracy for faster performance.
Here is a rough-cut at some code to do the conversion from BigDecimal (Modified Julian Days) to Instant. I suppose some clever person might find a leaner or meaner version of this code, but my code here seems to be working. Use at your own risk. I barely tested this code at all.
public Instant convertModifiedJulianDaysToInstant ( BigDecimal modJulDays ) {
Instant epoch = OffsetDateTime.of ( 1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC ).toInstant ( ); // TODO: Make into a constant to optimize.
long days = modJulDays.toBigInteger ( ).longValue ( );
BigDecimal fractionOfADay = modJulDays.subtract ( new BigDecimal ( days ) ); // Extract the fractional number, separate from the integer number.
BigDecimal secondsFractional = new BigDecimal ( TimeUnit.DAYS.toSeconds ( 1 ) ).multiply ( fractionOfADay );
long secondsWhole = secondsFractional.longValue ( );
long nanos = secondsFractional.subtract ( new BigDecimal ( secondsWhole ) ).multiply ( new BigDecimal ( 1_000_000_000L ) ).longValue ( );
Duration duration = Duration.ofDays ( days ).plusSeconds ( secondsWhole ).plusNanos ( nanos );
Instant instant = epoch.plus ( duration );
return instant;
}
And going the other direction.
public BigDecimal convertInstantToModifiedJulianDays ( Instant instant ) {
Instant epoch = OffsetDateTime.of ( 1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC ).toInstant ( ); // TODO: Make into a constant to optimize.
Duration duration = Duration.between ( epoch, instant );
long wholeDays = duration.toDays ( );
Duration durationRemainder = duration.minusDays ( wholeDays );
BigDecimal wholeDaysBd = new BigDecimal ( wholeDays );
BigDecimal partialDayInNanosBd = new BigDecimal ( durationRemainder.toNanos ( ) ); // Convert entire duration to a total number of nanoseconds.
BigDecimal nanosInADayBd = new BigDecimal ( TimeUnit.DAYS.toNanos ( 1 ) ); // How long is a standard day in nanoseconds?
int scale = 9; // Maximum number of digits to the right of the decimal point.
BigDecimal partialDayBd = partialDayInNanosBd.divide ( nanosInADayBd ); // Get a fraction by dividing a total number of nanos in a day by our partial day of nanos.
BigDecimal result = wholeDaysBd.add ( partialDayBd );
return result;
}
Calling those conversion methods.
BigDecimal input = new BigDecimal ( "57831.5" );
Instant instant = this.convertModifiedJulianDaysToInstant ( input );
BigDecimal output = this.convertInstantToModifiedJulianDays ( instant );
Dump to console.
System.out.println ( "input.toString(): " + input );
System.out.println ( "instant.toString(): " + instant );
System.out.println ( "output.toString(): " + output );
input.toString(): 57831.5
instant.toString(): 2017-03-19T12:00:00Z
output.toString(): 57831.5
See all that code running live at IdeOne.com.
Also, my Answer to a similar Question may be helpful.

How to sum two times in Java, using LocalTime?

Example:
LocalTime time1 = LocalTime.of(12, 30);
LocalTime time2 = LocalTime.of(8, 30);
time1 + time2 // Doesn't work.
time1.plus(time2) // Doesn't work.
I want to get the sum of the two times (12:30 + 8:30 = 21:00) in the format of (hours:minutes).
Any other suggestions?
You are trying to add two LocalTime variables. This is wrong as a concept. Your time2 should not be a LocalTime, it should be a Duration. A duration added to a time gives you another time. A time subtracted from a time gives you a duration. It is all nice and logical. Adding two times together is not.
It is possible with some hacking to convert your time to a duration, but I would strongly advise against that. Instead, restructure your code so that time2 is a Duration in the first place.
You can do the following...
LocalTime t1 = LocalTime.of(9, 0); // 09:00
LocalTime t2 = LocalTime.of(2, 30); // 02:30
LocalTime total = t1.plusHours(t2.getHour())
.plusMinutes(t2.getMinute()); // 11:30
The answer of Mike Nakis does not true.
The above sentence of mine doesn't true. I have checked and only Java 8 has LocalTime.of so Mike Nakis's answer is perfectly true. Please see his answer.
[This section still keep. in case LocalTime in joda library ]
I will explain:
A duration in Joda-Time represents a duration of time measured in milliseconds. The duration is often obtained from an interval. i.e. we
can subtract start from end of an interval to derive a duration.
A period in Joda-Time represents a period of time defined in terms of fields, for example, 3 years 5 months 2 days and 7 hours. This
differs from a duration in that it is inexact in terms of
milliseconds. A period can only be resolved to an exact number of
milliseconds by specifying the instant (including chronology and time
zone) it is relative to. e.g. consider the period of 1 year, if we add
this to January 1st we will always arrive at the next January 1st but
the duration will depend on whether the intervening year is a leap
year or not.
LocalTime is an immutable time class representing a time without a time zone. So, base on above definition, period is suitable for you adding time to LocalTime. In fact, API has proved this:
LocalTime localTime = new LocalTime(10, 30);
Duration d = new Duration(1, 0);
Period p = new Period(1, 0);
LocalTime newLocalTime = localTime.plus(d); // COMPILE ERROR
LocalTime newLocalTime = localTime.plus(p); // SUCCESS
you can use the method sum()
LocalTime time1 = LocalTime.of(12, 30);
LocalTime time2 = LocalTime.of(8, 30);
Integer hour1 = time1.getHour();
Integer hour2 = time2.getHour();
Integer minute1 = time1.getMinute();
Integer minute2 = time2.getMinute();
Integer first = Integer.sum(hour1,hour2);
Integer second = Integer.sum(minute1,minute2);
System.out.println("The time is "+ first + " : " + second);
It must work
LocalTime t1 = LocalTime.parse('03:33:24')
LocalTime t2 = LocalTime.parse('03:13:41')
t1.plusHours(t2.hour)
.plusMinutes(t2.minute)
.plusSeconds(t2.second)
.plusNanos(t2.nano)

Categories

Resources