How to sum two times in Java, using LocalTime? - java

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)

Related

efficiently convert DateTime in long format (yyyyMMddHHmmss) into another zone for comparison

I have a long value that represents a datetime like this 20200319234500 (translates into March 19th, 2020 11:45:00PM)
I want this long value (20200319234500) converted into another timezone again in long format so I can do a greater than and less than comparison with the current date time in local timezone.
I want to do it efficiently so I dont have to create any objects during run time or do string creations after the start up.
but it looks like I must first convert long time to a string and then call ZonedDateTime.parse() function to get a datetime and then do a comparison. Is there another way of doing this?
//This is known at compile time
ZoneId baseZone = ZoneId.of("Europe/Paris");
//This is known at compile time
ZoneId localZone = ZoneId.of("America/New_York");
//This is known at compile time
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuuMMddHHmmss").withZone(baseZone);
long baseDateTime = 20210321234500L;
//Dont want o be doing string operations. Is there a way to keep it in long and get another long in a different zone?
ZonedDateTime convertedBaseDateTime = ZonedDateTime.parse(Long.toString(baseDateTime), formatter);
//Can I just get a long that represents current time in local zone? local zone is not JVM zone
ZonedDateTime localDateTime = ZonedDateTime.now(localZone);
//Thats the only operation I want to perform
boolean result = convertedBaseDateTime.isBefore( localDateTime);
You can do some maths to get the year, month, day, hour. minute, second from the long, and then you can pass it to ZonedDateTime.of
long baseDateTime = 20210321234500L;
int year = (int)(baseDateTime / 10000000000L);
int month = (int)(baseDateTime / 100000000L % 100);
int day = (int)(baseDateTime / 1000000L % 100);
int hour = (int)(baseDateTime / 10000L % 100);
int minute = (int)(baseDateTime / 100L % 100);
int second = (int)(baseDateTime % 100);
ZonedDateTime convertedBaseDateTime = ZonedDateTime.of(year, month, day, hour, minute, second, 0, baseZone);
This won't create new strings.
After that, notice that if you just want to check if a date time is before "now", you don't need a zone for "now". You just need to compare the numbers of (milli/nano)seconds since the epoch of then, and now.
// don't need these
// ZoneId localZone = ZoneId.of("America/New_York");
// ZonedDateTime localDateTime = ZonedDateTime.now(localZone);
// you just need to compare
convertedBaseDateTime.toEpochSecond() * 1000 < System.currentTimeMillis()
That said, if performance is so important for you, maybe you shouldn't use Java, and should instead use a more low-level language.
long baseDateTime = 20210321234500L;
LocalDateTime time=LocalDateTime.ofEpochSecond(baseDateTime /1000,0,ZoneOffset.ofHours(8));
//then you can use time2.isAfter() or some other methond to comparable that is link to jdk 8 API .

Sum of time with result more than 24 hours

Suppose the following snippet:
LocalTime test = LocalTime.of(21, 14);
test.plusHours(5);
The result would be, normally, 02:14 but I want to sum beyond the 24 hour limit, so the result would be 26:14.
In this case I have a field that an user can input how much time it spent on a task. However I have to work with hours (eg 48 hours) instead of days (eg 2 days and 4 hours).
Is there a way that I can achieve that within the java.time API? If not, what can I do to achieve that? I am using Java 8, with Spring Boot and Hibernate to map the database.
java.time.Duration
You’re using the wront data type for the value. You need a Duration. Duration is the time-level counterpart of Period:
A time-based amount of time, such as '34.5 seconds'.
Duration durationTaken = Duration.of(5, ChronoUnit.HOURS);
If you want to relate that to a date concept, such as to compute the end time, you can plus durations to date/time types:
LocalTime endTime = test.plus(durationTaken); //02:14
And you can do that with LocalDateTime too:
LocalDateTime startTime = LocalDateTime.of(LocalDate.now(), test); //2019-02-07T21:14
//add the duration:
LocalDateTime endDate = startTime.plus(durationTaken); //2019-02-08T02:14
To specify how long a task takes, use Duration:
Duration initialDuration = Duration.ofHours(21).plusMinutes(34);
Duration afterFurtherWork = initialDuration.plusHours(5);
System.out.printf("Total duration was %2d hours and %02d minutes.%n",
afterFurtherWork.toHours(), afterFurtherWork.toMinutesPart());
Update: as Ole V.V. points out, toMinutesPart was added in Java 9. If still using Java 8, use toMinutes()%60.
LocalTime won't support this. It has a static initializer with some very baked in rules around 24 hours.
/**
* Constants for the local time of each hour.
*/
private static final LocalTime[] HOURS = new LocalTime[24];
static {
for (int i = 0; i < HOURS.length; i++) {
HOURS[i] = new LocalTime(i, 0, 0, 0);
}
MIDNIGHT = HOURS[0];
NOON = HOURS[12];
MIN = HOURS[0];
MAX = new LocalTime(23, 59, 59, 999_999_999);
}
Based on your updated comments, I might suggest converting hours to your lowest time value, i.e. with TimeUnit.HOURS.toMinutes(value).

Iterating through ZonedDateTime interval in Duration steps

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.

Perform A - B (A\B) with Time in java?

If I have this code:
DateTime start = new DateTime().withTime(4, 0, 0, 0);
DateTime end = start.withTime(5, 0, 0, 0);
DateTime s2 = start.withTime(4,30,0,0);
DateTime e2 = start.withTime(5,30,0,0);
Duration d1 = new Duration(start,end);
Duration d2 = new Duration(s2,e2);
Duration result = d1.minus(d2);
System.out.println((int)result.getStandardMinutes());
Is there a way that I can get essentially A - B or A \ B (set theory notation)?
In this scenario the result would be 30 because the first duration has 30 minutes of time which do not occur in the second duration.
I'm not looking for a solution specifically in Jodatime, just using that to explain the problem.
A Duration represents an amount of time (such as "10 minutes and 30 seconds"), but it's not attached to a timeline: 10 minutes and 30 seconds relative to what? To nothing in particular, it's just the amount of time (the values), by itself.
Specifically in Joda-Time, after creating the Duration, the object itself doesn't store the reference dates used to calculate it, so the Duration instance can't know if it's before or after a specific date (because it's an amount of time not attached to any particular date, so you can't compare it with a date).
If you want to consider a specific date (that's after or before another), and use this date to calculate the duration, you must check the date before calculating the duration:
// ignore dates before the start
DateTime date1 = s2.isBefore(start) ? start : s2;
// ignore dates after the end
DateTime date2 = e2.isAfter(end) ? end : e2;
Duration d2 = new Duration(date1, date2);
Or, you can do what you're already doing, but in the end, you check if s2 or e2 are outside the start/end interval, and add the respective durations back to the result:
if (s2.isBefore(start)) {
result = result.plus(new Duration(s2, start));
}
if (e2.isAfter(end)) {
result = result.plus(new Duration(end, e2));
}
Not sure if set theory really applies here, but I might be wrong (I'm not a pro in maths).

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.

Categories

Resources