欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

程序员文章站 2022-04-16 21:17:39
...

iOS 自带了控件,可以自动根据日历来计算 2 个时间相差的自然年、月、日、小时、分钟、秒。Java 没有自带此方法,只能自己来算了~

一、竖式减法实现

我自己写了一个方法,测试了一些时间和 iOS 作对比,暂时没有发现什么问题。如有错误,欢迎指正,也欢迎提意见~

源码见:TimeIntervalUtils.java

主要方法是 int[] getTimeIntervalArray(Calendar nextDate, Calendar previousDate) 。

主要思路是:

根据数学的竖式减法,从低位由 "秒→年" 依次进行减法计算,不够则向前借一位。

这里先分别将 [年, 月, 日, 小时, 分钟, 秒] 每一位的差值计算出来,然后从低位开始由 "秒→年" 依次判断:如果小于 0 ,则向前借一位,当前位补上对应的值,前一位则需要减1。

补值这里应该也好理解:"秒"、"分"补 60,"时"补 24,"月"补 12

需要注意的是 "",也就是"天数"的处理。因为是按照日历上的日期来计算的日期差值,所以需要考虑到月份的问题,每个月的天数是不一样的,那么补值也是不一样的。每个月的天数可能有28、29、30、31天,这里我是按照截止日期 nextDate 的上一个月的天数来作补值的。比如 nextDate 是 20160229,那么上个月是 1 月份,补值是 31 天。

/**
 * 获取 2 个时间的自然年历的时间间隔
 *
 * @param nextDate     后面的时间,需要大于 previousDate
 * @param previousDate 前面的时间
 * @return [年, 月, 日, 小时, 分钟, 秒]的数组
 */
public static int[] getTimeIntervalArray(Calendar nextDate, Calendar previousDate) {
    int year = nextDate.get(Calendar.YEAR) - previousDate.get(Calendar.YEAR);
    int month = nextDate.get(Calendar.MONTH) - previousDate.get(Calendar.MONTH);
    int day = nextDate.get(Calendar.DAY_OF_MONTH) - previousDate.get(Calendar.DAY_OF_MONTH);
    int hour = nextDate.get(Calendar.HOUR_OF_DAY) - previousDate.get(Calendar.HOUR_OF_DAY);// 24小时制
    int min = nextDate.get(Calendar.MINUTE) - previousDate.get(Calendar.MINUTE);
    int second = nextDate.get(Calendar.SECOND) - previousDate.get(Calendar.SECOND);

    boolean hasBorrowDay = false;// "时"是否向"天"借过一位

    if (second < 0) {
        second += 60;
        min--;
    }

    if (min < 0) {
        min += 60;
        hour--;
    }

    if (hour < 0) {
        hour += 24;
        day--;

        hasBorrowDay = true;
    }

    if (day < 0) {
        // 计算截止日期的上一个月有多少天,补上去
        Calendar tempDate = (Calendar) nextDate.clone();
        tempDate.add(Calendar.MONTH, -1);// 获取截止日期的上一个月
        day += tempDate.getActualMaximum(Calendar.DAY_OF_MONTH);

        // nextDate是月底最后一天,且day=这个月的天数,即是刚好一整个月,比如20160131~20160229,day=29,实则为1个月
        if (!hasBorrowDay
                && nextDate.get(Calendar.DAY_OF_MONTH) == nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)// 日期为月底最后一天
                && day >= nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)) {// day刚好是nextDate一个月的天数,或大于nextDate月的天数(比如2月可能只有28天)
            day = 0;// 因为这样判断是相当于刚好是整月了,那么不用向 month 借位,只需将 day 置 0
        } else {// 向month借一位
            month--;
        }
    }

    if (month < 0) {
        month += 12;
        year--;
    }

    return new int[]{year, month, day, hour, min, second};
}

重点讲一下 day < 0 的情况。

if (day < 0) {
    // 计算截止日期的上一个月有多少天,补上去
    Calendar tempDate = (Calendar) nextDate.clone();
    tempDate.add(Calendar.MONTH, -1);// 获取截止日期的上一个月
    day += tempDate.getActualMaximum(Calendar.DAY_OF_MONTH);

    // nextDate是月底最后一天,且day=这个月的天数,即是刚好一整个月,比如20160131~20160229,day=29,实则为1个月
    if (!hasBorrowDay
            && nextDate.get(Calendar.DAY_OF_MONTH) == nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)// 日期为月底最后一天
            && day >= nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)) {// day刚好是nextDate一个月的天数,或大于nextDate月的天数(比如2月可能只有28天)
        day = 0;
    } else {// 向month借一位
        month--;
    }
}

这里需要注意一下: 什么情况下刚好算作日历上的一整个月呢?

(1)这里拿 2 月份来举个栗子。比如 2016 年 2 月有 29 天,① "20160129~20160229"、② "20160130~20160229"、③ "20160131~20160229",这 3 个时间段的差值结果分别是 31 天、30天、29天,但按自然月算其实都是 "1 个月" 。① 按照我们平常的普通算法直接"年-月-日"相减,刚好是 1 个月,③ 按照日历上的日期,也是刚好 1 个月。这种情况,是 nextDate 刚好是月底最后一天 2 月29 号,如果 previousDate 是在 29 号~31 号之间,并且 hasBorrowDay = false 没有借位,那么都是算作整月的。

nextDate 非月底的情况,如 "20160127~20160225" 是 29 天,"20160129~20160228" 是 30 天,计算结果则只显示天数,不会显示为 1 个月了。

(2)如果你还需要计算 时分秒 的差值,那么这里还要注意一点,如果 "时" 向 "天" 借了一位,会影响 "天" 的计算的(是否是刚好 1 个月,或者是否需要向 "月" 借位)。所以在 hour < 0 时添加了一个标识 hasBorrowDay = true 表示 "天" 被借了一位。

举个栗子:"2016-01-30 01:59:59 ~ 2016-02-29 00:59:59"。

我们来进行竖式减法运算, "[]" 内的顺序为 [year, month, day, hour, minute, second],以英文标识。

  2016-02-29 00:59:59

- 2016-01-30 01:59:59

------------------------------

①     [0, 1, -1, -1, 0, 0]

②     [0, 1, -2, 23, 0, 0]

③     [0, 0, 29, 23, 0, 0]

① 分别计算 [year, month, day, hour, minute, second] 的差值,得到: [0, 1, -1, -1, 0, 0];

② 因为 hour = -1 < 0,需要向 day 借一位来补值,补值为 24,hour = -1 + 24 = 23;

day 被借位了,需要自减 1,day = -1 -1 = -2, 并标识 hasBorrowDay = true ;得到:[0, 1, -2, 23, 0, 0];

③ 因为 day = -2 < 0,需要向 month 借一位来补值。2016-02-29 的上一个月是 1 月份 有 31 天,所以补值为 31,day = -2 + 31 = 29;

这里,hasBorrowDay = true,所以 month 被借位了需要自减 1 ,month = 1 - 1 = 0;得到:[0, 0, 29, 23, 0, 0];

④ 最后结果为 [0, 0, 29, 23, 0, 0],即 29 天 23 小时。

和 iOS 的结果一致(见下图)。

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

那如果我们没有标识 hasBorrowDay = true 会有什么影响呢?

回到第 ③ 步,我们让 hasBorrowDay 一直为 false:

因为 day = -2 < 0,需要向 month 借一位来补值。2016-02-29 的上一个月是 1 月份 有 31 天,所以补值为 31,day = -2 + 31 = 29;

2016-02-29是月底最后一天,所以 

nextDate.get(Calendar.DAY_OF_MONTH) == nextDate.getActualMaximum(Calendar.DAY_OF_MONTH) //为真

day = 29,而 2 月份正好只有 29 天,所以

day >= nextDate.getActualMaximum(Calendar.DAY_OF_MONTH) //为真

那么按照这个逻辑:

if (!hasBorrowDay
        && nextDate.get(Calendar.DAY_OF_MONTH) == nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)// 日期为月底最后一天
        && day >= nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)) {// day刚好是nextDate一个月的天数,或大于nextDate月的天数(比如2月可能只有28天)
    day = 0; // 会执行此步,因为这样判断是相当于刚好是整月了,那么不用向 month 借位,只需将 day 置 0
} else {// 向month借一位
    month--;
}

day 被置 0 了,最后得到的结果是:[0, 1, 0, 23, 0, 0],即 1 个月 23 小时。这样和日历上的自然时间就不是很符合了。

-------------------------------------------------------------分析完毕-----------------------------------------------------

gist 可能打不开(墙),这里附上整个类的源码:

package com.test.Utils;

import android.text.TextUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/**
 * desc   : 获取时间间隔
 * version:
 * date   : 2018/8/7
 * author : DawnYu
 * GitHub : DawnYu9
 */
public class TimeIntervalUtils {
    /**
     * 获取当前时间
     *
     * @param template 时间格式,默认为 "yyyy-MM-dd HH:mm:ss"
     * @return
     */
    public static String getCurrentDateString(String template) {
        if (TextUtils.isEmpty(template)) {
            template = "yyyy-MM-dd HH:mm:ss";// 大写"HH":24小时制,小写"hh":12小时制
        }

        SimpleDateFormat formatter = new SimpleDateFormat(template, Locale.getDefault());

        System.out.println("getCurrentDateString = " + formatter.format(new Date()));

        return formatter.format(new Date());
    }

    /**
     * 获取 2 个时间的自然年历的时间间隔
     *
     * @param nextTime     后面的时间,需要大于 previousTime,空则默认为当前时间
     * @param previousTime 前面的时间,空则默认为当前时间
     * @param format       时间格式,eg:"yyyy-MM-dd", "yyyy-MM-dd hh:mm:ss"
     * @return [年, 月, 日, 小时, 分钟, 秒]的数组
     */
    public static int[] getTimeIntervalArray(String nextTime, String previousTime, String format) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.getDefault());
        Date nextDate;
        Date previousDate;
        Calendar nextCalendar = Calendar.getInstance();
        Calendar previousCalendar = Calendar.getInstance();

        // 空则取当前时间
        try {
            nextDate = dateFormat.parse(TextUtils.isEmpty(nextTime) ? getCurrentDateString(format) : nextTime);
            nextCalendar.setTime(nextDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        // 空则取当前时间
        try {
            previousDate = dateFormat.parse(TextUtils.isEmpty(previousTime) ? getCurrentDateString(format) : previousTime);
            previousCalendar.setTime(previousDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        return getTimeIntervalArray(nextCalendar, previousCalendar);
    }

    /**
     * 获取 2 个时间的自然年历的时间间隔
     *
     * @param nextDate     后面的时间,需要大于 previousDate
     * @param previousDate 前面的时间
     * @return [年, 月, 日, 小时, 分钟, 秒]的数组
     */
    public static int[] getTimeIntervalArray(Calendar nextDate, Calendar previousDate) {
        int year = nextDate.get(Calendar.YEAR) - previousDate.get(Calendar.YEAR);
        int month = nextDate.get(Calendar.MONTH) - previousDate.get(Calendar.MONTH);
        int day = nextDate.get(Calendar.DAY_OF_MONTH) - previousDate.get(Calendar.DAY_OF_MONTH);
        int hour = nextDate.get(Calendar.HOUR_OF_DAY) - previousDate.get(Calendar.HOUR_OF_DAY);// 24小时制
        int min = nextDate.get(Calendar.MINUTE) - previousDate.get(Calendar.MINUTE);
        int second = nextDate.get(Calendar.SECOND) - previousDate.get(Calendar.SECOND);

        boolean hasBorrowDay = false;// "时"是否向"天"借过一位

        if (second < 0) {
            second += 60;
            min--;
        }

        if (min < 0) {
            min += 60;
            hour--;
        }

        if (hour < 0) {
            hour += 24;
            day--;

            hasBorrowDay = true;
        }

        if (day < 0) {
            // 计算截止日期的上一个月有多少天,补上去
            Calendar tempDate = (Calendar) nextDate.clone();
            tempDate.add(Calendar.MONTH, -1);// 获取截止日期的上一个月
            day += tempDate.getActualMaximum(Calendar.DAY_OF_MONTH);

            // nextDate是月底最后一天,且day=这个月的天数,即是刚好一整个月,比如20160131~20160229,day=29,实则为1个月
            if (!hasBorrowDay
                    && nextDate.get(Calendar.DAY_OF_MONTH) == nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)// 日期为月底最后一天
                    && day >= nextDate.getActualMaximum(Calendar.DAY_OF_MONTH)) {// day刚好是nextDate一个月的天数,或大于nextDate月的天数(比如2月可能只有28天)
                day = 0;// 因为这样判断是相当于刚好是整月了,那么不用向 month 借位,只需将 day 置 0
            } else {// 向month借一位
                month--;
            }
        }

        if (month < 0) {
            month += 12;
            year--;
        }

        return new int[]{year, month, day, hour, min, second};
    }
}

测试代码:

/**
 * 测试时间间隔
 */
private void testTimeInterval() {
    String nextTime = "2016-02-29 00:59:59";
    String preTime = "2016-01-30 01:59:59";
    String format = "yyyy-MM-dd hh:mm:ss";

    System.out.println("----------------------\n"
            + "nextTime = " + nextTime + "\n"
            + "preTime  = " + preTime + "\n"
            + Arrays.toString(TimeIntervalUtils.getTimeIntervalArray(nextTime, preTime, format)) + "\n"
            + "----------------------");
}

部分测试结果和 iOS 对比:

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

 

二、 Period 类(Java 8,Android 8.0 以上)

参考博客:Java计算时间差、日期差总结

现在 Java 8 中有一个周期类 Period ,Android 需要 API level 26 以上,即 8.0 以上系统才可以使用 Period 。但是 Period 只支持计算 "年、月、日" 的差值,而且和 iOS 的算法也不太一样。

在 Period 的源码注释里可以看到其只能计算 "年、月、日" 的差值。

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

测试代码:

public void testTimeIntervalByPeriod() {
    LocalDate nextDate = LocalDate.of(2016, 2, 29);
    LocalDate preDate = LocalDate.of(2016, 1, 31);

    Period p = Period.between(preDate, nextDate);

    Log.i("testTimeIntervalByPeriod",
            "-------\n"
                    + "nextDate:" + nextDate + "\n"
                    + "preDate: " + preDate + "\n"
                    + "Period 时间差:" + p.getYears() + " 年 " + p.getMonths() + " 月 " + p.getDays() + " 天 ");
}

测试结果:

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

【Java】根据日历计算2个时间相差多少#自然#年、月、日、小时、分钟、秒

可见,Period 并没有按照日历来计算,只是单纯地做了减法。。