7.4. 日付操作(Joda Time)


7.4.1. Overview

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

7.4.2. How to use

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

7.4.2.1. 日付取得

7.4.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();

とすればよい。


7.4.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 を参照されたい。


7.4.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を利用することを推奨する。


7.4.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);

7.4.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始まりである。


7.4.2.2. 型変換

7.4.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 への変換を行う。

7.4.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 を参照されたい。

Note

Java SE 11ではJava SE 8と日付の文字列表現が異なる場合がある。 Java SE 8と同様に表現するにはデフォルトで使用されるロケール・データの変更を参照されたい。


7.4.2.2.3. 文字列からのパース

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

7.4.2.3. 日付操作

7.4.2.3.1. 日付の計算

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

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


7.4.2.3.2. 月末月初の取得

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

7.4.2.3.3. 週末週初の取得

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

7.4.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を返す。

7.4.2.4. 期間の取得

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

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

7.4.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();


7.4.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 を参照されたい。


7.4.2.5. Thymeleafでのフォーマット

ThymeleafのテンプレートHTMLでも、「文字列へのフォーマット」と同様に toString メソッドを使用した文字列へのフォーマットが可能である。
ここでは、Joda TimeのオブジェクトをテンプレートHTML上で文字列へフォーマットする方法を説明する。

Joda Timeの DateTime オブジェクトをフォーマット文字列を指定してフォーマットする例を以下に示す。
  • Controllerクラス
DateTime dateTime = new DateTime();
model.addAttribute("currentDateTime", dateTime); // (1)
  • テンプレートHTML
<p th:text="|currentDateTime = ${currentDateTime.toString('yyyy/MM/dd HH:mm:ss')}.|"></p> <!--/* (2) */-->
  • 出力結果例(html)
<p>currentDate = 2013/10/25 13:02:32.</p> <!-- (3) -->
項番 説明
(1)
Model オブジェクトにJoda Timeの DateTime オブジェクトを追加する。
ここでは、現在日時を指定している。
(2)
DateTime オブジェクトを指定したフォーマット文字列でフォーマットする。
ここでは、フォーマット文字列を yyyy/MM/dd HH:mm:ss 形式で指定している。

ここでは簡易な例を示しているため実装していないが、必要に応じて toString メソッド実行前に null チェックを実装すること。
(3)
現在の日付が2013年10月25日13時2分32秒の場合、2013/10/25 13:02:32 と表示される。

Note

Java SE 11ではJava SE 8と日付の文字列表現が異なる場合がある。 Java SE 8と同様に表現するにはデフォルトで使用されるロケール・データの変更を参照されたい。


7.4.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) {
        LocalDate today = new LocalDate();
        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) {
        LocalDate firstDayOfMonth = new LocalDate(year, month, 1);
        LocalDate lastDayOfMonth = firstDayOfMonth.dayOfMonth()
                .withMaximumValue();

        LocalDate firstDayOfCalendar = firstDayOfMonth.dayOfWeek()
                .withMinimumValue();
        LocalDate lastDayOfCalendar = lastDayOfMonth.dayOfWeek()
                .withMaximumValue();

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

            if (weekList == null) {
                weekList = new ArrayList<LocalDate>();
                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;
            }
        }

        LocalDate nextMonth = firstDayOfMonth.plusMonths(1);
        LocalDate 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<LocalDate>> calendar;

    private LocalDate firstDayOfMonth;

    private int yearOfNextMonth;

    private int monthOfNextMonth;

    private int yearOfPrevMonth;

    private int monthOfPrevMonth;

    // omitted getter/setter
}

Warning

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


テンプレートHTML(calendar.html)で、次のように出力する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
  <div th:object="${output}">
    <p>
      <a href="calendar.html"
        th:href="@{/calendar/month(year=*{yearOfPrevMonth}, month=*{monthOfPrevMonth})}">&larr;Prev</a>
      <a href="calendar.html"
        th:href="@{/calendar/month(year=*{yearOfNextMonth}, month=*{monthOfNextMonth})}">Next&rarr;</a> <br>
      <span th:text="*{firstDayOfMonth.toString('yyyy-M')}"></span>
    </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>
      <tr th:each="week : *{calendar}">
        <td th:each="day : ${week}">
          <span th:text="${day != null}? ${day.toString('d')} : '&nbsp;'"></span>
        </td>
      </tr>
    </table>
  </div>
</body>
</head>
</html>

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

/calendar

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

/calendar/month?year=2018&month=1

7.4.3. Appendix

7.4.3.1. 和暦操作

JSR-310 Date and Time APIとは異なり、Joda Timeでは和暦を扱うクラスが提供されていない。
そのため、和暦を扱うにはjava.time.chrono.JapaneseDateもしくはjava.util.Calendarを使用する。
java.time.chrono.JapaneseDateについては、日付操作(JSR-310 Date and Time API)和暦(JapaneseDate)を参照されたい。
java.util.Calendarで和暦操作するには、java.util.Calendarクラス、java.text.DateFormatクラスに以下のjava.util.Localeを指定する必要がある。
Locale locale = new Locale("ja", "JP", "JP");
以下に、Calendarクラスを利用した和暦表示の例を示す。
Locale locale = new Locale("ja", "JP", "JP");
Calendar cal = Calendar.getInstance(locale); // Ex, 2015-06-05
String format1 = "Gy.MM.dd";
String format2 = "GGGGyy/MM/dd";

DateFormat df1 = new SimpleDateFormat(format1, locale);
DateFormat df2 = new SimpleDateFormat(format2, locale);

df1.format(cal.getTime()); // "H27.06.05"
df2.format(cal.getTime()); // "平成27/06/05"
また、同様に文字列からのパースも行うことが出来る。
Locale locale = new Locale("ja", "JP", "JP");
String format1 = "Gy.MM.dd";
String format2 = "GGGGyy/MM/dd";

DateFormat df1 = new SimpleDateFormat(format1, locale);
DateFormat df2 = new SimpleDateFormat(format2, locale);

Calendar cal1 = Calendar.getInstance(locale);
Calendar cal2 = Calendar.getInstance(locale);

cal1.setTime(df1.parse("H27.06.05"));
cal2.setTime(df2.parse("平成27/06/05"));

Note

new Locale("ja", "JP", "JP")getInstanceメソッドに指定することで、 和暦に対応したjava.util.JapaneseImperialCalendarオブジェクトが作成される。
その他を指定するとjava.util.GregorianCalendarオブジェクトが作成されるため、留意されたい。