Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

2月29日に女性からプロポーズされた男性は、断ってはいけない

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

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

年齢計算は日本の場合は、「年齢計算ニ関スル法律」に基づいた計算になります。

ここで、大きく別れるのが、時刻基準で計算するか日基準で計算するかです。
一般的には時刻基準で計算するので、4月1日生まれの人は、4月1日の0時ちょうどに歳をとることにすればよいですが…、日基準の場合には誕生日前日に歳をとることになります。日基準で計算される有名なのは学齢がありますが、選挙権なんかも同じく日基準のようです。

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

一番問題となる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. 誕生日と基準日の年の差をとり、年齢とする。
  2. 誕生日の月が基準日の月を比較
    1. 誕生日の月の方が大きい場合:
      1. 年齢から1を引く
    2. 誕生日の月と基準日の月が同じ場合:
      1. 誕生日の日と基準日の日を比較
        1. 誕生日の日の方が大きい場合:
          1. 年齢から1を引く
  3. 年齢を返す

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

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/

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;
}

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

日基準の計算

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

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というのがあります。

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

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

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

java.time.LocalDate

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

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

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

まとめ

  • 年齢計算をする場合は、日基準か時刻基準か、仕様をまず明らかにしよう。
  • 法令のしばりがなければ時刻基準を使おう。
kawasima
Clojure関連のことをブログがわりに書き綴ります。 ※ここでの発言はシステムエンジニアを代表するものであって、所属する組織は二の次です。
https://github.com/kawasima/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした