【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 並沒有按照日曆來計算,只是單純地做了減法。。