5.21.2. 日付操作(Joda Time)


5.21.2.1. Overview

java.util.Datejava.util.Calender クラスのAPIは、非常に貧弱であるため、複雑な日付計算ができない。
本ガイドラインでは、日付計算が強力なJoda Timeの使用を推奨している。
Joda Timeでは、 java.util.Date の代わりに、 org.joda.time.DateTime オブジェクトを用いて日付を表現する。
なお、 org.joda.time.DateTime オブジェクトは、immutableである(日付計算等の結果は、新規オブジェクトである)。

5.21.2.2. How to use

Joda Time, Joda Time JSP tags の利用方法を、以下で説明する。

5.21.2.2.1. 日付取得

5.21.2.2.1.1. 現在時刻を取得

利用用途に併せて、 org.joda.time.DateTime , org.joda.time.LocalDate , org.joda.time.LocalTime を使い分けること。以下に、使用方法を示す。
  1. ミリ秒まで取得したい場合は、 org.joda.time.DateTime を使用する。
DateTime dateTime = new DateTime();
  1. TimeZoneと、時間を除いた日付だけが必要な場合は、 org.joda.time.LocalDate を使用する。
LocalDate localDate = new LocalDate();
  1. TimeZoneと、日付を除いた時間だけが必要な場合は、 org.joda.time.LocalTime を使用する。
LocalTime localTime = new LocalTime();
  1. 日付開始時刻と現在日付を取得したい場合は、 org.joda.time.DateTime.withTimeAtStartOfDay() を使用する。
DateTime dateTimeAtStartOfDay = new DateTime().withTimeAtStartOfDay();

Note

LocalDateとLocalTimeは、TimeZone情報を持たない。

Note

実際ServiceやControllerで現在時刻を取得するときのDateTime, LocalDate や、 LocalTimeのインスタンス取得には、 org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactoryを利用することを推奨する。

DateTime dateTime = dataFactory.newDateTime();

DateFactoryの利用方法は、 システム時刻 を参照されたい。

LocalDateやLocalTimeの生成は

LocalDate localDate = dataFactory.newDateTime().toLocalDate();
LocalTime localTime = dataFactory.newDateTime().toLocalTime();

とすればよい。


5.21.2.2.1.2. タイムゾーンを指定して現在時刻を取得

org.joda.time.DateTimeZoneは、timezoneを表すクラスである。
Timezoneを指定して取得したい場合に使用する。以下に、使用方法を示す。
DateTime dateTime = new DateTime(DateTimeZone.forID("Asia/Tokyo"));

org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactoryを利用する場合は、以下のようになる。

// Fetching current system date using default TimeZone
DateTime dateTime = dataFactory.newDateTime();

// Changing to TimeZone of Tokyo
DateTime dateTimeTokyo = dateTime.withZone(DateTimeZone.forID("Asia/Tokyo"));

他の使用可能なTimezone ID文字列の一覧は、 Available Time Zones を参照されたい。


5.21.2.2.1.3. タイムゾーンを指定せず現在時刻を取得

タイムゾーンを指定せず現在時刻を取得したい場合に使用する。以下に、使用方法を示す。
LocalDateTime localDateTime = new LocalDateTime();

org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactoryを利用する場合は、以下のようになる。

// Fetching current system date using default TimeZone
LocalDateTime localDateTime = dateFactory.newDateTime().toLocalDateTime();

Note

TimeZoneを意識する必要がない場合は、DateTimeではなくLocalDateTimeを利用することを推奨する。


5.21.2.2.1.4. 年月日時分秒を指定して取得

コンストラクタで、特定の時間を指定することができる。以下に例を示す。

  • ミリ秒まで指定して、DateTimeを取得したい場合
DateTime dateTime = new DateTime(year, month, day, hour, minite, second, millisecond);
  • 年月日を指定して、LocalDateを取得したい場合
LocalDate localDate = new LocalDate(year, month, day);
  • 時分秒を指定して、LocalDate取得したい場合
LocalTime localTime = new LocalTime(hour, minutes, seconds, milliseconds);

5.21.2.2.1.5. 年月日等の個別取得

DateTimeでは、年、月などを取得するメソッドを用意している。以下に、利用例を示す。
DateTime dateTime = new DateTime(2013, 1, 10, 2, 30, 22, 123);

int year = dateTime.getYear();  // (1)
int month = dateTime.getMonthOfYear();  // (2)
int day = dateTime.getDayOfMonth();  // (3)
int week = dateTime.getDayOfWeek();  // (4)
int hour = dateTime.getHourOfDay();  // (5)
int min = dateTime.getMinuteOfHour();  // (6)
int sec = dateTime.getSecondOfMinute();  // (7)
int millis = dateTime.getMillisOfSecond();  // (8)
項番 説明
(1)
年を取得する。本例では、2013が返却される。
(2)
月を取得する。本例では、1が返却される。
(3)
日を取得する。本例では、10が返却される。
(4)
曜日を取得する。本例では、4が返却される。
返却される値と曜日の対応は、[1:月曜、2:火曜、3:水曜、4:木曜、5:金曜、6:土曜、7:日曜]となる。
(5)
時を取得する。本例では、2が返却される。
(6)
分を取得する。本例では、30が返却される。
(7)
秒を取得する。本例では、22が返却される。
(8)
ミリ秒を取得する。本例では、123が返却される。

Note

java.util.Calendar の仕様とは異なり、getDayOfMonth()は、1始まりである。


5.21.2.2.2. 型変換

5.21.2.2.2.1. java.util.Dateとの相互運用性

DateTimeでは、 java.util.Date との型変換を、容易に行える。
Date date = new Date();

DateTime dateTime = new DateTime(date);  // (1)

Date convertDate = dateTime.toDate();  // (2)
項番 説明
(1)
DateTimeのコンストラクタの引数に、 java.util.Date を引数に渡すことで、 java.util.Date -> DateTime への変換を行う。
(2)
DateTime#toDate メソッドで、DateTime -> java.util.Date への変換を行う。

5.21.2.2.2.2. 文字列へのフォーマット

DateTime dateTime = new DateTime();

dateTime.toString("yyyy-MM-dd HH:mm:ss");  // (1)
項番 説明
(1)
“yyyy-MM-dd HH:mm:ss” 形式で変換された、文字列が取得される。
toStringの引数として指定可能な値については、 Input and Output を参照されたい。

5.21.2.2.2.3. 文字列からのパース

DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd").parseDateTime("2012-08-09");  // (1)
項番 説明
(1)
“yyyy-MM-dd” 形式の文字列を、DateTime型に変換する。
DateTimeFormat#forPatternの引数として指定可能な値は、 Formatters を参照されたい。

5.21.2.2.3. 日付操作

5.21.2.2.3.1. 日付の計算

DateTimeには、日付の加減算を行うメソッドが用意されている。以下に、利用例を示す。
DateTime dateTime = new DateTime(); // dateTime is 2013-01-10T13:30:22.123Z
DateTime yesterday = dateTime.minusDays(1);  // (1)
DateTime tomorrow = dateTime.plusDays(1);  // (2)
DateTime afterThreeMonth = dateTime.plusMonths(3);  // (3)
DateTime nextYear = dateTime.plusYears(1);  // (4)
項番 説明
(1)
DateTime#minusDays 引数に、指定した値分の日付が減算される。本例では2013-01-09T13:30:22.123Zとなる。
(2)
DateTime#plusDays 引数に、指定した値分の日付が加算される。本例では2013-01-11T13:30:22.123Zとなる。
(3)
DateTime#plusMonths 引数に、指定した値分の月数が加算される。本例では2013-04-10T13:30:22.123Zとなる。
(4)
DateTime#plusYears 引数に、指定した値分の年数が加算される。本例では2014-01-10T13:30:22.123Zとなる。

上記で示したメソッド以外は、 DateTime JavaDoc を参照されたい。


5.21.2.2.3.2. 月末月初の取得

現在日時を基準日とした、月末日と月初日の取得方法を、以下に示す。
下記の例では、時・分・秒・ミリ秒は、new DateTime()で取得した値のままとなる。
DateTime dateTime = new DateTime(); // dateTime is 2013-01-10T13:30:22.123Z
Property dayOfMonth = dateTime.dayOfMonth();  // (1)
DateTime firstDayOfMonth = dayOfMonth.withMinimumValue();  // (2)
DateTime lastDayOfMonth = dayOfMonth.withMaximumValue();  // (3)
項番 説明
(1)
現在月の日付に関する属性値を保持するPropertyオブジェクトを取得する。
(2)
Propertyオブジェクトから最小値を取得する事で、月初日を取得する事ができる。本例では2013-01-01T13:30:22.123Zとなる。
(3)
Propertyオブジェクトから最大値を取得する事で、月末日を取得する事ができる。本例では2013-01-31T13:30:22.123Zとなる。

5.21.2.2.3.3. 週末週初の取得

現在日時を基準日とした、週末日と週初日の取得方法を、以下に示す。
下記の例では、時・分・秒・ミリ秒は、new DateTime()で取得した値のままとなる。
DateTime dateTime = new DateTime(); // dateTime is 2013-01-10T13:30:22.123Z
Property dayOfWeek = dateTime.dayOfWeek();  // (1)
DateTime firstDayOfWeek = dayOfWeek.withMinimumValue();  // (2)
DateTime lastDayOfWeek = dayOfWeek.withMaximumValue();  // (3)
項番 説明
(1)
現在週の日付に関する属性値を保持するPropertyオブジェクトを取得する。
(2)
Propertyオブジェクトから最小値を取得する事で、週初日(月曜日)を取得する事ができる。本例では2013-01-07T13:30:22.123Zとなる。
(3)
Propertyオブジェクトから最大値を取得する事で、週末日(日曜日)を取得する事ができる。本例では2013-01-13T13:30:22.123Zとなる。

5.21.2.2.3.4. 日時の比較

日時を比較して過去か未来を判定できる。

DateTime dt1 = new DateTime();
DateTime dt2 = dt1.plusHours(1);
DateTime dt3 = dt1.minusHours(1);


System.out.println(dt1.isAfter(dt1)); // false
System.out.println(dt1.isAfter(dt2)); // false
System.out.println(dt1.isAfter(dt3)); // true

System.out.println(dt1.isBefore(dt1)); // false
System.out.println(dt1.isBefore(dt2)); // true
System.out.println(dt1.isBefore(dt3)); // false

System.out.println(dt1.isEqual(dt1)); // true
System.out.println(dt1.isEqual(dt2)); // false
System.out.println(dt1.isEqual(dt3)); // false
項番 説明
(1)
isAfterメソッドは対象の日時が引数の日時より未来の場合にtrueを返す。
(2)
isBeforeメソッドは対象の日時が引数の日時より過去の場合にtrueを返す。
(3)
isEqualメソッドは対象の日時が引数の日時と同じ場合にtrueを返す。

5.21.2.2.4. 期間の取得

Joda-Timeでは、期間に関して、いくつかのクラスが提供されている。ここでは以下の2クラスについて説明する。

  • org.joda.time.Interval
  • org.joda.time.Period

5.21.2.2.4.1. Interval

2つのインスタンス(DateTime)の期間を表すクラス。

Intervalで調べられることは、以下4つである。

  • 期間内に指定の日付や期間が含まれるかのチェック
  • 2つの期間が連続するかのチェック
  • 2つの期間の差を期間で取得
  • 2つの期間の重なった期間を取得

実装例は、以下を参照されたい。

DateTime start1 = new DateTime(2013,8,14,0,0,0);
DateTime end1 = new DateTime(2013,8,16,0,0,0);

DateTime start2 = new DateTime(2013,8,16,0,0,0);
DateTime end2 = new DateTime(2013,8,18,0,0,0);

DateTime anyDate = new DateTime(2013, 8, 15, 0, 0, 0);

Interval interval1 = new Interval(start1, end1);
Interval interval2 = new Interval(start2, end2);

interval1.contains(anyDate);  // (1)

interval1.abuts(interval2);  // (2)

DateTime start3 = new DateTime(2013,8,18,0,0,0);
DateTime end3 = new DateTime(2013,8,20,0,0,0);
Interval interval3 = new Interval(start3, end3);

interval1.gap(interval3);  // (3)

DateTime start4 = new DateTime(2013,8,15,0,0,0);
DateTime end4 = new DateTime(2013,8,17,0,0,0);
Interval interval4 = new Interval(start4, end4);

interval1.overlap(interval4);  // (4)
項番 説明
(1)
Interval#containsメソッドで、期間内に指定の日付や期間が含まれるかのチェックを行う。
期間内に含まれる場合、”true”、含まれない場合、”false”を返却する。
(2)
Interval#abutsメソッドで、2つの期間が連続するかのチェックを行う。
2つの期間が連続する場合は”true”、連続しない場合は”false”を返却する。
(3)
Interval#gapメソッドで、2つの期間の差を期間(Interval)で取得する。
本例では、”2013-08-16~2013-08-18” の期間が取得される。
期間の差が存在しない場合、nullが戻り値となる。
(4)
Interval#overlapメソッドで、2つの期間の重なった期間(Interval)を取得する。
本例では、”2013-08-15~2013-08-16” の期間が取得される。
重なった期間が存在しない場合、nullが戻り値となる。

Interval同士を比較したい場合は、Periodに変換して行う。

  • 月、日、などより抽象的な観点で比較をしたい場合は、Periodに変換すること。
// Convert to Period
interval1.toPeriod();


5.21.2.2.4.2. Period

Periodは、期間を、年、月、週などの単位で表すクラスである。

たとえば、「3月1日」を表すInstant(DateTime)に「1ヶ月」に相当するPeriodを追加した場合、DateTimeは「4月1日」になる。
「3月1日」と「4月1日」に対して、「1か月」に相当するPeriodを追加した時の結果を以下に示す。
  • 「3月1日」に「1ヶ月」というPeriodを追加したときの日数は「31日」
  • 「4月1日」に「1ヶ月」というPeriodを追加したときの日数は「30日」

「1ヶ月」に相当するPeriodの追加は、対象のDateTimeによって、違う意味を持つ。

Periodは、さらに2種類の実装が用意されている。
  • Single field Period (例:「1日」や「1ヶ月」など一つの単位の値しか持たないタイプ)
  • Any field Period (例:「1ヶ月2日4時間」など、複数の単位の値を持てて期間を表すタイプ)

詳細は、 Period を参照されたい。


5.21.2.2.5. JSP Tag Library

JSTLの fmt:formatDate タグは、java.util.Dateと、java.util.TimeZoneオブジェクトを扱う。
Joda-timeのDateTime, LocalDateTime, LocalDate, LocalTimeと、DateTimeZoneオブジェクトを扱うためには、Jodaのタグライブラリを使う。
機能面でJSTLとほぼ同じであるため、JSTLの知識がある場合は、JodaのJSPタグライブラリを容易に使える。

5.21.2.2.5.1. 設定方法

タブライブラリを利用するには、以下のtaglib定義が必要である。

<%@ taglib uri="http://www.joda.org/joda/time/tags" prefix="joda"%>

5.21.2.2.5.2. joda:format タグ

joda:format タグとは、DateTime, LocalDateTime, LocalDate, LocalTimeオブジェクトをフォーマットするタグである。

<% pageContext.setAttribute("now", new org.joda.time.DateTime()); %>

<span>Using pattern="yyyyMMdd" to format the current system date</span><br/>
<joda:format value="${now}" pattern="yyyyMMdd" />
<br/>
<span>Using style="SM" to format the current system date</span><br/>
<joda:format value="${now}" style="SM" />

出力結果

/jodatime

joda:formatタグの属性一覧は、以下の通りである。

属性情報
No. Attributes Description
value
ReadableInstantかReadablePartialのインスタンスを設定する。
var
時刻情報を持つ変数名
scope
時刻情報を持つ変数名のスコープ
locale
ロケール情報
style
フォーマットするためのスタイル情報(2桁。日付部分と時刻部分それぞれのスタイルを設定する。入力可能な値は S=Short, M=Medium, L=Long, F=Full, -=None)
pattern
フォーマットするためのパターン(yyyyMMddなど)。入力可能なパターンは、 Input and Output を参照されたい。
dateTimeZone
タイムゾーン

Joda-Timeのほかのタグは、 Joda Time JSP tags User guide を参照されたい。

Note

style属性を指定して日付と時刻部分を表示する場合、ブラウザのlocaleによって表示内容が異なる。 上記style属性で表示した形式のlocaleは”en”である。


5.21.2.2.6. 応用例(カレンダーの表示)

Spring MVCを使って、月単位のカレンダーを表示するサンプルを示す。

処理名 URL 処理メソッド
今月のカレンダー表示 /calendar today
指定月のカレンダー表示 /calendar/month?year=yyyy&month=m month

コントローラの実装は、以下のようになる。

@Controller
@RequestMapping("calendar")
public class CalendarController {

    @RequestMapping
    public String today(Model model) {
        DateTime today = new DateTime();
        int year = today.getYear();
        int month = today.getMonthOfYear();
        return month(year, month, model);
    }

    @RequestMapping(value = "month")
    public String month(@RequestParam("year") int year,
            @RequestParam("month") int month, Model model) {
        DateTime firstDayOfMonth = new DateTime(year, month, 1, 0, 0);
        DateTime lastDayOfMonth = firstDayOfMonth.dayOfMonth()
                .withMaximumValue();

        DateTime firstDayOfCalender = firstDayOfMonth.dayOfWeek()
                .withMinimumValue();
        DateTime lastDayOfCalender = lastDayOfMonth.dayOfWeek()
                .withMaximumValue();

        List<List<DateTime>> calendar = new ArrayList<List<DateTime>>();
        List<DateTime> weekList = null;
        for (int i = 0; i < 100; i++) {
            DateTime d = firstDayOfCalender.plusDays(i);
            if (d.isAfter(lastDayOfCalender)) {
                break;
            }

            if (weekList == null) {
                weekList = new ArrayList<DateTime>();
                calendar.add(weekList);
            }

            if (d.isBefore(firstDayOfMonth) || d.isAfter(lastDayOfMonth)) {
                // skip if the day is not in this month
                weekList.add(null);
            } else {
                weekList.add(d);
            }

            int week = d.getDayOfWeek();
            if (week == DateTimeConstants.SUNDAY) {
                weekList = null;
            }
        }

        DateTime nextMonth = firstDayOfMonth.plusMonths(1);
        DateTime prevMonth = firstDayOfMonth.minusMonths(1);
        CalendarOutput output = new CalendarOutput();
        output.setCalendar(calendar);
        output.setFirstDayOfMonth(firstDayOfMonth);
        output.setYearOfNextMonth(nextMonth.getYear());
        output.setMonthOfNextMonth(nextMonth.getMonthOfYear());
        output.setYearOfPrevMonth(prevMonth.getYear());
        output.setMonthOfPrevMonth(prevMonth.getMonthOfYear());

        model.addAttribute("output", output);

        return "calendar";
    }
}

以下の CalendarOutput クラスは、画面に出力する情報をまとめたJavaBeanである。

public class CalendarOutput {
    private List<List<DateTime>> calendar;

    private DateTime firstDayOfMonth;

    private int yearOfNextMonth;

    private int monthOfNextMonth;

    private int yearOfPrevMonth;

    private int monthOfPrevMonth;

    // omitted getter/setter
}

Warning

このサンプルコードは単純なためControllerの処理メソッドに全ての処理を記述しているが、 メンテナンス性向上のため本来この処理は、Helperクラスに記述すべきである。


JSP(calendar.jsp)で、次のように出力する。

<p>
    <a
        href="${pageContext.request.contextPath}/calendar/month?year=${f:h(output.yearOfPrevMonth)}&month=${f:h(output.monthOfPrevMonth)}">&larr;
        Prev</a> <a
        href="${pageContext.request.contextPath}/calendar/month?year=${f:h(output.yearOfNextMonth)}&month=${f:h(output.monthOfNextMonth)}">Next
        &rarr;</a> <br>
    <joda:format value="${output.firstDayOfMonth}"
        pattern="yyyy-M" />
</p>
<table>
    <tr>
        <th>Mon.</th>
        <th>Tue.</th>
        <th>Wed.</th>
        <th>Thu.</th>
        <th>Fri.</th>
        <th>Sat.</th>
        <th>Sun.</th>
    </tr>
    <c:forEach var="week" items="${output.calendar}">
        <tr>
            <c:forEach var="day" items="${week}">
                <td><c:choose>
                        <c:when test="${day != null}">
                            <joda:format value="${day}"
                                pattern="d" />
                        </c:when>
                        <c:otherwise>&nbsp;</c:otherwise>
                    </c:choose></td>
            </c:forEach>
        </tr>
    </c:forEach>
</table>

{contextPath}/calendarにアクセスすると、以下のカレンダーが表示される(2012年11月時点での結果である)。

/calendar

{contextPath}/calendar/month?year=2012&month=12にアクセスすると、以下のカレンダーが表示される。

/calendar/month?year=2012&month=12