Java中Period和Duration之间的细微差别

19

我不确定自己是否掌握了Java中PeriodDuration之间的微妙差别。

当我阅读Oracle的说明时,它说我可以像这样找出自生日以来的天数(使用他们使用的示例日期):

LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1960, Month.JANUARY, 1);
Period birthdayPeriod = Period.between(birthday, today);
int daysOld = birthdayPeriod.getDays();

但是,即使他们指出这一点,这也没有考虑到你出生时所处的时区和现在所在的时区。但这是一台计算机,我们可以精确计算,对吗?那么我应该使用一个 Duration 吗?

ZoneId bornIn = ZoneId.of("America/New_York");
ZonedDateTime born = ZonedDateTime.of(1960, Month.JANUARY.getValue(), 1, 2, 34, 56, 0, bornIn);
ZonedDateTime now = ZonedDateTime.now();
Duration duration = Duration.between(born, now);
long daysPassed = duration.toDays();

现在实际时间是准确的,但如果我理解正确,天数可能不正确地表示日历天数,例如考虑DST等情况。

那么为了根据我的时区获得精确答案,我应该怎么做?我能想到的唯一办法就是回到使用LocalDate,但首先从ZonedDateTime值归一化时区,然后使用Duration

ZoneId bornIn = ZoneId.of("America/New_York");
ZonedDateTime born = ZonedDateTime.of(1960, Month.JANUARY.getValue(), 1, 2, 34, 56, 0, bornIn);
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime nowNormalized=now.withZoneSameInstant(born.getZone());
Period preciseBirthdayPeriod = Period.between(born.toLocalDate(), nowNormalized.toLocalDate());
int preciseDaysOld = preciseBirthdayPeriod.getDays();

但是这似乎过于复杂,只为了得到一个精确的答案。


4
“精确答案”指的是什么?假设你在两天前的上午12:30出生,但昨天发生了夏令时转换,所以那一天长达25个小时,现在又是凌晨12:30。你已经活了两个日历天,但是49小时。那么,在这种情况下,你想要什么“精确”的答案?如果你能明确地说明你期望得到的答案,计算起来就不会太难... 但你确实需要非常具体。 (这甚至还没有考虑到不同的时区。)此外,请在发布时使用预览功能,以确保你的代码格式正确。 - Jon Skeet
3
周期是指日历日期之间的时间跨度,不考虑具体时间。如果您需要更高的精度,则不应使用周期。 - VGR
4个回答

17

关于Java-8类PeriodDuration的分析基本上是正确的。

  • java.time.Period类仅限于日历日期精度。
  • java.time.Duration类仅处理秒(和纳秒)精度,但将天始终视为等同于24小时= 86400秒。

通常,在计算一个人的年龄时,忽略时钟精度或时区完全足够,因为个人文件(如护照)不记录某个人出生时的确切时间。如果确实需要,那么Period类可以胜任其工作(但请小心处理其方法,例如getDays() - 见下文)。

但是您想要更高的精度,并以考虑时区的局部字段来描述结果。好吧,第一部分(精度)由Duration支持,但不包括第二部分。

使用Period也没有帮助,因为精确的时间差异(被Period忽略)可能会影响天数的差异。另外(只是打印你代码的输出):

    Period preciseBirthdayPeriod =
        Period.between(born.toLocalDate(), nowNormalized.toLocalDate());
    int preciseDaysOld = preciseBirthdayPeriod.getDays();
    System.out.println(preciseDaysOld); // 13
    System.out.println(preciseBirthdayPeriod); // P56Y11M13D

从上面可以看出,使用方法preciseBirthdayPeriod.getDays()来获取总天数是非常危险的。不,它只是总时间差的一部分,还有11个月和56年。 我认为最好打印出总时间差而不仅仅是天数,因为这样人们可以更容易地想象总时间差有多大(在社交媒体上经常见到“3年2个月4天”等打印持续时间的用例)。

显然,你需要一种方法来确定包括日历单位和时钟单位在内的特定时区中的持续时间(例如:某人出生的时区)。Java-8-time-library的不好之处在于:它不支持任何PeriodDuration的组合。导入外部库Threeten-Extra-class Interval也没有帮助,因为long daysPassed = interval.toDuration().toDays();仍会忽略时区效应(1天== 24小时),而且也无法以其他单位(如月份等)打印时间差。

总结:

您尝试了Period解决方案。 @swiedsw提供了Duration解决方案。两种方法都存在精度方面的缺点。您可以尝试在一个新的类中组合这两个类并实现必要的时间算术(不是那么简单),该类实现TemporalAmount

附注:

我已经在我的时间库Time4J中实现了你所需的内容,所以它可能对你自己的实现有用。例如:

Timezone bornZone = Timezone.of(AMERICA.NEW_YORK);
Moment bornTime =
    PlainTimestamp.of(1960, net.time4j.Month.JANUARY.getValue(), 1, 22, 34, 56).in(
        bornZone
    );
Moment currentTime = Moment.nowInSystemTime();
MomentInterval interval = MomentInterval.between(bornTime, currentTime);

MachineTime<TimeUnit> mt = interval.getSimpleDuration();
System.out.println(mt); // 1797324427.356000000s [POSIX]

net.time4j.Duration<?> duration =
    interval.getNominalDuration(
        bornZone, // relevant if the moments are crossing a DST-boundary
        CalendarUnit.YEARS,
        CalendarUnit.MONTHS,
        CalendarUnit.DAYS,
        ClockUnit.HOURS,
        ClockUnit.MINUTES
    );

// P56Y11M12DT12H52M (12 days if the birth-time-of-day is after current clock time)
// If only days were specified above then the output would be: P20801D
System.out.println(duration);
System.out.println(duration.getPartialAmount(CalendarUnit.DAYS)); // 12

这个例子也表明了我的普遍态度,即使用“月”、“日”、“小时”等单位在严格意义上并不是非常准确的。唯一严格准确的方法(从科学角度来看)是使用以十进制秒为单位的机器时间(最好是SI-秒,也可以在1972年之后使用Time4J)。


“SI-seconds”指的是维基百科中定义的“国际秒”吗? - Basil Bourque
@BasilBourque 是的,确切地说,SI秒是由国际单位制定义的。就给出的Time4J示例而言,它需要表达式interval.getRealDuration(),但正如所说的,这不适用于OP的用例,因为OP还使用了1960年(1972年之前)。 - Meno Hochschild

5
JavaDocPeriod的说明声明其模型为:

ISO-8601日历系统中基于日期的时间段,例如“2年3个月4天”。

我理解它没有参考时间点。
你可能想检查Interval项目ThreeTen-Extra,它的模型是:

两个瞬间之间不可变的时间间隔。

该项目网站指出该项目“[...]由Java 8日期和时间库的主要作者Stephen Colebourne管理”。
您可以通过在区间上调用toDuration()来检索Duration
我将转换您的代码以提供示例:
ZoneId bornIn = ZoneId.of("America/New_York");
ZonedDateTime born = ZonedDateTime.of(1960, Month.JANUARY.getValue(), 1, 2, 34, 56, 0, bornIn);
ZonedDateTime now = ZonedDateTime.now();
Interval interval = Interval.of(born.toInstant(), now.toInstant());
long daysPassed = interval.toDuration().toDays();

3
好的答案。换句话说,“Interval”附加到时间轴上,而“Period”和“Duration”则没有。 - Basil Bourque

1
两个类之间的主要区别是:
  • java.time.Period 使用基于日期的值(例如:2018年5月31日)
  • java.time.Duration 更加精确,使用基于时间的值(例如:"2018-05-31T11:45:20.223Z")

0

java.time.Period 更适合人类阅读

  • 例如,A和B之间的时间段为2年3个月3天

java.time.Duration 则更适合机器使用。


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