Joda-Time 是由 Stephen Colebourne 於 2002 年開始建立,版本 1.0 於 2005 年釋出,而 2007 年釋出版本 2.0,撰寫此文件的時候,最新版本為 2.3。
有鑑於 Date
與 Calendar
的問題,Joda-Time 抽取了時間處理時幾個重要觀念,作為實作與使用 API 時的重要考量 …
Instant
連續時間軸上的某個瞬間,採用 UTC 1970 年 1 月 1 日 00:00:00 至該瞬間歷經的毫秒數(與 Unix 時間相同)…
在實作上,使用 ReadableInstant
介面來定義 Instant
的行為,其實作類別包括有:
Instant
:簡單的實作類別,常用作時區、日歷轉換時的中性資料。DateTime
:最常用的類別,常搭配時區、日歷資訊來建立或取得日期、時間等欄位資訊。MutableDateTime
:前三個類別的實例都是不可變(Immutable),MutableDateTime
的實例為可變。
Joda-Time 記取了 Date
與 Calendar
實例為可變時可能發生的問題,因而 Instant
、DateTime
都設計為不可變。ReadableInstant
介面的實例,行為上可基於毫秒數來進行時間運算,若要取得年、月、日、時、分、秒等欄位資訊,必須提供時區與年曆資訊。例如,若要取得目前時間,並使用預設年曆與時區(稍後說明)來取得月份資訊,以下是個簡單範例:
DateTime dt = new DateTime(); // 使用預設年曆與時區
int month = dt.getMonthOfYear(); // 取得目前月份,1 就是一月
month = dt.monthOfYear().get(); // 取得目前月份的另一方式
String monthDesc = dt.monthOfYear().getAsText(); // 取得月份的文字描述
Partial
人類在日常生活上使用時間,通常不需要完整的時間概念,只需要片段的時間資訊。例如,我們會說某人的生日是「5 月 26 日」,現在的時間是「下午 1 點 6 分」… 實際上,「5 月 26 日」這樣的片段時間資訊,可以指任何一年的「5 月 26 日」,而「下午 1 點 6 分」這樣的片段時間資訊,也可以用於任何一天的「下午 1 點 6 分」…
片段時間資訊的概念,在 Joda-Time 中定義為 Partial
,實作上由 ReadablePartial
定義出 Partial
的行為,其實作包括有以下類別,實例皆為不可變,從名稱上應可一目瞭然各自的作用:
LocalDate
、LocalTime
、LocalDateTime
YearMonth
、MonthDay
Partial
YearMonthDay
TimeOfDay
既然是片段的時間資訊,那麼只要補齊不全的部份,就可以轉為時間軸上確切的某個瞬間…
具體來說,也就是 ReadablePartial
的實例可以組合以產生 ReadableInstant
的實例。例如:
LocalDate date = new LocalDate(2004, 12, 25); // 年、月、日的日期片段資訊
LocalTime time = new LocalTime(12, 20); // 時、分的時間片段資訊
DateTime dt = date.toDateTime(time); // 使用預設時區
反過來說,如果你有個 ReadableInstant
實例,像是 DateTime
,也可以取得其中的時間片段。例如:
DateTime dt = new DateTime();
LocalDate date = new LocalDate(dt); // 只取出日期片段資訊
Interval
有時你會想要表達時間上某個區段,像是西元 1975 年 5 月 26 日到 西元 1978 年 7 月 23 日,像這類包括開始瞬間與結束瞬間的區段,Joda-Time 定義為 Interval
…1
實作上由 ReadableInterval
介面定義行為,實作類別有:
Interval
MutableInterval
建立 Interval
實例的程式片段之一如下:
DateTime start = new DateTime(1975, 5, 26, 0, 0, 0);
DateTime end = new DateTime(1978, 7, 23, 0, 0, 0);
Interval interval = new Interval(start, end);
Duration
現在的時間「再 1000 毫秒」後會是多?這類再持續多久的期間概念,就是 Joda-Time 中定義的 Interval
,不過持續的時間單位是毫秒。實作上使用 ReadableDuration
介面定義,實作類別有 Duration
。
自然地,如果在某個瞬間加上 Duration
,就會得到另一個瞬間:
具體來說,可以給予 ReadableInstant
的實例某個 ReadableDuration
實例,從而得到另一個 ReadableInstant
,例如 …
DateTime start = new DateTime(1975, 5, 26, 0, 0, 0);
Duration oneThousandMillis = new Duration(1000);
DateTime end = start.plus(oneThousandMillis);
Period
使用毫秒?那要表示「再三分鐘」、「再兩天」的概念不是很麻煩嗎?是的!沒有錯!人類通常很少使用毫秒,因此,Joda-Time 把人類慣用的時間持續定義為 Period
,實作上使用 ReadablePeriod
介面定義行為,實作的類別有以下幾個:
Period
MutablePeriod
Years
、Months
、Weeks
、Days
Hours
、Minutes
、Seconds
自然地,如果在某個瞬間加上 Period
,也會得到另一個瞬間:
只不過這次是人類常用的時間概念,例如,想知道兩個時間軸上的瞬間到底是多少天嗎?可以這麼撰寫程式來得知:
DateTime start = new DateTime(1975, 5, 26, 0, 0, 0);
DateTime end = new DateTime(1978, 7, 23, 0, 0, 0);
Days days = Days.daysBetween(start, end);
以上是有關於使用 Joda-Time 時,應該知道的幾個重要觀念,其中我們看到了年曆與時區 …
在 Joda-Time 的 API 設計上,年曆系統是可以抽換的,用以從某個瞬間計算日期時間的各個欄位,Chronology
是年曆實作類別的抽象父類別,許多情況下若沒有指定,會使用 ISOChronology
作為預設,這是依 ISO8601 的實作類別。
在 【Joda-Time 與 JSR310 】(2)時間的 ABC 中談過,JDK 中 Calendar
的實作類別 GregorianCalendar
,其實是個儒略曆與格里高利曆的混合年曆,在 Joda-Time 中對應的實作類別是 GJChronology
,如果你要純綷的儒略曆,在 Joda-Time 中是 JulianChronology
,如果你要純綷的格里高利曆,在 Joda-Time 中是 GregorianChronology
。除此之外,Joda-Time 中還有…
- Buddhist –
BuddhistChronology
- Coptic –
CopticChronology
- Ethiopic –
EthiopicChronology
- Islamic –
IslamicChronology
要使用科普特曆(Coptic calendar)來表示倫敦的日期時間,以下是個示範:
DateTimeZone zone = DateTimeZone.forID("Europe/London");
Chronology coptic = CopticChronology.getInstance(zone);
DateTime dt = new DateTime(coptic);
int year = dt.getYear();
int month = dt.getMonthOfYear();
Joda-Time 會將時區資料編譯包裝為單一 JAR 檔案,你可以按照 Time zone update 資訊,隨時更新與重新編譯 JAR 檔案,Available Time Zones 中則列出了 Joda-Time 中所有的時區資訊。
談了這麼多觀念,該來端出些牛肉,看看使用 Joda-Time 與使用 JDK 的日期時間 API,到底有什麼不同,先來看看問題的這個片段:
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date birthDate = dateFormat.parse(args[0]);
Date nowDate = new Date();
long lifeMillis = nowDate.getTime() - birthDate.getTime();
System.out.printf("你今年的歲數為:%d%n",
lifeMillis / (365 * 24 * 60 * 60 * 1000));
不但囉嗦而且出錯了,改用 Joda-Time 之後是這樣的:
Years years = Years.yearsBetween(
DateTime.parse("1975-05-26"), DateTime.now());
System.out.printf("你今年的歲數為:%d%n", years.getYears());
接下來這個片段要表示的日期不正確:
Date date = new Date(13, 8, 2);
DateFormat dateFormat = DateFormat.getDateInstance();
System.out.printf("Taiwan Java Developer Day is %s.%n",
dateFormat.format(date));
這個也不對:
Calendar calendar = Calendar.getInstance();
calendar.set(2013, 8, 2);
DateFormat dateFormat = DateFormat.getDateInstance();
System.out.printf("Taiwan Java Developer Day is %s.%n",
dateFormat.format(calendar.getTime()));
其實,這兩個程式碼想要的結果,並不需要完整的時間概念,它沒有要時、分、秒的資訊,因而只需要使用 Joda-Time 中的 Partial
概念,也就是 ReadablePartial
的實例之一 LocalDate
:
LocalDate javaTwoDate = new LocalDate(2013, 8, 2);
System.out.printf("Taiwan Java Developer Day is %s.%n", javaTwoDate);
那這個方法呢?
public static long daysBetween(Calendar begin, Calendar end) {
long daysBetween = 0;
while(begin.before(end)) {
begin.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}
其實你需要的只是 Joda-Time 中 Days.daysBetween
之類的方法:
再來看個需求好了,如果要知道某個日期起加上 5 天、6 個月、3 週後會的日期時間是什麼,並使用指定的格式輸出。使用 JDK 的話,會需要如下的計算:
Calendar calendar = Calendar.getInstance();
calendar.set(1975, Calendar.MAY, 26, 0, 0, 0);
calendar.add(Calendar.DAY_OF_MONTH, 5);
calendar.add(Calendar.MONTH, 6);
calendar.add(Calendar.WEEK_OF_MONTH, 3);
SimpleDateFormat df = new SimpleDateFormat("E MM/dd/yyyy");
System.out.println(df.format(calendar.getTime()));
Joda-Time 實現了 流暢 API 的概念,寫來會輕鬆且流暢易讀:
LocalDate birthDate = new LocalDate(1975, 5, 26);
System.out.println(birthDate
.plusDays(5)
.plusMonths(6)
.plusWeeks(3).toString("E MM/dd/yyyy"));
那麼,既然 Joda-Time 這麼好,為什麼還要有 JSR310,幹嘛不把 Joda-Time 納入 JDK 就好了呢?JSR310 的設計上又有什麼不同呢?這會是下篇文章要探討的主題!