tl;博士
- 不要太努力。使用 ThreeTen-Extra 项目中的
Interval 类来表示和比较与时间线相关的时间跨度。
- 在
ZonedDateTime 中的分区时间和Instant 中的UTC 时间之间切换。同一时刻,不同的挂钟时间。
ThreeTen-Extra 项目
似乎其他答案正在尝试重新创建已经编写的类。请参阅 ThreeTen-Extra 项目,了解向 Java 8 及更高版本中内置的 java.time 框架添加功能的类。
具体来说,我们可以使用Interval——一对Instant对象,一个UTC的日期时间范围。该类提供了overlaps、intersection等比较方法。
从您的两个输入开始,日期和时间。将这些解析为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/Montreal、Africa/Casablanca 或Pacific/Auckland。切勿使用 3-4 个字母的缩写,例如 EST 或 IST,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。
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 视为由Instant 和ZoneId 组成。 Instant 和 ZonedDateTime 都代表同一时刻,时间线上的同一点。只有挂钟时间不同。
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 format。 P 标志着开始,而 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.Date、Calendar 和 SimpleDateFormat。
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 的试验场。您可以在这里找到一些有用的类,例如Interval、YearWeek、YearQuarter 和more。