🎂
Rails で誕生日から年齢を計算する方法
誕生日から年齢を計算する
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にせずベタ書きしたほうが可読性も高くなると思いました。日時系、境界値テスト系のときは特に意識しようと思います。
Discussion
@kanazawa さん、こんにちは。
本題とは若干逸れるのですが、テストコードが以前僕が書いたアンチパターンに該当しちゃってるな〜というのがちょっと気になってます。
テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita
プロダクション側のコードとほぼ同じロジックをテストコード上で展開すると、テストコードが読みづらくなったり、不具合を検出しづらくなったりするので、テストコードはなるべくリテラルを使ってベタ書きするように書くのがオススメです〜 😃
伊藤さん、ご指摘ありがとうございます。
修正してみたんですが、こんな感じでしょうか?👀
うーん、todayとかthis_years_birthdayのあたりがまだ動的なのでこれでもちょっとわかりづらいですね。僕だったらこう書きます。
これなら一見して仕様が明確になると思うのですが、いかがでしょう?
一般論として、APIドキュメントのサンプルコードを書くぐらいのつもり or 非プログラマの人にもなんとなく仕様を理解してもらうつもりでテストコードを書くのをオススメします。
なるほど〜!ここまでベタで書いてもいいんですね、とても勉強になりました。ありがとうございます!
本記事のほうにはこのコメント欄も参照してもらうよう変更します。