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

### 目標
・gem(`ransack`)を使わずに年齢の範囲検索フォームを作成する
・view上では年齢での検索だが、Usesテーブルにageカラムは作成していないので、birthdayカラムからユーザーを取ってくる

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

### 完成図
レイアウトは後で適当に調節するとして、
![スクリーンショット 2019-03-04 18.24.13.png](https://qiita-image-store.s3.amazonaws.com/0/298735/ab928861-04be-aa09-8490-de9b3e2be4e7.png)
とりあえずこんな感じ。

### ポイント

今回肝になるのは
**・生年月日から年齢の計算方法
・年齢の範囲から生年月日の範囲を求める方法**
である。

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

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

```
(現在-生年月日)/10000
```
の値を求め、この値の整数部分が年齢となる。

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

```
(20190304-19901225)/10000
=28.9079
```

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

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

> [生年月日から年齢を簡易計算する数式 - Qiita](https://qiita.com/aqril_1132/items/46e824541e657190ef3e)

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


**年齢の範囲から生年月日の範囲を求める方法**は
上記のことの逆を考えれば良い。
つまり、例えば20歳である人は現在と生年月日との差が

```
20 × 100000 ~ 20 × 10000 + 9999
```
であれば良いことが考えられる。

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

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

### 実装

まず、検索フォーム

**index.html.haml**

```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**

```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**

```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`メソッドを用いて、与えられた年齢の範囲での生年月日のうち、２つのローカル変数である、もっとも最近の場合(`younger_birthday`)と昔の場合(`older_birthday`)それぞれを引数に代入している。
そうして、これらの値を`Time.parse()`によって、日付形式に戻し、`where`でbirthdayカラムの値が条件の範囲に当てはまるUserを見つけられる。

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

### 少し改良

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

**users_controller.rb**

```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
```

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

### 終わりに

今回は本来はviewで用いるメソッドを主に記述するhelperにcontrollerで用いるメソッドを定義してしまっているので、ここのところはよしなに変更してくださると良いと思います。
懸念点としては、controllerはできる限りスッキリとさせなければならないが、今回はcontrollerがとてもごちゃごちゃとしている。少しでも可読性を上げるためにコメントを追加しているが、やはりcontrollerにしては少しむさっとしている。

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