【问题标题】:How to calculate the total amount of hours and minutes (in a specific range) between a timestamp range如何计算时间戳范围之间的小时和分钟总数(在特定范围内)
【发布时间】:2018-12-28 02:05:58
【问题描述】:

用户可以按初始和结束日期/时间(时间戳)过滤报告。假设当前的过滤器:

首字母:2018-01-01 13:00:00

结束:2018-01-05 04:00:00

如何在 Java 中计算过滤期间内所有天22:0005:00 AM(次日)之间发生的总小时数和分钟数。

我们目前正在使用 Java 8 类(LocalDateTime 等)。

上述过滤器的预期结果:27 小时 0 分钟(而不是 87 小时)

详情:

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.

如果结束时间戳是2018-01-05 04:30:00,那么最终结果将是 27 小时 30 分钟。

此外,解决方案必须考虑 DST 更改。我们有可用于操作的客户端时区,因此解决方案可能是使用OffsetDateTime 类。但我不知道在这种情况下如何正确处理 DST。

【问题讨论】:

  • @LuCio 我的问题不同。我不想知道总小时数。但仅限于特定小时范围之间的总量(也尊重时间戳范围)。请在评论之前仔细阅读描述并注意提供的示例。
  • 您自己尝试过什么吗?如果您投入一点研究工作,这看起来并不难。
  • 您是否总是在整小时内拥有InitialEndLow RangeHigh Range 值?
  • OffsetDateTime 不知道 DST,它只知道从 GMT 开始的小时数和分钟数。您需要 ZonedDateTime 来了解 DST 更改何时发生。虽然有更有效的算法,但我想说,对于这样的问题,你最好从初始时间戳开始,将其提前到开始时间,提前到结束时间,检查你是否已经达到结束时间戳,如果没有再次提前到开始时间,并处理此算法中的所有情况,直到您达到/超过结束时间戳。
  • 嗯…… 3 个人将该问题加注为收藏夹,但在我添加第二个之前只有 1 人赞成?

标签: java date datetime java-8


【解决方案1】:

我试过这个:

LocalDateTime startDate = LocalDateTime.now().minusDays(1);
LocalDateTime endDate = LocalDateTime.now();

long numberOfHours = Duration.between(startDate, endDate).toHours();

并按预期工作

输出:24 小时

或者你可以joda,上面的代码会是这样的:

DateTime startDate = new DateTime().minusDays(1);
DateTime endDate = new DateTime();
Hours hours = Hours.hoursBetween(startDate, endDate);
int numberOfHours = hours.getHours();

【讨论】:

【解决方案2】:

在错误地认为这个问题是重复的@Luiz 之后纠正了我。我的道歉来了。

为了计算与夏令时相关的Duration,我正在关注这个answer。该代码还涵盖闰年。

编辑:
@XiCoN JFS 指出了一些错误。我审查并测试了我的代码。我发布了 测试类 here。我试图涵盖我能想到的每一个案例。

这是我的解决方案:

public class Durations {

  public static Duration getSumOfHoursOnDays(ZonedDateTime dateTimeFrom, ZonedDateTime dateTimeTo, LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    Duration result = Duration.of(0, ChronoUnit.HOURS);

    Duration hoursOnFirstDay = getHoursOnFirstDay(dateTimeFrom, dateTimeTo, dailyTimeFrom, dailyTimeTo);
    result = result.plus(hoursOnFirstDay);

    long daysBetween = ChronoUnit.DAYS.between(dateTimeFrom.truncatedTo(ChronoUnit.DAYS), dateTimeTo.truncatedTo(ChronoUnit.DAYS));
    if (daysBetween > 0) {
      for (int i = 1; i < daysBetween; i++) {
        ZonedDateTime day = dateTimeFrom.plusDays(i);
        Duration hoursOnDay = getHoursOnDay(day, dailyTimeFrom, dailyTimeTo);
        result = result.plus(hoursOnDay);
      }

      Duration hoursOnLastDay = getHoursOnLastDay(dateTimeFrom, dateTimeTo, dailyTimeFrom, dailyTimeTo);
      result = result.plus(hoursOnLastDay);
    }

    return result;
  }

  protected static Duration getHoursOnFirstDay(ZonedDateTime dateTimeFrom, ZonedDateTime dateTimeTo, LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    ZonedDateTime dateTimeToOnFirstDay = dateTimeTo.truncatedTo(ChronoUnit.DAYS).isAfter(dateTimeFrom.truncatedTo(ChronoUnit.DAYS)) ?
      dateTimeFrom.plusDays(1).withHour(0) :
      dateTimeTo;

    return getHoursOnDay(dateTimeFrom, dateTimeToOnFirstDay, dailyTimeFrom, dailyTimeTo);
  }

  protected static Duration getHoursOnLastDay(ZonedDateTime dateTimeFrom, ZonedDateTime dateTimeTo, LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    return dateTimeTo.truncatedTo(ChronoUnit.DAYS).isAfter(dateTimeFrom.truncatedTo(ChronoUnit.DAYS)) ?
      getHoursOnDay(dateTimeTo.withHour(0), dateTimeTo, dailyTimeFrom, dailyTimeTo) :
      Duration.ofHours(0);
  }

  protected static Duration getHoursOnDay(ZonedDateTime day, LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    ZonedDateTime zoneTimeFrom = day.with(dailyTimeFrom);
    ZonedDateTime zoneTimeTo = day.with(dailyTimeTo);
    return zoneTimeFrom.isBefore(zoneTimeTo) ?
      Duration.between(zoneTimeFrom, zoneTimeTo) :
      Duration.between(day.withHour(0), zoneTimeTo).plus(Duration.between(zoneTimeFrom, day.plusDays(1).withHour(0)));
  }

  protected static Duration getHoursOnDay(ZonedDateTime dateTimeFrom, ZonedDateTime dateTimeTo, LocalTime dailyTimeFrom, LocalTime dailyTimeTo) {
    ZonedDateTime dailyDateTimeFrom = dateTimeFrom.with(dailyTimeFrom);
    ZonedDateTime dailyDateTimeTo = dateTimeFrom.with(dailyTimeTo);

    if (dailyDateTimeFrom.isBefore(dailyDateTimeTo)) {
      if (dailyDateTimeFrom.isAfter(dateTimeTo) || dailyDateTimeTo.isBefore(dateTimeFrom)) {
        return Duration.ofHours(0);
      }

      ZonedDateTime from = dateTimeFrom.isAfter(dailyDateTimeFrom) ?
        dateTimeFrom :
        dailyDateTimeFrom;

      ZonedDateTime to = dateTimeTo.isBefore(dailyDateTimeTo) ?
        dateTimeTo :
        dailyDateTimeTo;

      return Duration.between(from, to);
    }

    Duration result = Duration.ofHours(0);

    ZonedDateTime to = dateTimeTo.isBefore(dailyDateTimeTo) ?
      dateTimeTo :
      dailyDateTimeTo;
    if (dateTimeFrom.isBefore(dailyDateTimeTo)) {
      result = result.plus(Duration.between(dateTimeFrom, to));
    }
    ZonedDateTime from = dateTimeFrom.isAfter(dailyDateTimeFrom) ?
      dateTimeFrom :
      dailyDateTimeFrom;
    if (from.isBefore(dateTimeTo)) {
      result = result.plus(Duration.between(from, dateTimeTo));
    }
    return result;
  }
}

主要的挑战是处理 time fromtime to 之后的白天时间。问题中给出了一个示例:从 22 到 5。
在这种情况下,一天内可能有两个时间范围:22 - 24 / 0 - 5。这两个时间范围必须单独评估,因为其中可能存在 DST 变化。
但也可以有一个时间范围,例如:从 22 到 0。关于一天,这会导致时间范围:22 - 24。

我在链接的测试课程中介绍了这些情况和许多其他情况。我花了一些时间让它适用于所有测试用例。所以我希望它能为某人服务。

【讨论】:

  • 不错的 OOP 方法。但是我注意到了一个错误:在 2017-3-25 22:00 到 2017-10-29 05:00 的范围内,在 2017-3-26 +1 和 2017-10-29 -1 有一个 DST 变化,这将结果为 1526 小时。您的代码等于 1527 小时。
  • 很遗憾地通知您,您的错误仍未解决。如果您添加一天(2017-10-30 05:00),则结果应为 1526 + 7 = 1533。您的代码等于 1532。看看我的答案,我用 ZoneRules 解决了 DST 问题。
【解决方案3】:

要了解此解决方案,您应该熟悉位域和位掩码。如果没有,请考虑研究。

这是一个简单的例子:

Bit field: 1 0 1 1 1 0  AND
Bit mask : 1 1 0 1 0 1
______________________
Result   : 1 0 0 1 0 0

使用这种方法,您可以将一天中的实际小时数显示为位字段,并将所需小时数显示为位掩码。

我们从下午 22 点到凌晨 5 点的 24 位掩码(24 小时)将如下所示(MSB 最后):

23 22 21 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
 1  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  1  1  1  1

十六进制表示0xC0001F

编辑:

在计算某些范围内的日历天数与小时数时,我的最后一种方法出现了错误。事实证明,我必须将问题归结为矩阵来识别模式。

CD\HR <24 =24 >24 <48 =48 >48 <72 =72 >72 <96 =96 >96 
   0   x
   1   x   x   x   x
   2           x   x   x   x   x
   3                       x   x   x   x   x
   4                                   x   x   x   x

 CD = Calendar Days => 2017-01-01 23:00 until 2017-01-02 00:00 is one CD
 HR  = Hour Range

目标是为startDateendDate 创建一个bitField。每隔一天,我们只需添加 24 bitMask 中的位数。 bitField 始终是从 startTimeendTime 的范围。如果startTime > endTime 则我们翻转它们,计算出的bitField 也是如此。

三个条件决定在应用位掩码时是否需要忽略日历日:

calendarDays &gt; 0

bitField != 0

startTime &gt; endTime

这将在一个 24 位掩码的范围内计算小时数,并考虑可能的 DST:

private double getHoursInRange(ZonedDateTime startDate, ZonedDateTime endDate, int bitMask) {
    if (!startDate.isBefore(endDate)) throw new InputMismatchException("endDate is before or equal startDate");
    ZoneRules rules = startDate.getZone().getRules();
    int offset = 0;
    ZoneOffsetTransition zoneOffsetTransition = rules.nextTransition(startDate.toInstant());
    while (true) {
        Instant transitionInstant = zoneOffsetTransition.getInstant();
        if (transitionInstant.isBefore(endDate.toInstant()) || transitionInstant.equals(endDate.toInstant())) {
            ZoneOffset offsetAfter = zoneOffsetTransition.getOffsetAfter();
            offset += offsetAfter.getTotalSeconds() == 3600 ? -1 : 1;
            zoneOffsetTransition = rules.nextTransition(transitionInstant);
        } else {
            break;
        }
    }
    long calendarDays = Duration.between(startDate.withHour(0), endDate.withHour(23)).toDays();
    int startTime = startDate.getHour();
    int endTime = endDate.getHour();
    int bitField = 0;
    for (int o = startTime < endTime ? startTime : endTime; startTime < endTime ? o < endTime : o < startTime; o++) {
        bitField = bitField | (1 << o);
    }
    if (startTime > endTime) {
        bitField = ~bitField;
    }
    if (calendarDays > 0 && bitField != 0 && startTime > endTime) {
        calendarDays = calendarDays - 1;
    }
    double hoursInRange = calendarDays * Integer.bitCount(bitMask);
    hoursInRange += Integer.bitCount(bitField & bitMask);
    hoursInRange += offset;
    return hoursInRange;
}

如果你也想检查分钟,你可以添加以下行:

hoursInRange += (endDate.getMinute() - startDate.getMinute()) / 60.0;

或秒:

hoursInRange += (endDate.getSecond() - startDate.getSecond()) / 3600.0;

通过快速单元测试进行测试:

@Test
public void test() {
    ZoneId london = ZoneId.of("Europe/London");
    LocalDateTime startDate = LocalDateTime.of(2018, 1, 1, 13, 0, 0, 0);
    LocalDateTime endDate = LocalDateTime.of(2018, 1, 5, 4, 0, 0, 0);
    ZonedDateTime from = startDate.atZone(london);
    ZonedDateTime to = endDate.atZone(london);
    double hours = getHoursInRange(from, to, 0xC0001F);
    double allHours = getHoursInRange(from, to, 0xFFFFFF);
    assertEquals(27, hours, 0);
    assertEquals(87, allHours, 0);
}

【讨论】:

  • 这是一个很好的解决方案,但没有考虑到夏令时的变化。
  • @Luiz 看看,我更新了我的原始答案,并确保算法在 DST 和任何给定范围方面都是正确的。
  • 这个解决方案是最有趣的,感谢发布。但是,@LuCio 发布的解决方案是我最容易适应的解决方案,也可以计算半小时或任何以分钟为单位的时间段。假设结束时间戳为2018-01-05 04:30:00,在这种情况下,总小时数将为 27 小时和 30 分钟。我认为我的问题的标题具有误导性,因此我将对其进行更改。还是谢谢。
  • @Luiz,解决这个难题我玩得很开心。分钟也可以计算,我将代码作为附加代码发布,因为您的要求有点误导。
【解决方案4】:

我已经给了answer。但是已经给出的答案相当复杂。我认为它们并不容易理解。所以我对这些答案并不满意,想知道是否有一个简单易懂的解决方案。

我想,我找到了一个。该方法是定义一个日期时间 range(如 OP 所述)并流过它的单位并过滤适当的单位。

这是我的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;
        }

    }

}

这是改编后的Durations 类:

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));
    };
  }

}

由于Stream-Interface 众所周知,这种方法应该更容易理解。此外,它很容易与其他ChronoUnits 一起使用。应用Stream-Interface 可以轻松计算其他基于日期时间的值。此外,这遵循 Java 9 LocalDate.datesUntil 的示例。

虽然更容易理解,但这个解决方案不会像前面提到的那样快。我认为只要多年来没有人以纳米精度流式传输,它应该是可以接受的;)


我的资源链接:

【讨论】:

  • 很好的解决方案。但我不明白如何适应包括部分时间。我在 XiCoN 答案中提供的相同示例:如果结束时间戳为 2018-01-05 04:30:00,则结果应为 27 小时 30 分钟(或 1650 分钟)。您的解决方案仍然声明 1620 分钟。我尝试将 ChronoUnit 更改为 MINUTES,如果我在生成的持续时间(输出 1650)中使用 toHours() 而不是 toMinutes(),这是错误的...
  • @Luiz 我更新了我的答案。使用getDuration() 方法可以工作。请参阅更新的 DurationTest 类。我添加了测试用例testQuestion2()。结果是 Duration 1650 分钟。实际上它是相同的代码。 getDuration() 只是一种通用方法,可以使用ChronoUnit 进行额外参数化。
  • 我在等你回答我的评论。虽然这个解决方案比较慢,但我认为它是最容易理解和简洁的。此外,性能不是此用例的主要问题。我会使用这个解决方案,谢谢@LuCio。
  • 广泛的测试覆盖率是一大优势! ;)
  • @Luiz 多亏了测试,我才能够实施这种新方法。否则,这将是一个概念证明。现在应该可以使用了。
【解决方案5】:

tl;博士

  • 不要太努力。使用 ThreeTen-Extra 项目中的 Interval 类来表示和比较与时间线相关的时间跨度。
  • ZonedDateTime 中的分区时间和Instant 中的UTC 时间之间切换。同一时刻,不同的挂钟时间。

ThreeTen-Extra 项目

似乎其他答案正在尝试重新创建已经编写的类。请参阅 ThreeTen-Extra 项目,了解向 Java 8 及更高版本中内置的 java.time 框架添加功能的类。

具体来说,我们可以使用Interval——一对Instant对象,一个UTC的日期时间范围。该类提供了overlapsintersection等比较方法。

从您的两个输入开始,日期和时间。将这些解析为LocalDateTime,因为您忽略了时区。将中间的 SPACE 替换为 T 以符合 ISO 8601 标准格式。

LocalDateTime ldtStart = LocalDateTime.parse( "2018-01-01T13:00:00" );
LocalDateTime ldtStop = LocalDateTime.parse( "2018-01-05T04:00:00" );

LocalDateTime 确实 代表一个时刻,因为它缺少任何时区或与 UTC 偏移的概念。没有这个语境,它就没有真正的意义。您的意思是在印度加尔各答、法国巴黎或加拿大蒙特利尔从下午 1 点开始?这是三个非常不同的时刻。

因此,分配一个时区 ZoneId 以获取 ZonedDateTime

continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdtStart = ldtStart.atZone( z );
ZonedDateTime zdtStop = ldtStop.atZone( z );

使用org.threeten.extra.Interval 类来表示我们与时间线相关的时间跨度。此类表示一对Instant 对象。根据定义,Instant 采用 UTC。我们正在一个分区的日期时间工作。该怎么办?通过从我们的ZonedDateTime 中提取Instant 来调整为UTC。您可以在概念上将ZonedDateTime 视为由InstantZoneId 组成。 InstantZonedDateTime 都代表同一时刻,时间线上的同一点。只有挂钟时间不同。

Interval interval = Interval.of( zdtStart.toInstant() , zdtStop.toInstant() );

您的参考框架是我们在指定时区感知的时间间隔内的日期。因此,从每个ZonedDateTime 中提取LocalDate,一个仅限日期的值。

LocalDate ldStart = zdtStart.toLocalDate();
LocalDate ldStop = zdtStop.toLocalDate();

我们将按日期循环,一次增加一天。所以将该日期复制到一个递增变量中。

LocalDate localDate = ldStart;

指定我们定位的时间对。

LocalTime timeStart = LocalTime.of( 22 , 0 );
LocalTime timeStop = LocalTime.of( 5 , 0 );

设置Map 来存储我们的结果。我们将每个日期(在我们区域中看到的日期)映射到 Interval,这对 Instant 对象表示从该日期开始的晚上 10 点到凌晨 5 点目标区域中有多少被我们的输入间隔覆盖。

long initialCapacity = ( ChronoUnit.DAYS.between( ldStart , ldtStop ) + 1 );
Map< LocalDate, Interval > dateToIntervalMap = new HashMap<>( ( int ) initialCapacity );

循环每个日期,直到我们的输入间隔结束。

while ( ! localDate.isAfter( ldStop ) ) {

逐个记录每个日期,并应用我们的目标时间值来确定我们所在时区的某个时刻。

我不知道如何正确处理夏令时

ZonedDateTime 类的核心目的是处理异常,例如在不同时间点在不同区域发现的 DST。 DST 并不是唯一的此类异常。世界各地的政治家都表现出重新定义时区的奇怪倾向。

当我们将时区应用于给定日期和时间时,如果该日期在该日期的时间无效(例如夏令时 DST 转换),则ZonedDateTime 类会根据需要自动调整。请务必阅读文档以确保您理解并同意其调整逻辑。

    ZonedDateTime zdtTargetStart = localDate.atTime( timeStart ).atZone( z );
    ZonedDateTime zdtTargetStop = localDate.plusDays( 1 ).atTime( timeStop ).atZone( z );

对我们的目标时间范围进行间隔,我们的目标Interval

    Interval target = Interval.of( zdtTargetStart.toInstant() , zdtTargetStop.toInstant() );

测试目标区间是否与我们的输入区间重叠。当然,根据我们问题的定义,我们希望一开始就是这种情况。但最后,在最后一天,情况可能并非如此。在最后一个日期,我们的输入间隔可能会在下一个目标时间发生之前结束。事实上,这正是我们在问题中给出的输入所看到的(见下面的输出)。

使用intersection 方法生成一个Interval,它表示我们的目标区间和我们的输入区间之间的共同时间跨度。

    Interval intersection;
    if ( interval.overlaps( target ) ) {
        intersection = interval.intersection( target );
    } else {
        ZonedDateTime emptyInterval = localDate.atTime( timeStart ).atZone( z );   // Better than NULL I suppose.
        intersection = Interval.of( emptyInterval.toInstant() , emptyInterval.toInstant() );
    }

将生成的交叉点间隔存储在我们的地图中,分配给我们循环的日期。

    dateToIntervalMap.put( localDate , intersection );
    // Setup the next loop.
    localDate = localDate.plusDays( 1 );
}

完成业务逻辑。现在我们可以报告结果了。

// Report
System.out.println( "interval: " + interval + " = " + zdtStart + "/" + zdtStop );
int nthDate = 0;

我们使用java.time.Duration 类来跟踪每个交叉点间隔中包含的经过时间。

Duration totalDuration = Duration.ZERO;

我们必须做一些额外的工作来按时间顺序设置我们的报告循环。 Map::keySet 方法不一定按我们想要的顺序返回结果。

List< LocalDate > dates = new ArrayList<>( dateToIntervalMap.keySet() );
Collections.sort( dates );
List< LocalDate > keys = List.copyOf( dates );
for ( LocalDate date : keys ) {
    nthDate++;
    Interval i = dateToIntervalMap.get( date );
    Instant startInstant = i.getStart();
    Instant stopInstant = i.getEnd();
    Duration d = Duration.between( startInstant , stopInstant );
    totalDuration = totalDuration.plus( d );
    ZonedDateTime start = startInstant.atZone( z );
    ZonedDateTime stop = stopInstant.atZone( z );
    System.out.println( "Day # " + nthDate + " = " + date + " ➙ " + i + " = " + start + "/" + stop + " = " + d );
}

报告我们所有交叉点间隔中包含的总时间。我们的Duration 对象生成的@​​987654407@ 使用标准ISO 8601 duration formatP 标志着开始,而 T 将任何年-月-日与任何小时-分钟-秒分开。

System.out.println("Total duration: " + totalDuration);

为方便起见,让我们再看一遍相同的代码,全部放在一个块中。

LocalDateTime ldtStart = LocalDateTime.parse( "2018-01-01T13:00:00" );
LocalDateTime ldtStop = LocalDateTime.parse( "2018-01-05T04:00:00" );

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdtStart = ldtStart.atZone( z );
ZonedDateTime zdtStop = ldtStop.atZone( z );

Interval interval = Interval.of( zdtStart.toInstant() , zdtStop.toInstant() );

LocalDate ldStart = zdtStart.toLocalDate();
LocalDate ldStop = zdtStop.toLocalDate();
LocalDate localDate = ldStart;

LocalTime timeStart = LocalTime.of( 22 , 0 );
LocalTime timeStop = LocalTime.of( 5 , 0 );

long initialCapacity = ( ChronoUnit.DAYS.between( ldStart , ldtStop ) + 1 );
Map< LocalDate, Interval > dateToIntervalMap = new HashMap<>( ( int ) initialCapacity );

while ( ! localDate.isAfter( ldStop ) ) {
    ZonedDateTime zdtTargetStart = localDate.atTime( timeStart ).atZone( z );
    ZonedDateTime zdtTargetStop = localDate.plusDays( 1 ).atTime( timeStop ).atZone( z );
    Interval target = Interval.of( zdtTargetStart.toInstant() , zdtTargetStop.toInstant() );
    Interval intersection;
    if ( interval.overlaps( target ) ) {
        intersection = interval.intersection( target );
    } else {
        ZonedDateTime emptyInterval = localDate.atTime( timeStart ).atZone( z );   // Better than NULL I suppose.
        intersection = Interval.of( emptyInterval.toInstant() , emptyInterval.toInstant() );
    }
    dateToIntervalMap.put( localDate , intersection );
    // Setup the next loop.
    localDate = localDate.plusDays( 1 );
}

// Report
System.out.println( "interval: " + interval + " = " + zdtStart + "/" + zdtStop );
int nthDate = 0;
Duration totalDuration = Duration.ZERO;
List< LocalDate > dates = new ArrayList<>( dateToIntervalMap.keySet() );
Collections.sort( dates );
List< LocalDate > keys = List.copyOf( dates );
for ( LocalDate date : keys ) {
    nthDate++;
    Interval i = dateToIntervalMap.get( date );
    Instant startInstant = i.getStart();
    Instant stopInstant = i.getEnd();
    Duration d = Duration.between( startInstant , stopInstant );
    totalDuration = totalDuration.plus( d );
    ZonedDateTime start = startInstant.atZone( z );
    ZonedDateTime stop = stopInstant.atZone( z );
    System.out.println( "Day # " + nthDate + " = " + date + " ➙ " + i + " = " + start + "/" + stop + " = " + d );
}
System.out.println("Total duration: " + totalDuration);

运行时,我们得到这个输出。我们:

  • 在大多数日子里获得完整的 7 小时
  • 倒数第二个日期下降到六个小时
  • 最后在最后一个日期看到零小时,因为我们的输入间隔在当天的开始时间之前结束。

间隔:2018-01-01T18:00:00Z/2018-01-05T09:00:00Z = 2018-01-01T13:00-05:00[美国/蒙特利尔]/2018-01-05T04:00- 05:00[美国/蒙特利尔]

第 1 天 = 2018-01-01 ➙ 2018-01-02T03:00:00Z/2018-01-02T10:00:00Z = 2018-01-01T22:00-05:00[美国/蒙特利尔]/ 2018-01-02T05:00-05:00[美国/蒙特利尔] = PT7H

第 2 天 = 2018-01-02 ➙ 2018-01-03T03:00:00Z/2018-01-03T10:00:00Z = 2018-01-02T22:00-05:00[美国/蒙特利尔]/ 2018-01-03T05:00-05:00[美国/蒙特利尔] = PT7H

第 3 天 = 2018-01-03 ➙ 2018-01-04T03:00:00Z/2018-01-04T10:00:00Z = 2018-01-03T22:00-05:00[美国/蒙特利尔]/ 2018-01-04T05:00-05:00[美国/蒙特利尔] = PT7H

第 4 天 = 2018-01-04 ➙ 2018-01-05T03:00:00Z/2018-01-05T09:00:00Z = 2018-01-04T22:00-05:00[美国/蒙特利尔]/ 2018-01-05T04:00-05:00[美国/蒙特利尔] = PT6H

第 5 天 = 2018-01-05 ➙ 2018-01-06T03:00:00Z/2018-01-06T03:00:00Z = 2018-01-05T22:00-05:00[美国/蒙特利尔]/ 2018-01-05T22:00-05:00[美国/蒙特利尔] = PT0S

总时长:PT27H


关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。

从哪里获得 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

  • 多么棒的解决方案和解释,谢谢!在我看来,最容易理解。也感谢分享 ThreeTen-Extra,我不知道这个项目。
  • 此解决方案不适用于以下情况: Initial = 2018-07-21 00:00:00 -- End = 2018-07-30 15:10:00 结果应为 68 小时,方法返回 63 小时。不考虑第一天的前 5 个小时。
  • @Luiz 您没有为输入间隔的每一端定义规则。所以我按了分区日期,每个日期都针对您定义的开始时间(晚上 10 点)。如果您想考虑输入间隔之前的片段,请在附加步骤中修改目标开始时间。提示:这种混淆说明了为什么您应该学习编写规范,而不仅仅是一个示例。它不仅有助于避免这些歧义,而且以通俗易懂的方式编写决策规则将引导您找到编写代码的算法。
猜你喜欢
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 2019-05-27
  • 1970-01-01
  • 1970-01-01
  • 2012-08-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多