我试图计算两个LocalDateTime之间的差异。

输出格式为y年m个月d天h小时m分s秒。以下是我所写的:

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.ZoneId;

public class Main {

    static final int MINUTES_PER_HOUR = 60;
    static final int SECONDS_PER_MINUTE = 60;
    static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;

    public static void main(String[] args) {
        LocalDateTime toDateTime = LocalDateTime.of(2014, 9, 9, 19, 46, 45);
        LocalDateTime fromDateTime = LocalDateTime.of(1984, 12, 16, 7, 45, 55);

        Period period = getPeriod(fromDateTime, toDateTime);
        long time[] = getTime(fromDateTime, toDateTime);

        System.out.println(period.getYears() + " years " + 
                period.getMonths() + " months " + 
                period.getDays() + " days " +
                time[0] + " hours " +
                time[1] + " minutes " +
                time[2] + " seconds.");


    }

    private static Period getPeriod(LocalDateTime dob, LocalDateTime now) {
        return Period.between(dob.toLocalDate(), now.toLocalDate());
    }

    private static long[] getTime(LocalDateTime dob, LocalDateTime now) {
        LocalDateTime today = LocalDateTime.of(now.getYear(),
                now.getMonthValue(), now.getDayOfMonth(), dob.getHour(), dob.getMinute(), dob.getSecond());
        Duration duration = Duration.between(today, now);

        long seconds = duration.getSeconds();

        long hours = seconds / SECONDS_PER_HOUR;
        long minutes = ((seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
        long secs = (seconds % SECONDS_PER_MINUTE);

        return new long[]{hours, minutes, secs};
    }
}

我得到的输出是29年8个月24天12小时0分50秒。我已经从这个网站检查了我的结果(值12/16/1984 07:45:55和09/09/2014 19:46:45)。输出结果如下截图所示:

我非常确定,月份值之后的字段在我的代码中是错误的。任何建议都会很有帮助。

更新

我从另一个网站测试了我的结果,得到的结果是不同的。计算两个日期之间的持续时间(结果:29年8个月24天12小时0分50秒)。

更新

由于我从两个不同的网站得到了两个不同的结果,我想知道我的计算算法是否合法。如果我使用以下两个LocalDateTime对象:

LocalDateTime toDateTime = LocalDateTime.of(2014, 9, 10, 6, 40, 45);
LocalDateTime fromDateTime = LocalDateTime.of(1984, 12, 16, 7, 45, 55);

然后输出来了:29年8个月25天-1小时-5分-10秒。

从这个环节开始应该是29年8个月24天22小时54分50秒。所以算法也需要处理负数。

注意,这个问题不是关于哪个站点给了我什么结果,我需要知道正确的算法,需要有正确的结果。


当前回答

请记住:日历月和年并不代表固定的时间长度。例如,如果这段代码显示了1个月的差异,它可能意味着从28天到31天(其中1天= 24小时)。如果这对您的用例不合适,我强烈建议完全放弃月和年(这样您就可以只使用Duration类),或者最多为月和年设置一个固定的时间值(例如,一个月为30天,一年为365天)。

如果您仍然想继续(例如,因为您的用户已经期望月和年意味着日历月和年),您将注意到代码不像人们最初认为的那样简单(这也意味着它更容易出错;感谢@Alex注意到一个错误,现在已经修复了),这正是因为Java库设计者足够聪明,不会将日历周期(Period类)和确切的持续时间(Duration类)混合在一起。


博士TL;

前提条件:start <= end。

// Closest start datetime with same time-of-day as the end datetime.
// This is to make sure to only count full 24h days in the period.
LocalDateTime closestFullDaysStart = LocalDateTime.of(
    start.toLocalDate()
         .plusDays(end.toLocalTime().compareTo(start.toLocalTime()) < 0 ? 1 : 0),
    end.toLocalTime()
);

// Get the calendar period between the dates (full years, months & days).
Period period = Period.between(closestFullDaysStart.toLocalDate(), end.toLocalDate());

// Get the remainder as a duration (hours, minutes, etc.).
Duration duration = Duration.between(start, closestFullDaysStart);

然后使用方法period.getYears(), period.getMonths(), period.getDays(), duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()。

在网上试试!


扩大的答案

我将回答最初的问题,即如何以年、月、日、小时和分钟为单位获得两个LocalDateTimes之间的时间差,以便不同单位的所有值的“和”(见下面的注释)等于总时间差,并且每个单位中的值小于下一个较大的单位—即。分钟< 60,小时< 24,依此类推。

给定两个LocalDateTimes开始和结束,例如:

LocalDateTime start = LocalDateTime.of(2019, 11, 28, 17, 15);
LocalDateTime end = LocalDateTime.of(2020, 11, 30, 16, 44);

我们可以用Duration表示两者之间的绝对时间跨度——也许使用Duration。(开始、结束)之间。但是我们可以从Duration中提取的最大单位是天(作为时间单位相当于24小时)-请参阅下面的说明以了解解释。要使用更大的单位*(月,年),我们可以用一对(Period, Duration)来表示这个Duration,其中Period测量精确到天的差值,Duration表示余数。

We need to be careful here, because a Period is really a date difference, not an amount of time, and all its calculations are based on calendar dates (see the section below). For example, from 1st January 2000 at 23:59 to 2nd January 2000 at 00:01, a Period would say there is a difference of 1 day, because that's the difference between the two dates, even though the time delta is 2 minutes, much less than 24h. So, we need to start counting the calendar period at the next closest point in time which has the same time of day as the end point, so that any calendar days that we count actually correspond to full 24h durations:

LocalDateTime closestFullDaysStart = LocalDateTime.of(
    start.toLocalDate()
         // if the end time-of-day is earlier than the start time-of-day,
         // the next point in time with that time-of-day is one calendar day ahead
         // (the clock "wraps around" at midnight while advancing to it)
         .plusDays(end.toLocalTime().compareTo(start.toLocalTime()) < 0 ? 1 : 0),
    end.toLocalTime()
);

现在我们已经有效地将开始和结束之间的时间跨度分成了两部分:从开始到closefulldaysstart的时间跨度,它的构造将小于24h,因此我们可以使用Duration对象来测量它,不包含天数部分,

Duration duration = Duration.between(start, closestFullDaysStart);

以及从closefulldaysstart到end的时间跨度,我们知道我们现在可以可靠地用一个Period来测量。

Period period = Period.between(closestFullDaysStart.toLocalDate(), end.toLocalDate());

现在我们可以简单地使用Period和Duration中定义的方法来提取单个单元:

System.out.printf(
    "%d years, %d months, %d days, %d hours, %d minutes, %d seconds",
    period.getYears(), period.getMonths(), period.getDays(), 
    duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()
);
1 years, 0 months, 1 days, 23 hours, 29 minutes, 0 seconds

或者,使用默认格式:

System.out.println(period + " + " + duration);
P1Y1D + PT23H29M

*注年、月、日

注意,在java中。时间的概念,像“月”或“年”这样的周期“单位”并不代表一个固定的、绝对的时间值——它们依赖于日期和日历,如下面的例子所示:

LocalDateTime
        start1 = LocalDateTime.of(2020, 1, 1, 0, 0),
        end1 = LocalDateTime.of(2021, 1, 1, 0, 0),
        start2 = LocalDateTime.of(2021, 1, 1, 0, 0),
        end2 = LocalDateTime.of(2022, 1, 1, 0, 0);
System.out.println(Period.between(start1.toLocalDate(), end1.toLocalDate()));
System.out.println(Duration.between(start1, end1).toDays());
System.out.println(Period.between(start2.toLocalDate(), end2.toLocalDate()));
System.out.println(Duration.between(start2, end2).toDays());
P1Y
366
P1Y
365

另一个例子,从2000年1月1日23:59到2000年1月2日00:01,一个Period会说有1天的差,因为这是两个日期之间的差,即使时间增量小于24小时。

消极的跨越

如果开始>结束,上面的代码生成的答案在技术上是正确的,如果我们把所有的单位相加,但以一种意想不到的方式呈现。

例如,对于

LocalDateTime start = LocalDateTime.of(2020, 1, 1, 1, 00);
LocalDateTime end = LocalDateTime.of(2020, 1, 1, 0, 0);

我们得到:

0 years, 0 months, -1 days, 23 hours, 0 minutes, 0 seconds

-1天加23小时等于-1小时,这是正确的。但我们期望答案是-1小时。

当前显示的持续时间总是正的,这是因为closefulldaysstart总是在未来开始。

如果开始>结束,但是,从开始到结束的方向是“回到时间”,所以我们需要从开始回溯,以找到与开始和结束之间的一天的结束时间最近的datetime。 此外,环绕条件也会发生变化:因为我们向后绕时钟,所以如果开始时间比结束时间早,我们就会环绕时钟(因此需要减去一个日历日)。

综合所有这些因素可以得出:

int endStartComparison = end.compareTo(start);
int endStartTimeComparison = end.toLocalTime().compareTo(start.toLocalTime());
LocalDateTime closestFullDaysStart = LocalDateTime.of(
    start.toLocalDate()
         .plusDays(
             (
                 (endStartComparison > 0)
                 ? (endStartTimeComparison < 0)
                 : (endStartTimeComparison > 0)
             )
             ? (long) Math.signum(endStartComparison)  // advance one unit in the direction start->end 
             : 0
         ),
    end.toLocalTime()
);

我们可以用一种更简洁但更hack的方式重写丑陋的嵌套条件:

(endStartComparison * endStartTimeComparison < 0)

在网上试试!

其他回答

不幸的是,似乎没有一个period类也可以跨越时间,所以您可能必须自己进行计算。

幸运的是,日期和时间类有很多实用程序方法,在某种程度上简化了这一点。这里有一个计算差异的方法,虽然不一定是最快的:

LocalDateTime fromDateTime = LocalDateTime.of(1984, 12, 16, 7, 45, 55);
LocalDateTime toDateTime = LocalDateTime.of(2014, 9, 10, 6, 40, 45);

LocalDateTime tempDateTime = LocalDateTime.from( fromDateTime );

long years = tempDateTime.until( toDateTime, ChronoUnit.YEARS );
tempDateTime = tempDateTime.plusYears( years );

long months = tempDateTime.until( toDateTime, ChronoUnit.MONTHS );
tempDateTime = tempDateTime.plusMonths( months );

long days = tempDateTime.until( toDateTime, ChronoUnit.DAYS );
tempDateTime = tempDateTime.plusDays( days );


long hours = tempDateTime.until( toDateTime, ChronoUnit.HOURS );
tempDateTime = tempDateTime.plusHours( hours );

long minutes = tempDateTime.until( toDateTime, ChronoUnit.MINUTES );
tempDateTime = tempDateTime.plusMinutes( minutes );

long seconds = tempDateTime.until( toDateTime, ChronoUnit.SECONDS );

System.out.println( years + " years " + 
        months + " months " + 
        days + " days " +
        hours + " hours " +
        minutes + " minutes " +
        seconds + " seconds.");

//prints: 29 years 8 months 24 days 22 hours 54 minutes 50 seconds.

基本的想法是这样的:创建一个临时的开始日期,并得到完整的年份结束。然后按年数调整该日期,使开始日期距离结束日期少于一年。按降序对每个时间单位重复上述操作。

最后一个免责声明:我没有考虑不同的时区(两个日期应该在同一个时区),我也没有测试/检查夏令时或日历中的其他变化(比如萨摩亚的时区变化)如何影响这个计算。所以要小心使用。

我发现最好的方法就是使用ChronoUnit。

long minutes = ChronoUnit.MINUTES.between(fromDate, toDate);
long hours = ChronoUnit.HOURS.between(fromDate, toDate);

其他文档在这里:https://docs.oracle.com/javase/tutorial/datetime/iso/period.html

塔帕斯玻色码和托马斯码存在一些问题。如果time differenс为负值,则array获得负值。例如,如果

LocalDateTime toDateTime = LocalDateTime.of(2014, 9, 10, 6, 46, 45);
LocalDateTime fromDateTime = LocalDateTime.of(2014, 9, 9, 7, 46, 45);

它返回0年0月1天-1小时0分0秒。

我认为正确的输出是:0年0月0天23小时0分0秒。

我建议在LocalDate和LocalTime实例上分离LocalDateTime实例。之后,我们可以获得Java 8 Period和Duration实例。Duration实例根据天数和全天时间值(< 24h)进行分离,并对周期值进行后续更正。当第二个LocalTime值在第一个LocalTime值之前时,需要将周期缩短一天。

下面是我计算LocalDateTime差异的方法:

private void getChronoUnitForSecondAfterFirst(LocalDateTime firstLocalDateTime, LocalDateTime secondLocalDateTime, long[] chronoUnits) {
    /*Separate LocaldateTime on LocalDate and LocalTime*/
    LocalDate firstLocalDate = firstLocalDateTime.toLocalDate();
    LocalTime firstLocalTime = firstLocalDateTime.toLocalTime();

    LocalDate secondLocalDate = secondLocalDateTime.toLocalDate();
    LocalTime secondLocalTime = secondLocalDateTime.toLocalTime();

    /*Calculate the time difference*/
    Duration duration = Duration.between(firstLocalDateTime, secondLocalDateTime);
    long durationDays = duration.toDays();
    Duration throughoutTheDayDuration = duration.minusDays(durationDays);
    Logger.getLogger(PeriodDuration.class.getName()).log(Level.INFO,
            "Duration is: " + duration + " this is " + durationDays
            + " days and " + throughoutTheDayDuration + " time.");

    Period period = Period.between(firstLocalDate, secondLocalDate);

    /*Correct the date difference*/
    if (secondLocalTime.isBefore(firstLocalTime)) {
        period = period.minusDays(1);
        Logger.getLogger(PeriodDuration.class.getName()).log(Level.INFO,
                "minus 1 day");
    }

    Logger.getLogger(PeriodDuration.class.getName()).log(Level.INFO,
            "Period between " + firstLocalDateTime + " and "
            + secondLocalDateTime + " is: " + period + " and duration is: "
            + throughoutTheDayDuration
            + "\n-----------------------------------------------------------------");

    /*Calculate chrono unit values and  write it in array*/
    chronoUnits[0] = period.getYears();
    chronoUnits[1] = period.getMonths();
    chronoUnits[2] = period.getDays();
    chronoUnits[3] = throughoutTheDayDuration.toHours();
    chronoUnits[4] = throughoutTheDayDuration.toMinutes() % 60;
    chronoUnits[5] = throughoutTheDayDuration.getSeconds() % 60;
}

上述方法可用于计算任何本地日期和时间值的差值,例如:

public long[] getChronoUnits(String firstLocalDateTimeString, String secondLocalDateTimeString) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    LocalDateTime firstLocalDateTime = LocalDateTime.parse(firstLocalDateTimeString, formatter);
    LocalDateTime secondLocalDateTime = LocalDateTime.parse(secondLocalDateTimeString, formatter);

    long[] chronoUnits = new long[6];
    if (secondLocalDateTime.isAfter(firstLocalDateTime)) {
        getChronoUnitForSecondAfterFirst(firstLocalDateTime, secondLocalDateTime, chronoUnits);
    } else {
        getChronoUnitForSecondAfterFirst(secondLocalDateTime, firstLocalDateTime, chronoUnits);
    }
    return chronoUnits;
}

为上述方法编写单元测试很方便(它们都是PeriodDuration类成员)。代码如下:

@RunWith(Parameterized.class)
public class PeriodDurationTest {

private final String firstLocalDateTimeString;
private final String secondLocalDateTimeString;
private final long[] chronoUnits;

public PeriodDurationTest(String firstLocalDateTimeString, String secondLocalDateTimeString, long[] chronoUnits) {
    this.firstLocalDateTimeString = firstLocalDateTimeString;
    this.secondLocalDateTimeString = secondLocalDateTimeString;
    this.chronoUnits = chronoUnits;
}

@Parameters
public static Collection<Object[]> periodValues() {
    long[] chronoUnits0 = {0, 0, 0, 0, 0, 0};
    long[] chronoUnits1 = {0, 0, 0, 1, 0, 0};
    long[] chronoUnits2 = {0, 0, 0, 23, 0, 0};
    long[] chronoUnits3 = {0, 0, 0, 1, 0, 0};
    long[] chronoUnits4 = {0, 0, 0, 23, 0, 0};
    long[] chronoUnits5 = {0, 0, 1, 23, 0, 0};
    long[] chronoUnits6 = {29, 8, 24, 12, 0, 50};
    long[] chronoUnits7 = {29, 8, 24, 12, 0, 50};
    return Arrays.asList(new Object[][]{
        {"2015-09-09 21:46:44", "2015-09-09 21:46:44", chronoUnits0},
        {"2015-09-09 21:46:44", "2015-09-09 22:46:44", chronoUnits1},
        {"2015-09-09 21:46:44", "2015-09-10 20:46:44", chronoUnits2},
        {"2015-09-09 21:46:44", "2015-09-09 20:46:44", chronoUnits3},
        {"2015-09-10 20:46:44", "2015-09-09 21:46:44", chronoUnits4},
        {"2015-09-11 20:46:44", "2015-09-09 21:46:44", chronoUnits5},
        {"1984-12-16 07:45:55", "2014-09-09 19:46:45", chronoUnits6},
        {"2014-09-09 19:46:45", "1984-12-16 07:45:55", chronoUnits6}
    });
}

@Test
public void testGetChronoUnits() {
    PeriodDuration instance = new PeriodDuration();
    long[] expResult = this.chronoUnits;
    long[] result = instance.getChronoUnits(this.firstLocalDateTimeString, this.secondLocalDateTimeString);
    assertArrayEquals(expResult, result);
}

}

无论第一个LocalDateTime值是否在任何LocalTime值之前或之前,所有测试都是成功的。

Groovy中@Thomas的版本将所需的单位放在列表中,而不是硬编码值。这个实现(可以很容易地移植到Java -我将函数声明显式)使Thomas方法更具可重用性。

def fromDateTime = LocalDateTime.of(1968, 6, 14, 0, 13, 0)
def toDateTime = LocalDateTime.now()
def listOfUnits = [
    ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.DAYS,
    ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS,
    ChronoUnit.MILLIS]

println calcDurationInTextualForm(listOfUnits, fromDateTime, toDateTime)    

String calcDurationInTextualForm(List<ChronoUnit> listOfUnits, LocalDateTime ts, LocalDateTime to)
{
    def result = []

    listOfUnits.each { chronoUnit ->
        long amount = ts.until(to, chronoUnit)
        ts = ts.plus(amount, chronoUnit)

        if (amount) {
            result << "$amount ${chronoUnit.toString()}"
        }
    }

    result.join(', ')
}

在撰写本文时,上面的代码返回47年8个月9天22小时52分7秒140 Millis。并且,对于@Gennady Kolomoets输入,代码返回23 Hours。

当你提供一个单位列表时,它必须按单位的大小排序(最大的优先):

def listOfUnits = [ChronoUnit.WEEKS, ChronoUnit.DAYS, ChronoUnit.HOURS]
// returns 2495 Weeks, 3 Days, 8 Hours

这里有一个使用Duration和TimeUnit获取'hh:mm:ss'格式的例子。

Duration dur = Duration.between(localDateTimeIni, localDateTimeEnd);
long millis = dur.toMillis();

String.format("%02d:%02d:%02d", 
        TimeUnit.MILLISECONDS.toHours(millis),
        TimeUnit.MILLISECONDS.toMinutes(millis) - 
        TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millis)),
        TimeUnit.MILLISECONDS.toSeconds(millis) - 
        TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)));