将Java日期拆分为小时

3

我在程序中接收以下startdate和enddate作为输入。

2017-03-07 06:30:00到2017-03-07 11:35:00 (yyyy-mm-dd hh:mi:ss)

我想按小时拆分日期。 对于上面给出的示例,我想创建类似于下面列出的值列表。

2017-03-07 0630-07 
2017-03-07 07-08  
2017-03-07 08-09 
2017-03-07 09-10  
2017-03-07 10-11  
2017-03-07 11-1135

您能否建议我可以探索的方法?


好的,我明白了!我会尝试看看我能找到什么。 - Aaron
谢谢Aaron,是的,那是正确的。我想按时间段分解它们。我还没有编写代码。在实施之前,我想检查推荐的方法。 - Punter Vicky
你完全可以用Java来完成这个任务,但通常情况下,当我需要做这种一次性的工作时,我会使用像Python这样的工具。 - David Ehrmann
@DavidEhrmann,谢谢。我正在尝试作为报告应用程序的一部分获取特定表中数据的计数。查询很复杂,即使调整了多个条件,仍需要超过2分钟的时间。因此,我正在尝试按小时缓存数据。如果任何请求通过缓存来获取数据,则我将通过累加缓存中每小时的计数来计算计数。我只会查询未缓存的数据。 - Punter Vicky
1
@PunterVicky 在标题中提到的 Map 有何作用?我建议您要么更明确地解释一下,要么编辑标题以删除该提及(如果与您的核心问题无关)。 - Basil Bourque
显示剩余2条评论
3个回答

4

如果您可以使用Java8,这是我建议的方法:

// with LocalDateTime startDate and LocalDateTime endDate defined,
LocalDateTime currentRangeStart = startDate;
while (currentRangeStart.isBefore(endDate)) {
    LocalDateTime nextHour = currentRangeStart.withMinute(0).plusHours(1);
    LocalDateTime currentRangeEnd = nextHour.isBefore(endDate) ? nextHour : endDate;
    System.out.printf("%s - %s%n", currentRangeStart, currentRangeEnd);
    currentRangeStart = currentRangeEnd;
}

在这里,您可以看到它的运行情况。如果您目前拥有Date对象,请参考此问题了解如何从Date转换为LocalDateTime


太棒了,亚伦!非常感谢你的帮助。这看起来如此简单而优雅。 - Punter Vicky
1
是的,Java 8 中引入的 java.time 包真正简化了日期处理 :) - Aaron
谢谢!非常感谢您的时间和帮助,正是我所需要的。 - Punter Vicky

2
我会从开始日期到计数构建一张地图,类似于...
Map<java.util.Date,Integer> map = new TreeMap<>();
Calendar cal = new GregorianCalendar();
for (java.sql.Timestamp exactDate: dates) {
  cal.setTime(exactDate);
  cal.set(Calendar.MINUTE, 0);
  cal.set(Calendar.SECOND, 0);
  cal.set(Calendar.MILLISECOND, 0);
  java.util.Date key = new java.util.Date(cal.getTime());
  Integer count;
  if ((count = map.get(key)) == null) {
    map.put(key, Integer.valueOf(1);
  } else {
    map.put(key, Integer.valueOf(1 + count.intValue()));
  }
}

然后遍历映射以提取值; 或者使用 for 循环遍历从第一个到最后一个可能的值,以便打印“零”小时。

然而,更加可靠和高效的方法是在数据库查询中完成排序和分组工作,例如:

SELECT trunc(log_date,'HH24'),count(*) 
FROM tbl
GROUP BY trunc(log_date,'HH24')
ORDER BY trunc(log_date,'HH24')

谢谢你,@David!是的,我也像你建议的一样从数据库获取数据。 - Punter Vicky

1

Aaron的回答很好,但它忽略了输入中可能存在的秒数或分数秒。此外,我可以更详细地展开,并应用时区。

截断

您可以截断LocalDateTime对象,以在增加小时数时消除任何整个或分数秒。

LocalDateTime ldtTruncated = ldt.truncatedTo( ChronoUnit.MINUTES ); // Lop off seconds and nanoseconds.

…或者…

LocalDateTime ldtTruncated = ldt.truncatedTo( ChronoUnit.HOURS ); // Lop off minutes, seconds, and nanoseconds.

时区

一个关键问题是时区。这些值缺乏任何与UTC偏移量时区的指示器。因此,正如Aaron所正确展示的那样,我们应该最初将它们解析为LocalDateTime

LocalDateTime ldtStart = LocalDateTime.parse ( "2017-03-07 06:30:00".replace ( " " , "T" ) );
LocalDateTime ldtStop = LocalDateTime.parse ( "2017-03-07 11:35:00".replace ( " " , "T" ) );

但从问题的措辞中可以看出,我们希望处理特定的时刻。根据定义,LocalDateTime 并不是一个具体的时刻,而只是潜在时刻的模糊想法。您必须添加时区(或偏移量)的上下文来确定实际时刻。因此,如果您知道这些值所代表的时区,请应用它。 ZonedDateTime 类会针对由于异常情况(如 夏令时 (DST))而无效的输入值进行调整。
ZoneId z = ZoneId.of ( "America/Montreal" );
ZonedDateTime zdtStart = ldtStart.atZone ( z );
ZonedDateTime zdtStop = ldtStop.atZone ( z );

确保输入内容合理。
// Validate the inputs.
if ( zdtStop.isBefore ( zdtStart ) ) {
    // perhaps throw exception
    System.out.println ( "ERROR - stop is befor start." );
    return;
}

日期时间范围作为一对ZonedDateTime对象

现在我们可以逐小时增加以生成所需的范围。我们可以使用来自ThreeTen-Extra项目的Interval类(见下文),该类跟踪始终处于UTC的一对Instant对象。但是,这对我们可能没有用处,因为我们真正想要处理时区小时(或者我这样认为)。因此,让我们将每个日期时间范围作为一对ZonedDateTime类进行跟踪。我们可以为这对创建自己的类,但是相反,我们重新利用AbstractMap.SimpleImmutableEntry类。该类将一对对象作为键和值进行跟踪。

不要让那个配对类的名称让你困惑。该类最初的意图是与一个Map一起使用,但它本身不是一个地图。该类的目的是引用来自Map的键和值,但我们将以其“键”作为起始时刻,“值”作为停止时刻的方式使用它。我们将每个计算出的时间范围收集到一个List类型的AbstractMap.SimpleImmutableEntry中。
int initialCapacity = ( ( int ) ChronoUnit.HOURS.between ( zdtStart , zdtStop ) ) + 2; // Plus two for good measure. (not sure if needed).
List<AbstractMap.SimpleImmutableEntry<ZonedDateTime , ZonedDateTime>> ranges = new ArrayList<> ( initialCapacity );

设置循环的起始值并进行循环。在每次循环中,我们希望将值截断为整点(以去除秒和小数秒)。在截断后,我们再加上一个小时以到达下一个小时。我们只需要在第一次循环时进行截断,因此您可以重新编写此代码以更高效地避免在连续循环中不必要的截断。除非您正在处理非常大量的小时,否则我不会费心。

ZonedDateTime zdt = zdtStart;
while ( zdt.isBefore ( zdtStop ) ) {
    ZonedDateTime zdt2 = zdt.truncatedTo ( ChronoUnit.HOURS ).plusHours ( 1 );  // Truncate to whole hour, and add one.
    if ( zdt2.isAfter ( zdtStop ) ) { // Oops, went too far. Clip this range to end at `zdtStop`.
        zdt2 = zdtStop;
    }
    AbstractMap.SimpleImmutableEntry<ZonedDateTime , ZonedDateTime> range = new AbstractMap.SimpleImmutableEntry<> ( zdt , zdt2 );
    ranges.add ( range );
    // Prepare for next loop.
    zdt = zdt2;
}

将内容输出到控制台。

System.out.println ( "ldtStart/ldtStop: " + ldtStart + "/" + ldtStop );
System.out.println ( "zdtStart/zdtStop: " + zdtStart + "/" + zdtStart );
System.out.println ( "ranges: " + ranges );

当运行时。 ldtStart/ldtStop: 2017年3月7日06:30至2017年3月7日11:35 zdtStart/zdtStop: 2017年3月7日06:30-05:00[美国蒙特利尔]/2017年3月7日06:30-05:00[美国蒙特利尔] ranges: [2017年3月7日06:30-05:00[美国蒙特利尔]=2017年3月7日07:00-05:00[美国蒙特利尔],2017年3月7日07:00-05:00[美国蒙特利尔]=2017年3月7日08:00-05:00[美国蒙特利尔],2017年3月7日08:00-05:00[美国蒙特利尔]=2017年3月7日09:00-05:00[美国蒙特利尔],2017年3月7日09:00-05:00[美国蒙特利尔]=2017年3月7日10:00-05:00[美国蒙特利尔],2017年3月7日10:00-05:00[美国蒙特利尔]=2017年3月7日11:00-05:00[美国蒙特利尔],2017年3月7日11:00-05:00[美国蒙特利尔]=2017年3月7日11:35-05:00[美国蒙特利尔]]

关于 java.time

java.time 框架是内置于 Java 8 及更高版本中的。这些类取代了令人头痛的旧的传统日期时间类,例如java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在处于维护模式,建议迁移到java.time 类。

要了解更多,请参见Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310
从哪里获取java.time类?

ThreeTen-Extra 项目扩展了 java.time 的额外类。该项目是 java.time 可能未来添加的试验场。您可能会在这里找到一些有用的类,例如 Interval, YearWeek, YearQuarter更多


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接