まだ年齢計算で消耗してるの？


> [2月29日に女性からプロポーズされた男性は、断ってはいけない](http://magazine.gow.asia/love/column_details.php?column_uid=00001088)

サービスの申し込みで年齢制限を設けるなど、誕生日から年齢を算出したいことがあります。さてどういうロジックを書けばよいのでしょうか。

## 年齢とは何か(仕様の定義)

年齢計算は日本の場合は、「[年齢計算ニ関スル法律](https://ja.wikipedia.org/wiki/%E5%B9%B4%E9%BD%A2%E8%A8%88%E7%AE%97%E3%83%8B%E9%96%A2%E3%82%B9%E3%83%AB%E6%B3%95%E5%BE%8B)」に基づいた計算になります。

ここで、大きく別れるのが、時刻基準で計算するか日基準で計算するかです。
一般的には時刻基準で計算するので、4月1日生まれの人は、4月1日の0時ちょうどに歳をとることにすればよいですが…、日基準の場合には誕生日前日に歳をとることになります。日基準で計算される有名なのは[学齢](https://ja.wikipedia.org/wiki/%E5%AD%A6%E9%BD%A2)がありますが、選挙権なんかも同じく日基準のようです。

ここの仕様をどちらにするか、しっかり定義する必要があります。法によっても解釈の仕方が異なるのが現状のようです。

一番問題となる2月29日生まれの人が歳をとるのは、基準の違いによって以下のようになります。

|   |うるう年でない|うるう年である|
|:--|:--|:--|
|日基準|2月28日|2月28日|
|時刻基準|3月1日|2月29日|

3月1日の人は、

|   |うるう年でない|うるう年である|
|:--|:--|:--|
|日基準|2月28日|2月29日|
|時刻基準|3月1日|3月1日|

境界値としては、この2つ(時刻基準は2月29日生まれのみ特別)なので、ここをテストケースにあげればよいわけです。

## 一般的な実装例

### 業務SEが業務仕様決めるノリで作りがちなロジック(時刻基準)

1. 誕生日と基準日の年の差をとり、年齢とする。
1. 誕生日の月が基準日の月を比較
    1. 誕生日の月の方が大きい場合:
        1. 年齢から1を引く
    1. 誕生日の月と基準日の月が同じ場合:
        1. 誕生日の日と基準日の日を比較
            1. 誕生日の日の方が大きい場合:
                1. 年齢から1を引く 
1. 年齢を返す

コードにすると、こんな感じです。

```java
public static int calcAgeAruAru(Date birthday, Date now) {
    int age = now.getYear() - birthday.getYear();
    if (now.getMonth() < birthday.getMonth()) {
        age--;
    } else if (now.getMonth() == birthday.getMonth()) {
        if (now.getDay() < birthday.getDay()) {
            age--;
        }
    }

    return age;
}
```

### エレガントな計算(時刻基準)

年齢計算で広く知られている簡単な方法は、以下のようなものです。
http://itpro.nikkeibp.co.jp/article/Watcher/20070822/280097/

```java
public static int calcAge(Date birthday, Date now) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    return (Integer.parseInt(sdf.format(now)) - Integer.parseInt(sdf.format(birthday))) / 10000;
}
```

パッと見、これでうまくいくのが不思議な気がしますが、実際に値入れて自分で計算してみると、納得できると思います。

### 日基準の計算

日基準の場合は、時刻基準の計算を一日ずらして処理をします。

```java
public static int calcAgeDateStd(Date birthday, Date now) {
    Calendar nowCal = Calendar.getInstance();
    nowCal.setTime(now);
    nowCal.add(Calendar.DAY_OF_MONTH, 1);

    return calcAge(birthday, nowCal.getTime());
}
```

## Javaのライブラリでの実装

Javaのライブラリには、年齢計算に使えそうな、2つの日付の間の年数を数えるものがあります。使えるのか仕様を確認してみましょう。

### Joda Time

日時系ライブラリ定番のJoda Timeには、`Years.yearsBetween`というのがあります。

```java
import org.joda.time.LocalDate;
import org.joda.time.Years;

public class JodaAge {
    public static int calcAge(LocalDate birthday, LocalDate now) {
        Years age = Years.yearsBetween(birthday, now);
        return age.getYears();
    }
}
```

非常にすっきり書けますが、下記Issueのような問題があります。

[Years between failing for leap years #132](https://github.com/JodaOrg/joda-time/issues/132)

2月29日生まれの人が、うるう年には2月28日に歳をとってしまうようです。作者からするとこれは仕様どおり、`between`系操作は年齢計算用ではなく、あくまでも日付の差を出すためのものだ、という説明。一理あります。

ということで、Joda Timeを使っている場合は、年齢計算に`Years.yearsBetween`を使わないようにしましょう。

### java.time.LocalDate

Java8から使えるLocalDateではどうでしょうか。やはり`between`というメソッドが`ChronoUnit`にあります。

```java
long calcDate(LocalDate birthday, LocalDate today) {
    return ChronoUnit.YEARS.between(birthday, today);
} 
```

これは、2月29日生まれの人の計算も、時刻基準の計算と同等になります。
ということで、時刻基準の年齢計算であれば、この`LocalDate`の`between`は使えそうです。

## まとめ

- 年齢計算をする場合は、日基準か時刻基準か、仕様をまず明らかにしよう。
- 法令のしばりがなければ時刻基準を使おう。