【Joda-Time 與 JSR310 】(3)使用 Joda-Time


Joda-Time 是由 Stephen Colebourne 於 2002 年開始建立,版本 1.0 於 2005 年釋出,而 2007 年釋出版本 2.0,撰寫此文件的時候,最新版本為 2.3。

有鑑於 DateCalendar 的問題,Joda-Time 抽取了時間處理時幾個重要觀念,作為實作與使用 API 時的重要考量 …

Instant

連續時間軸上的某個瞬間,採用 UTC 1970 年 1 月 1 日 00:00:00 至該瞬間歷經的毫秒數(與 Unix 時間相同)…

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

在實作上,使用 ReadableInstant 介面來定義 Instant 的行為,其實作類別包括有:

  • Instant:簡單的實作類別,常用作時區、日歷轉換時的中性資料。
  • DateTime:最常用的類別,常搭配時區、日歷資訊來建立或取得日期、時間等欄位資訊。
  • MutableDateTime:前三個類別的實例都是不可變(Immutable),MutableDateTime 的實例為可變。

Joda-Time 記取了 DateCalendar 實例為可變時可能發生的問題,因而 InstantDateTime 都設計為不可變。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 的行為,其實作包括有以下類別,實例皆為不可變,從名稱上應可一目瞭然各自的作用:

  • LocalDateLocalTimeLocalDateTime
  • YearMonthMonthDay
  • Partial
  • YearMonthDay
  • TimeOfDay

既然是片段的時間資訊,那麼只要補齊不全的部份,就可以轉為時間軸上確切的某個瞬間…

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

具體來說,也就是 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

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

實作上由 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,就會得到另一個瞬間:

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

具體來說,可以給予 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
  • YearsMonthsWeeksDays
  • HoursMinutesSeconds

自然地,如果在某個瞬間加上 Period,也會得到另一個瞬間:

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

只不過這次是人類常用的時間概念,例如,想知道兩個時間軸上的瞬間到底是多少天嗎?可以這麼撰寫程式來得知:

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 之類的方法:

【Joda-Time 與 JSR310 】(3)使用 Joda-Time

再來看個需求好了,如果要知道某個日期起加上 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 的設計上又有什麼不同呢?這會是下篇文章要探討的主題!