Joda-Timeを使ってみる

概要

Javaのイケてない日付関連の機能に頭を抱えた時、手を出したくなるライブラリ、Joda-Time。

Joda Time
http://joda-time.sourceforge.net/

java.util.Calendarなんかよりも表現力豊かで多機能。そして何よりマルチスレッド下で走らせるとあっさり壊れるSimpleDateFormatとは違いimmutablethread-safeな作りになっている安心の一品。

JSR310が仕事で使えるようになるのは2〜3年後とかいう割と先の話になりそうなので、今日は諦めてJoda-Timeの使い方を勉強してみた。

@Author mwSoft
@Date 2011/12/23
@Env Joda-Time2.0

DateTimeの初期化

日時はDateTimeというクラスで扱える。

import org.joda.time.DateTime;

// 引数なしの場合は現在日時で初期化される
DateTime dt = new DateTime();

// java.util.Dateやjava.util.Calendarを引数にとって初期化することも可能
Date date = new Date();
DateTime dt = new DateTime(date);

// 年月日時分秒を指定して初期化(バージョン2.0で追加)
DateTime dt = new DateTime(2011, 10, 23, 3, 50, 0);

// タイムゾーンを指定して初期化
DateTime dt = new DateTime(DateTimeZone.forID("Asia/Tokyo"));

こんな感じでいろいろ方法で初期化可能。詳しくはJavaDoc参照。

DateTime (Joda time 2.0 API)
http://joda-time.sourceforge.net/api-release/org/joda/time/DateTime.html

普通のDateTime以外にも、Timeが00:00:00に固定されるDateMidnightとか、Localeと時間の情報を持たないLocalDateとか、Localeと日の情報を持たないLocalTimeとか、いろんな日時を扱うクラスがいる。

この辺りの機能は基本immutableだけど、MutableDateTimeというクラスもいる。

toStringで文字列整形

DateTimeクラスのtoStringメソッドは日付を整形して文字列を出力する。パターンを渡すことも可能。

DateTime dt = new DateTime();

// 引数なしのtoString()はこんな感じの形式
dt.toString()
  //=> 2011-12-21T22:27:35.776+09:00

// toStringにパターンを渡すことができる
dt.toString("yyyy/MM/dd")
  //=> 2011/12/21

// パターンと一緒にLocaleも渡せる
dt.toString("yyyy-MM-dd E", Locale.US)
  //=> 2011-12-21 Wed

ソースを見た限りでは利用したパターンはキャッシュされるので、毎回インスタンスが生成されるということはなさそう。

toStringの引数にDateTimeFormatというクラスを使うこともできる。

import org.joda.time.format.DateTimeFormat;

// ロケールがja_JPの場合、DateTimeFormatのfullDateを渡すと年月日で出力される
dt.toString(DateTimeFormat.fullDate())
  //=> 2011年12月21日

// fullDateTimeを使うと時分秒が加わってJSTも付く
dt.toString(DateTimeFormat.fullDateTime())
  //=> 2011年12月21日 23時01分29秒 JST

// longDateTimeだとスラッシュとコロンで区切ってJST付き
dt.toString(DateTimeFormat.longDateTime())
  //=> 2011/12/21 23:03:59 JST

// mediumDateTimeだとスラッシュとコロンで区切ってJSTなし
dt.toString(DateTimeFormat.mediumDateTime())
  //=> 2011/12/21 23:03:00

// shortDateTimeだとyearが2桁になって秒省略
dt.toString(DateTimeFormat.shortDateTime())
  //=> 11/12/21 23:06

この辺の処理はLocaleによって結果が変わるので使う時には注意が必要。

ISODateTimeFormatを使ってISO8601に沿った出力をすることも可能。

import org.joda.time.format.ISODateTimeFormat

// basicDateはyyyyMMdd
dt.toString(ISODateTimeFormat.basicDate())
  //=> 20111221

// basicDateTimeはミリ秒とZZまで表示
dt.toString(ISODateTimeFormat.basicDateTime())
  //=> 20111221T231630.979+0900

// basicDateTimeNoMillisはミリ秒なし
ISODateTimeFormat.basicDateTimeNoMillis())
  //=> 20111221T161738+0900

// dateはyyyy-MM-dd
dt.toString(ISODateTimeFormat.date())
  //=> 2011-12-21

// dateHourMinuteSecondはyyyy-MM-dd'T'HH:mm:ss
dt.toString(ISODateTimeFormat.dateHourMinuteSecond())
  //=> 2011-12-21T16:21:55

// yearMonthはyyyy-MM
dt.toString(ISODateTimeFormat.yearMonth())
  //=> 2011-12
年月日とかを取得する場合

getYearとかgetMontOfYearで年や月が取れる。

DateTime dt = new DateTime();

// 年
dt.getYear()
  //=> 2011

// 年(2桁)
dt.getYearOfCentury()
  //=> 11

// 月(ちゃんと当月が返ってくる)
dt.getMonthOfYear()
  //=> 12

// 日
dt.getDayOfMonth()
  //=> 21

// 時
dt.getHourOfDay()
  //=> 22

// 分
dt.getMinuteOfHour()
  //=> 51

// 秒
dt.getSecondOfMinute()
  //=> 30

// 曜日(月曜=1, 火曜=2, 水曜=3 ・・・ 日曜=7)
dt.getDayOfWeek()
  //=> 3

// 1970/1/1からのミリ秒
dt.getMillis()
  //=> 1324543605407

getDayOfMonthとかgetSecondOfMinuteとか表記が長めになっているのは、dayOfYear(年単位での日数)とか、secondOfDay(日単位での秒数)とか、いろいろ取得できるようになっているため。

withで日時を再設定

withYearとかwithDayOfMonthとかを使うと、指定した年や月が反映された新しいDateTimeインスタンスが生成される。

DateTime dt = new DateTime()

// 年だけ2005にして他はdtが持つ時間
dt.withYear(2005)
  //=> 2005-12-21T23:23:33.554+09:00

// 月だけ10月にして他はdtが持つ時間
dt.withMonthOfYear(10)
  //=> 2011-10-21T23:23:33.554+09:00

// withDateで年月日をまとめて設定
dt.withDate(2000, 3, 10)
  //=> 2000-03-10T23:23:33.554+09:00

// withTimeで時分秒ミリ秒をまとめて設定
dt.withTime(5, 10, 20, 0)
  //=> 2011-12-21T05:10:20.000+09:00

破壊的ではないので、元の値はちゃんと残っている。

最近のライブラリはこういうwith的なメソッドを用意してくれてることが多くなった気がする。

全然関係ない話だけど、私はウィズという言葉を聞くとダンジョンに行きたくなる。

年月日の加算減算

DateTimeにはplusYearとかminuDayOfMinutesなどの日時を加算、減算するメソッドが存在する。

// plusDaysで10日間足してみる
dt.plusDays(10).toString("yyyy/MM/dd HH:mm")
  //=> 2011/12/31 23:19

// plusHoursで10時間足してみる
dt.plusHours(10).toString("yyyy/MM/dd HH:mm")
  //=> 2011/12/22 09:19

// plusHoursに負の数を入れて10時間引いてみる
dt.plusHours(-10).toString("yyyy/MM/dd HH:mm")
  //=> 2011/12/31 13:19

// minusHoursでも同じことができる
dt.minusHours(10).toString("yyyy/MM/dd HH:mm")
  //=> 2011/12/31 13:19

// addWeekで来週を取れたりもする
dt.plusWeeks(1)
  //=> 2011/12/29 13:19

// 1月31日の1ヶ月後は2月28日になるらしい
DateTime jan31 = new DateTime(2011, 01, 31, 0, 0);
jan31.plusMonths(1)
  //=> 2011-02-28T00:00:00.000+09:00

// minusYearsで1000年前に戻ってみる
dt.minusYears(1000).toString("yyyy/MM/dd HH:mm")
  //=> 1011/12/21 13:19

// minusYearsで紀元前に行ってみる
dt.minusYears(3000).toString("yyyy/MM/dd HH:mm")
  //=> -0989/12/21 14:24

// 前月の1日とかはwithと組み合わせてこんな感じで取ってもいいのかな
dt.minusMonths(1).withDayOfMonth(1)
  //=> 2011-11-01T23:52:57.917+09:00

これを使えば指定日から1週間分の日付について処理するとかも楽に書けそう。

日付文字列のパース

日付のパースは整形のところでも紹介したDateTimeFormatを使うっぽい。

forPatternでパターン文字列を指定して、あとはparseDateTimeの引数に整形する日付文字列を渡す。

import org.joda.time.format.DateTimeFormat;

// yyyy/MM/ddでparse
DateTimeFormat.forPattern("yyyy/MM/dd").parseDateTime("2011/10/31")

// yyyy/MM/dd HH:mm:ssでparse
DateTimeFormat.forPattern("yyyy/MM/dd HH:mm:ss").parseDateTime("2011/10/31 12:25:31")

toStringの時に使ったfullDateとかmediumDateTimeとかも利用可能。

// mediumDate(ロケールがja_JPの場合は、yyyy/MM/dd)
DateTimeFormat.mediumDate().parseDateTime("2011/10/31")

// yyyy/MM/dd HH:mmのパースはforStyleを使ってこんな風に書けたり
DateTimeFormat.forStyle("MS").parseDateTime("2011/10/31 11:51")

2つ目の例のforStyleは日付と時刻のパターンを個別に指定するメソッドで、MSなら日付はmediumで時刻はshort、FLなら日付がfullで時刻がlongという意味になる。ハイフンはなしを意味して、-Mと指定すると日付なしで時刻はmediumになる。

このクラスもthread-safeimmutableなので一度作ったFormatterを安心して使い回せる。

2つの日時を比較する

2つの日時を比較して過去か未来を判定する、isAfterisBeforeというメソッドもある。

DateTime dt1 = new DateTime().withHourOfDay(3);
  //=> 2011-12-21T03:47:48
DateTime dt2 = new DateTime().withHourOfDay(5);
  //=> 2011-12-21T05:47:48

// dt1(3時)はdt2(5時)より未来か?
dt1.isAfter(dt2)
  //=> false

// dt1(3時)はdt2(5時)より過去か?
dt1.isBefore(dt2)
  //=> true

同じ日時かどうかを判定するisEqualというメソッドもある。

// dt1(3時)とdt2(5時)は同じ時刻か?
dt1.isEqual(dt2)
  //=> false

// LocalDateかDateMidnightに変換すれば同日かどうか判定できたり
dt1.toDateMidnight().isEqual(dt2.toDateMidnight())
  //=> true

現在時刻と比較するisAfterNowisBeforeNowもある。

// isAfterNowで現在より未来かが判定できる
dt1.isAfterNow()
  //=> false

// isBeforeNowもある
dt1.isBeforeNow()
  //=> true
Durationで差を取る

Durationを使うと、2つのDateTimeの差が何日か、何時間か、といったことがすぐ分かる。

import org.joda.time.Duration;

// 2010/12/21 10:00:00
DateTime dt1 = DateTimeFormat.forStyle("MM").parseDateTime("2011/12/21 10:00:00");
// 2011/08/13 15:32:51
DateTime dt2 = DateTimeFormat.forStyle("MM").parseDateTime("2011/08/13 15:32:51");

// 上の2つのDateTimeのDuration
Duration d = new Duration(dt1, dt2);

// 日の差分
d.getStandardDays()
  //=> -129

// 時の差分
d.getStandardHours()
  //=> -3114

// 分の差分
d.getStandardMinutes()
  //=> -186867

// 秒の差分
d.getStandardSeconds()
  //=> -11212029

getStandardDaysとかはバージョン2.0から追加されたメソッド。

日時のfloorとかceil

DateTime.Propertyの機能を使えば、日付を指定の箇所で切り捨てたり切り上げたりできる。

DateTime dt = new DateTime();

// 年で切り捨ててみる
dt.year().roundFloorCopy()
  //=> 2011/01/01 00:00:00

// 年で切り上げてみる
dt.year().roundCeilingCopy()
  //=> 2012/01/01 00:00:00

// 四捨五入っぽいのもある(今年の場合は7/2の11:59:59までは切り捨てだった)
new DateTime(2011, 7, 2, 0, 0).year().roundHalfEvenCopy()
  //=> 2011/01/01 00:00:00
new DateTime(2011, 7, 3, 0, 0).year().roundHalfEvenCopy()
  //=> 2012/01/01 00:00:00

四捨五入っぽいヤツはソースを見たらgetMillisとかしていたので、おそらくミリ秒単位での真ん中(365日だから182日目の12時)で切っているのだと思われる。

月や日、時分秒に対しても同じ事ができる

// 月
dt.monthOfYear().roundCeilingCopy()

// 日
dt.dayOfMonth().roundFloorCopy()

// 時
dt.hourOfDay().roundHalfEvenCopy()

// 分
dt.minuteOfHour().roundCeilingCopy()

// 秒
dt.secondOfMinute().roundHalfCeilingCopy()
おまけ
// うるう年?
dt.year().isLeap()
  //=> false

// その月の末日
DateTime lastDay = dt.dayOfMonth().withMaximumValue()
  //=> 2011-12-31T12:02:20.192+09:00

// 5桁の年とかも普通に扱えるので、1万年問題も大丈夫
DateTimeFormat.forStyle("MS").parseDateTime("12011/12/22 23:39")
  //=> 12011-12-29T23:39:00.000+09:00

// じゃ、3000億年問題はどうだろうと思って調べたら、なぜか292278977年(約3億)で落ちた
DateTime dt = DateTimeFormat.forStyle("MS").parseDateTime("292278977/02/01 00:00")
  //=> Cannot parse "292278977/02/01 00:00": Illegal instant due to time zone offset transition (Asia/Tokyo)
あとがき

というわけでJoda Timeの機能をDateTimeを中心に使ってみた。まだ全部の機能は追いきれてないけど、なかなかに使い心地は良い。

ただクラスの数がやたら多いので、学び始める時に戸惑う人も多いような気がする。

最初はJavaのCalendarに対応するのがDateTime、JavaのDateFormatに対応するのがDateTimeFormatという感じで2つのクラスだけ覚えて、後は気が向いたら見てみるくらいのスタンスで使うのが良い感じだろうか。