How to calculate the total amount of hours and minutes (in a specific range) between a timestamp range

Luiz :

The user can filter a report by an initial and end date/time (timestamp). Supposing the current filters:

Initial: 2018-01-01 13:00:00

End: 2018-01-05 04:00:00

How to calculate in Java the total amount of hours and minutes that happened between 22:00 and 05:00 AM (of the next day) for all days inside the filtered period.

We are currently using Java 8 classes (LocalDateTime etc).

Expected result for the filters above: 27 hours and 0 minutes (and NOT 87 hours)!

Details:

From day 01 to day 02 we overlap the interested hour range (22h - 5h) so
for day 01 to day 02 we add 7 hours to the total amount.
From day 02 to day 03 we add another 7 hours.
From day 03 to day 04 we add another 7 hours.
From day 04 to day 05 we add 6 hours because the end filter finishes at 04:00 AM so we should not consider the last hour.

If the end timestamp was 2018-01-05 04:30:00 then the final result would be 27 hours and 30 minutes.

Also, the solution must take into account DST changes. We have the client timezone available to use in the operation so the solution might be to use the OffsetDateTime class. But I don't know how to properly handle DST in this scenario.

LuCio :

I've already given an answer. But the already given answers are rather complex. I think they aren't easy to understand. So I wasn't satisfied by these answers and was wondering if there can be a simple solution, easy to understand.

I think, I found one. The approach is to define a date-time range (as mentioned by OP) and to stream over it's units and filter the appropriate ones.

Here is my DateTimeRange:

public class DateTimeRange {

    private final ZonedDateTime from;
    private final ZonedDateTime to;

    DateTimeRange(ZonedDateTime from, ZonedDateTime to) {
        this.from = from;
        this.to = to;
    }

    public static DateTimeRange of(LocalDateTime from, LocalDateTime to, ZoneId zoneId) {
        Objects.requireNonNull(from);
        Objects.requireNonNull(to);
        Objects.requireNonNull(zoneId);

        return new DateTimeRange(ZonedDateTime.of(from, zoneId), ZonedDateTime.of(to, zoneId));
    }
    public Stream<ZonedDateTime> streamOn(ChronoUnit unit) {
        Objects.requireNonNull(unit);

        ZonedDateTimeSpliterator zonedDateTimeSpliterator = new ZonedDateTimeSpliterator(from, to, unit);
        return StreamSupport.stream(zonedDateTimeSpliterator, false);
    }

    static class ZonedDateTimeSpliterator implements Spliterator<ZonedDateTime> {

        private final ChronoUnit unit;

        private ZonedDateTime current;
        private ZonedDateTime to;

        ZonedDateTimeSpliterator(ZonedDateTime from, ZonedDateTime to, ChronoUnit unit) {
            this.current = from.truncatedTo(unit);
            this.to = to.truncatedTo(unit);
            this.unit = unit;
        }

        @Override
        public boolean tryAdvance(Consumer<? super ZonedDateTime> action) {
            boolean canAdvance = current.isBefore(to);

            if (canAdvance) {
                action.accept(current);
                current = current.plus(1, unit);
            }

            return canAdvance;
        }

        @Override
        public Spliterator<ZonedDateTime> trySplit() {
            long halfSize = estimateSize() / 2;
            if (halfSize == 0) {
                return null;
            }

            ZonedDateTime splittedFrom = current.plus(halfSize, unit);
            ZonedDateTime splittedTo = to;
            to = splittedFrom;

            return new ZonedDateTimeSpliterator(splittedFrom, splittedTo, unit);
        }

        @Override
        public long estimateSize() {
            return unit.between(current, to);
        }

        @Override
        public Comparator<? super ZonedDateTime> getComparator() {
            // sorted in natural order
            return null;
        }

        @Override
        public int characteristics() {
            return Spliterator.NONNULL | Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED | Spliterator.SORTED | Spliterator.DISTINCT;
        }

    }

}

And this is the adapted Durations class:

public class Durations {

  public static Duration getSumOfHoursOnDays(ZoneId zoneId, LocalDateTime dateTimeFrom, LocalDateTime dateTimeTo, LocalTime dailyTimeFrom,
    LocalTime dailyTimeTo) {
    return getDuration(zoneId, dateTimeFrom, dateTimeTo, dailyTimeFrom, dailyTimeTo, ChronoUnit.HOURS);
  }

  public static Duration getDuration(ZoneId zoneId, LocalDateTime dateTimeFrom, LocalDateTime dateTimeTo, LocalTime dailyTimeFrom,
    LocalTime dailyTimeTo, ChronoUnit precision) {
    long count = DateTimeRange.of(dateTimeFrom, dateTimeTo, zoneId)
      .streamOn(precision)
      .filter(getFilter(dailyTimeFrom, dailyTimeTo))
      .count();
    return Duration.of(count, precision);
  }

  protected static Predicate<? super ZonedDateTime> getFilter(LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    return dailyTimeFrom.isBefore(dailyTimeTo) ?
      filterFromTo(dailyTimeFrom, dailyTimeTo) :
      filterToFrom(dailyTimeFrom, dailyTimeTo);
  }

  protected static Predicate<? super ZonedDateTime> filterFromTo(LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    return zdt -> {
      LocalTime time = zdt.toLocalTime();
      return (time.equals(dailyTimeFrom) || time.isAfter(dailyTimeFrom)) && time.isBefore(dailyTimeTo);
    };
  }

  protected static Predicate<? super ZonedDateTime> filterToFrom(LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    return zdt -> {
      LocalTime time = zdt.toLocalTime();
      return (time.equals(dailyTimeFrom) || time.isAfter(dailyTimeFrom)) || (time.isBefore(dailyTimeTo));
    };
  }

}

As the Stream-Interface is well known this approach should be easier to understand. Moreover it's simple to use it with other ChronoUnits. Applying the Stream-Interface makes it easy to compute other date-time based values. Moreover this follows the example of Java 9 LocalDate.datesUntil.

While being more easier to understand this solution will not be as fast like the both mentioned earlier. I think as long as nobody streams at nano precision over years it should be acceptable ;)


My resource links:

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=88125&siteId=1