🎂

Rails で誕生日から年齢を計算する方法

2022/06/21に公開
4

誕生日から年齢を計算する

Railsで誕生日から年齢を計算する方法をインターネットで検索すると、以下のような実装例がヒットします。

def age(date)
    date_format = "%Y%m%d"
    (date.strftime(date_format).to_i - born_on.strftime(date_format).to_i) / 10000
end

この方法でもいいと思いますが、個人的には以下の方法が好きです。(Active Decorator使用を想定)

app/decorators/user_decorator.rb
module UserDecorator
  def age
    today = Time.zone.today
    this_years_birthday = Time.zone.local(today.year, birth_date.month, birth_date.day)
    age = today.year - birth_date.year
    if today < this_years_birthday
      age -= 1
    end
    "#{age} years old"
  end
end

(Userのbirth_date属性はNOT NULLという前提)

誕生日がまだ来ていなければ、ageから1を引いた年齢を返します。
行数は増えてしまいますが、何をしているのかがわかりやすく、個人的には好きです。コードの行数が多い=可読性が低い、ではないと思っています。

また、ヘルパーを使って表示させる方法もありますが、今回のようにテストを書いたほうがいい場合はデコレータを使うほうが好みです。

テスト

ついでにテストも書いてみました。

require 'rails_helper'

RSpec.describe UserDecorator do
  describe '#age' do
    let(:user) { create(:user, birth_date: Faker::Date.birthday) }
    let(:today) { Time.zone.today }
    let(:this_years_birthday) { Time.zone.local(today.year, user.birth_date.month, user.birth_date.day) }
    let(:age) { today.year - user.birth_date.year }
    let(:decorated_user) { ActiveDecorator::Decorator.instance.decorate(user) }

    context '誕生日以降の場合' do
      it '誕生日を過ぎた年齢が返る' do
        travel_to(this_years_birthday) do
          expect(decorated_user.age).to eq("#{age} years old")
        end
      end
    end

    context '誕生日以前の場合' do
      it '誕生日前の年齢が返る' do
        travel_to(this_years_birthday.yesterday) do
          expect(decorated_user.age).to eq("#{age - 1} years old")
        end
      end
    end
  end
end

補足:travel_toを使うにはrails_helper.rbに以下を記述する必要があります。

rails_helper.rb
config.include ActiveSupport::Testing::TimeHelpers

ActiveSupport::Testing::TimeHelpers

追記: 良いテストコード

コメント欄より伊藤さん(@jnchito)からご指摘いただいた改善コードです。

describe '#age' do
  let(:user) { create(:user, birth_date: '1977/07/17'.to_date) }
  let(:decorated_user) { ActiveDecorator::Decorator.instance.decorate(user) }

  context '誕生日より以前の場合' do
    it '誕生日前の年齢が返る' do
      travel_to('2022/07/16 23:59'.in_time_zone) do
        expect(decorated_user.age).to eq("44 years old")
      end
    end
  end

  context '誕生日以降の場合' do
    it '誕生日を過ぎた年齢が返る' do
      travel_to('2022/07/17 00:00'.in_time_zone) do
        expect(decorated_user.age).to eq("45 years old")
      end
    end
  end
end

確かに、今回の場合は期待値をDRYにせずベタ書きしたほうが可読性も高くなると思いました。日時系、境界値テスト系のときは特に意識しようと思います。

参考:テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita

Discussion

Junichi ItoJunichi Ito

@kanazawa さん、こんにちは。

本題とは若干逸れるのですが、テストコードが以前僕が書いたアンチパターンに該当しちゃってるな〜というのがちょっと気になってます。

テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita

プロダクション側のコードとほぼ同じロジックをテストコード上で展開すると、テストコードが読みづらくなったり、不具合を検出しづらくなったりするので、テストコードはなるべくリテラルを使ってベタ書きするように書くのがオススメです〜 😃

KotaKota

伊藤さん、ご指摘ありがとうございます。
修正してみたんですが、こんな感じでしょうか?👀

  describe '#age' do
    let(:user) { create(:user, birth_date: '2000/01/01') }
    let(:today) { Time.zone.today }
    let(:this_years_birthday) { Time.zone.local(today.year, user.birth_date.month, user.birth_date.day) }
    let(:decorated_user) { ActiveDecorator::Decorator.instance.decorate(user) }

    context '誕生日以降の場合' do
      it '誕生日を過ぎた年齢が返る' do
        travel_to(this_years_birthday) do
          expect(decorated_user.age).to eq("22 years old")
        end
      end
    end

    context '誕生日より以前の場合' do
      it '誕生日前の年齢が返る' do
        travel_to(this_years_birthday.yesterday) do
          expect(decorated_user.age).to eq("21 years old")
        end
      end
    end
  end
Junichi ItoJunichi Ito

うーん、todayとかthis_years_birthdayのあたりがまだ動的なのでこれでもちょっとわかりづらいですね。僕だったらこう書きます。

describe '#age' do
  let(:user) { create(:user, birth_date: '1977/07/17'.to_date) }
  let(:decorated_user) { ActiveDecorator::Decorator.instance.decorate(user) }

  context '誕生日より以前の場合' do
    it '誕生日前の年齢が返る' do
      travel_to('2022/07/16 23:59'.in_time_zone) do
        expect(decorated_user.age).to eq("44 years old")
      end
    end
  end

  context '誕生日以降の場合' do
    it '誕生日を過ぎた年齢が返る' do
      travel_to('2022/07/17 00:00'.in_time_zone) do
        expect(decorated_user.age).to eq("45 years old")
      end
    end
  end
end

これなら一見して仕様が明確になると思うのですが、いかがでしょう?

一般論として、APIドキュメントのサンプルコードを書くぐらいのつもり or 非プログラマの人にもなんとなく仕様を理解してもらうつもりでテストコードを書くのをオススメします。

KotaKota

なるほど〜!ここまでベタで書いてもいいんですね、とても勉強になりました。ありがとうございます!
本記事のほうにはこのコメント欄も参照してもらうよう変更します。