1. 程式人生 > >【Java】根據日曆計算2個時間相差多少#自然#年、月、日、小時、分鐘、秒

【Java】根據日曆計算2個時間相差多少#自然#年、月、日、小時、分鐘、秒

iOS 自帶了控制元件,可以自動根據日曆來計算 2 個時間相差的自然年、月、日、小時、分鐘、秒。Java 沒有自帶此方法,只能自己來算了~

一、豎式減法實現

我自己寫了一個方法,測試了一些時間和 iOS 作對比,暫時沒有發現什麼問題。如有錯誤,歡迎指正,也歡迎提意見~

主要思路是:

根據數學的豎式減法,從低位由 "秒→年" 依次進行減法計算,不夠則向前借一位。

這裡先分別將 [年, 月, 日, 小時, 分鐘, 秒] 每一位的差值計算出來,然後從低位開始由 "秒→年" 依次判斷:如果小於 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 的結果一致(見下圖)。

那如果我們沒有標識 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 對比:

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

現在 Java 8 中有一個週期類 Period ,Android 需要 API level 26 以上,即 8.0 以上系統才可以使用 Period 。但是 Period 只支援計算 "年、月、日" 的差值,而且和 iOS 的演算法也不太一樣。

在 Period 的原始碼註釋裡可以看到其只能計算 "年、月、日" 的差值。

測試程式碼:

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() + " 天 ");
}

測試結果:

可見,Period 並沒有按照日曆來計算,只是單純地做了減法。。