【Rails 5】Userの現在の年齢を生年月日から計算する方法と、ransackを使わずに年齢の範囲検索フォームの実装


目標

・gem(ransack)を使わずに年齢の範囲検索フォームを作成する

・view上では年齢での検索だが、Usesテーブルにageカラムは作成していないので、birthdayカラムからユーザーを取ってくる


悩んだこと

・年齢で検索するのにもかかわらず、birthdayカラムからUserを探して取り出す方法


完成図

レイアウトは後で適当に調節するとして、

スクリーンショット 2019-03-04 18.24.13.png

とりあえずこんな感じ。


ポイント

今回肝になるのは

・生年月日から年齢の計算方法

・年齢の範囲から生年月日の範囲を求める方法


である。

生年月日から年齢の計算方法

現在と生年月日それぞれをyyyymmdd形式で表して、

(現在-生年月日)/10000

の値を求め、この値の整数部分が年齢となる。

例えば、現在が2019/03/04、生年月日が1990/12/25だったとすると、

(20190304-19901225)/10000

=28.9079

よって、28歳ということになる。

なぜこの式で良いのかが知りたい人は


生年月日から年齢を簡易計算する数式 - Qiita


に詳しく載っているので、参考にしてください。

年齢の範囲から生年月日の範囲を求める方法

上記のことの逆を考えれば良い。

つまり、例えば20歳である人は現在と生年月日との差が

20 × 100000 ~ 20 × 10000 + 9999

であれば良いことが考えられる。

ということはもしも、20~30歳を検索しようと思ったのならば、現在と生年月日との差が

200000~309999

になる生年月日を見つけてくれば良い、ということだ。


実装

まず、検索フォーム

index.html.haml

= form_tag(users_serch_path, method: :get) do

%p 年齢
= text_field_tag :search_min_age
%p
= text_field_tag :search_max_age
%br
= submit_tag '検索', name: nil

ここでのusers_serch_pathの部分はよしなに自分の表示させたいviewのpathを書いてください。

users_helper.rb

module UsersHelper

# 渡された年齢に本日なったばかりの生年月日をyyyymmdd形式で出力
def calc_younger_birthday(age)
Date.today.strftime("%Y%m%d").to_i - age.to_i * 10000
end
# 渡された年齢であるギリギリの生年月日をyyyymmdd形式で出力
def calc_older_birthday(age)
Date.today.strftime("%Y%m%d").to_i - age.to_i * 10000 - 9999
end
end

ここでは、controllerをスッキリさせるためにメソッドを作成しています。引数のageはparamsの値を入れる予定なので、計算を実装するために文字列を数値へと変換してくれるto_iメソッドを使用しています。

users_controller.rb

class UsersController < ApplicationController

include UsersHelper

def search
# 指定された年齢となる生年月日をyyyymmdd形式の文字列へと変換
younger_birth_ymd = calc_younger_birthday(params[:search_min_age]).to_s
older_birth_ymd = calc_older_birthday(params[:search_max_age]).to_s
# yyyymmdd形式の生年月日を日付形式に変換
younger_birthday = Time.parse(younger_birth_ymd)
older_birthday = Time.parse(older_birth_ymd)
# 条件に当てはまるUserを検索
@users = User.where(birthday: older_birthday..younger_birthday)
end
end

ここでは、先ほどのhelperに書いたメソッドと、数値を文字列に変換するto_sメソッドを用いて、与えられた年齢の範囲での生年月日のうち、2つのローカル変数である、もっとも最近の場合(younger_birthday)と昔の場合(older_birthday)それぞれを引数に代入している。

そうして、これらの値をTime.parse()によって、日付形式に戻し、whereでbirthdayカラムの値が条件の範囲に当てはまるUserを見つけられる。

あとは@users.each do |user|などを用いてviewに出力すれば検索結果を表示できる。


少し改良

このままでも問題はないが、〜以上〜以下のようにして検索しようとした時に、片方だけに値を入れて検索するということもよくあるだろう。そんな時のために条件分岐を書くことをオススメする。コードは以下のようだ。

users_controller.rb

def search

unless params[:search_min_age] == ""
# 指定された年齢となる生年月日をyyyymmdd形式の文字列へと変換
younger_birth_ymd = calc_min_birthday(params[:search_min_age]).to_s
else
# 最小の年齢として0歳にした
younger_birth_ymd = calc_min_birthday("0").to_s
end
unless params[:search_max_age] == ""
older_birth_ymd = calc_max_birthday(params[:search_max_age]).to_s
else
# 最大の年齢として十分な150歳にした
older_birth_ymd = calc_min_birthday("150").to_s
end
# yyyymmdd形式の生年月日を日付形式に変換
younger_birthday = Time.parse(younger_birth_ymd)
older_birthday = Time.parse(older_birth_ymd)
# 条件に当てはまるUserを検索
@users = User.where(birthday: older_birthday..younger_birthday)
end

やっていることは単純だ。空白だったら、代表値として代わりに0150を引数に入れている。


終わりに

今回は本来はviewで用いるメソッドを主に記述するhelperにcontrollerで用いるメソッドを定義してしまっているので、ここのところはよしなに変更してくださると良いと思います。

懸念点としては、controllerはできる限りスッキリとさせなければならないが、今回はcontrollerがとてもごちゃごちゃとしている。少しでも可読性を上げるためにコメントを追加しているが、やはりcontrollerにしては少しむさっとしている。

美しいリファクタリングのアドバイスをいただけると幸いです。