JavaでISO 8601の計算を考える

はじめに

エキサイトで内定者インターンをしている岡崎です。 今日はISO 8601の日付の計算を考えたときにハマったので、その時に学んだことについてまとめていきます。

ISO 8601とは

ISO 8601とは、日付と時刻についてのISO(スイスのジェネーブに本部をおく非政府機関)の国際規格の一種です。 シンプルでわかりやすいため、APIなどで利用されることが多いです。

ISO 8601の年と週と曜日の数え方には以下のような特徴があります。

  • 週において、その週が1年の中の何番目かを指す
  • 1月の最初の木曜日を含む週をその年の第1週として定義
  • 第1週は01、年末の最終週は52、53と表記
  • 週の開始曜日は月曜で、終わりは日曜

実際のコード

日付から年、月、週、曜日を求める方法

実際にJavaで年の何週目か、を実装してみます。

LocalDate today = LocalDate.now();
Integer todayWeek = today.get(WeekFields.ISO.weekOfMonth());
System.out.println(todayWeek);

ちなみに、LocalDateでは週の方かに年、月、曜日も簡単に求められる関数が存在します。 実際のコードは以下です。

// 年を求める
Integer year = today.getYear();
// 月を求める
Integer month = today.getMonthValue();
// 曜日を求める
Integer yobi = today.getDayOfWeek().getValue();

年、週、曜日から日付を求める方法

さて。ここまでは日付から年、月、週(ISO 8601)、曜日を求める方法を示しました。 それでは、反対にISO 8601で年、週、曜日から日付を求めることを考えてみます。

デフォルトの関数で用意されていれば便利ですが、もちろん存在はしていません。 計算式を考えます。

私が考えたのは以下の方法です。

与えられた年の1月1日に、与えられた週から1を引いたものを足し、さらに与えられた曜日から1月1日の曜日の差の日を足す、というものでした。

言葉だとわかりづらいので、実装してみたものを示します。

public LocalDate getDate(Integer year, Integer week, Integer yobi) {
    return LocalDate.of(year, 1, 1)
    .plusWeeks(week - 1)
    .plusDays(yobi - LocalDate.of(year, 1, 1).getDayOfWeek().getValue());
}

ここでうまくいくと思っていたんですけど、計算してみると、実は年によっては正解で、年によっては合っていないことがありました。

ISO 8601 の注意点とその解決方法

なんでそんなことになるのかというと、上記の計算式では与えられた年の1月1日は必ず第1週として考えていたからでした。

しかし、これは間違いです。 なぜならISO 8601では、その年の第1週が、1月の最初の木曜日を含む週と定義されるからです。

実際、1月1日の曜日がどんな風になっているのか試してみました。 その実装は以下です。

class Main{
  public static void main(String args[]){

    List<Integer> years = Arrays.asList(2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023);

    for (Integer year : years) {
        Integer week = LocalDate.of(year, 1, 1)
                .get(WeekFields.ISO.weekOfYear());
        
        if (week.equals(0)) {
                System.out.println("weekが0から始まる年: " + year);
        } 

        if (week.equals(1)) {
           System.out.println("weekが1から始まる年: " + year);
        }
    }
  }
}

この結果を載せます。

weekが0から始まる年: 2011
weekが0から始まる年: 2012
weekが1から始まる年: 2013
weekが1から始まる年: 2014
weekが1から始まる年: 2015
weekが0から始まる年: 2016
weekが0から始まる年: 2017
weekが1から始まる年: 2018
weekが1から始まる年: 2019
weekが1から始まる年: 2020
weekが0から始まる年: 2021
weekが0から始まる年: 2022
weekが0から始まる年: 2023

つまり、これを考慮しなければ、正しい日付の計算ができないということですね。 実際にコードを修正してみます。

public LocalDate getDate(Integer year, Integer week, Integer yobi) {
    LocalDate startDate = LocalDate.of(year, 1, 1);

    return startDate
    .plusWeeks(startDate.get(WeekFields.ISO.weekOfYear()).equals(0)
        ? week : week - 1)
    .plusDays(yobi - LocalDate.of(year, 1, 1).getDayOfWeek().getValue());
}

これで正しく計算されるようになりました。

まとめ

ISO 8601で何週目かを表すときに学んだ話をまとめました。 日付の計算を考えることは大変なので、そもそもこのようなことを考えないような設計にしておくことが大切だと思いました。

ここまで読んでいただきありがとうございます。