'How can I get the Duration between two dates / times in YMDHMS (ISO 8601)

Given 2 dates, how can I calculate the difference between them in Year Month Day, Hour Minute Second format, as per ISO 8601 Durations?

I've only found Java libraries that can give the difference in days or smaller.

Given that months and years have irregular numbers of days, I'm not sure how to figure out the difference in months and years.

Even Duration.between() is close, but it gives the result in hours minutes seconds:

ZonedDateTime event1 = ZonedDateTime.of(2022, 2, 2, 2, 2, 2, 0, ZoneId.of("UTC-7"));
ZonedDateTime event2 = ZonedDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC-7"));
//ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC-7"));
Duration duration = Duration.between(event1, event2);
Log.d("Duration ISO-8601: ", duration.toString());


Output:    PT25533H57M58S

Which is 25533 hours, 57 minutes, 58 seconds.

I'd like to see something like:

__ years, __ months, __ days, 9 hours, 57 minutes, 58 seconds


Solution 1:[1]

You can create a custom class that will maintain Period and Duration fields (credits to @Ole V.V. since he mentioned it earlier in the comments).

Here is an example of such a class implementation, which exposes a static method between(), that expects two arguments of type LocalDateTime.

Methods like getYears() and getHours() will delegate the call to Period and Duration objects.

class DateTimeSlot {
    private Period period;
    private Duration duration;
    
    private DateTimeSlot(Period period, Duration duration) {
        this.period = period;
        this.duration = duration;
    }
    
    public int getYears() {
        return period.getYears();
    }
    
    public int getMonth() {
        return period.getMonths();
    }
    
    public int getDays() {
        return period.getDays();
    }
    
    public int getHours() {
        return duration.toHoursPart(); // this method can be safely used instead `toHours()` because `between()` implementation guerantees that duration will be less than 24 hours
    }
    
    public int getMinutes() {
        return duration.toMinutesPart();
    }
    
    public int getSeconds() {
        return (int) (duration.getSeconds() % 60);
    }
    
    public static DateTimeSlot between(LocalDateTime from, LocalDateTime to) {
        if (from.isAfter(to) || from.equals(to)) {
            throw new IllegalArgumentException();
        }
        
        Duration duration;
        Period period;
        
        if (from.toLocalTime().isBefore(to.toLocalTime())) {
            duration = Duration.between(from.toLocalTime(), to.toLocalTime());
            period = Period.between(from.toLocalDate(), to.toLocalDate());
        } else {
            duration = Duration.between(to.withHour(from.getHour())
                .withMinute(from.getMinute())
                .withSecond(from.getSecond())
                .minusDays(1), to);                    // apply shift one day back
            period = Period.between(from.toLocalDate()
                .plusDays(1), to.toLocalDate());       // apply shift one day forward (to compensate the previous shift)
        }
        return new DateTimeSlot(period, duration);
    }
    
    @Override
    public String toString() {
        return String.format("%s years, %s months, %s days, %s hours, %s minutes, %s seconds",
            getYears(), getMonth(), getDays(), getHours(), getMinutes(), getSeconds());
    }
}

main() - demo

public static void main(String[] args) {
    LocalDateTime now = LocalDateTime.of(2022, 5, 20, 18, 0, 18);
    LocalDateTime withTimeBefore = LocalDateTime.of(2020, 12, 31, 15, 9, 27);
    LocalDateTime withTimeAfter = LocalDateTime.of(2020, 12, 31, 22, 50, 48);
    
    DateTimeSlot slot1 = DateTimeSlot.between(withTimeBefore, now);
    DateTimeSlot slot2 = DateTimeSlot.between(withTimeAfter, now);
    
    System.out.println(slot1);
    System.out.println(slot2);
}

Output

1 years, 4 months, 20 days, 2 hours, 50 minutes, 51 seconds  // between 2020-12-31T15:09:27 and 2022-05-20T18:00:18
1 years, 4 months, 19 days, 19 hours, 9 minutes, 30 seconds  // between 2020-12-31T22:50:48 and 2022-05-20T18:00:18

Solution 2:[2]

I believe I have solved it:

ZonedDateTime event1 = ZonedDateTime.of(2022, 2, 2, 2, 2, 2, 0, ZoneId.of("UTC-7"));
ZonedDateTime event2 = ZonedDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC-7"));
Duration duration = Duration.between(event1, event2);
Period period = Period.between(event1.toLocalDate(), event2.toLocalDate());
Log.d("Duration ISO-8601: ", period.toString() + duration.toString());

This prints:

P2Y10M30DPT25533H57M58S

Which is 2 years, 10 months, 30 days, 25533 hours, 57 minutes, 58 seconds.

When extracting the individual values (which is what I need), using modulus fixes the problem with hours:

 String.valueOf(duration.toHours() % 24)

And if there's any problem with minutes and seconds, % 60 will fix it.

Solution 3:[3]

My library Time4J has support for calculating and printing such durations. Example using your input:

    PlainTimestamp event1 = PlainTimestamp.of(2022, 2, 2, 2, 2, 2);
    PlainTimestamp event2 = PlainTimestamp.of(2025, 1, 1, 0, 0, 0);
    TZID tzid = ZonalOffset.ofHours(OffsetSign.BEHIND_UTC, 7);

    Duration<IsoUnit> duration =
        Duration.in(
                Timezone.of(tzid), 
                CalendarUnit.YEARS,
                CalendarUnit.MONTHS, 
                CalendarUnit.DAYS,
                ClockUnit.HOURS, 
                ClockUnit.MINUTES, 
                ClockUnit.SECONDS)
            .between(event1, event2);
    System.out.println(PrettyTime.of(Locale.US).print(duration));
    // 2 years, 10 months, 29 days, 21 hours, 57 minutes, and 58 seconds

A big advantage is here good internationalization of the output including language-dependent plural rules, see also javadoc.

You can also use instants/moments as input and then transform to local timestamps like this way if your input changes (some transformation examples):

    tzid = () -> "America/Chicago"; // probably better than your fixed offset
    event1 = TemporalType.LOCAL_DATE_TIME.translate(LocalDateTime.of(2022, 2, 2, 2, 2, 2));
    event2 = Moment.nowInSystemTime().toZonalTimestamp(tzid);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Gimme the 411
Solution 3 Meno Hochschild