Railsのtime_selectでデフォルトで入力される日付を、画面上にある別のフィールドの入力値にする


背景とやったこと

Railsのアプリで、こんな感じのフォームを作っていました。
実施日をdate_fieldで選択し、開始時間・終了時間をそれぞれtime_selectで選択します。

なお、実行環境はrails 5.2.4.2です。

viewファイルの中身はこんな感じです。データ型は、全てのフィールドでDateTime型でした。

view
.form-group  
  = f.label :date, "実施日"
  = f.date_field :date
.form-group  
  = f.label :begin_at, "開始時間"
  = f.time_select :begin_at
.form-group  
  = f.label :closed_at, "終了時間"
  = f.time_select :closed_at

起こっていた問題

このフォームから、データを送信すると、次のようになりました。

#<Activity:0x00007fc97030fe30
 id: 1,
 date: Fri, 11 Sep 2020 00:00:00 JST +09:00,

 begin_at: Mon, 1 Jan 0001 00:10:00 JST +09:00,
 closed_at: Mon, 1 Jan 0001 00:20:00 JST +09:00

 time_took: 600,

 created_at: Fri, 04 Sep 2020 13:53:01 JST +09:00,
 updated_at: Fri, 04 Sep 2020 13:53:01 JST +09:00>

…はい??
何で、西暦1年の1月1日の時間になっているのでしょうか....。

いろいろ調べたところ、これはRailsの仕様でしょうがないようです。
こちらの記事のように時間だけを入力させる方法もあるにはありました。
(ただし、begin_atclosed_atのデータ型はTime型にする必要がありそうです)

time_selectで時間だけを入力させる[rails]

ただ、今回は、time_tookという別カラムで「かかった時間」を計算していたり、今後これらの数字を分析に使うことが予想されていたりと、

色々とbegin_atclosed_atの値を使い回しそうだったので、正確に日時を表すよう、time_selectフォームの入力値に、dateフィールドで入力した年・月・日を入れることにしました。

やったこと

モデルに以下のようなメソットを定義し、それをbefore_saveコールバックで呼び出します。

model
class Activity < ApplicationRecord
  before_save :set_the_day_implement

  private

  def set_the_day_implement
    year = self.date.year
    month = self.date.month
    day = self.date.day

    self.begin_at = self.begin_at.change(year: year, month: month, day: day)
    self.closed_at = self.closed_at.change(year: year, month: month, day: day)
  end
end

コードの解説です。self.date.yearのかたちで、フォームから入力した「実施日(date)」の年をそれぞれ取り出すことができます。month, dayもそれぞれ同様です。

# dateフィールドの値が '2020-09-04'の場合
self.date.year
#=> 2020
self.date.month
#=> 9
self.date.day
#=> 4

また、self.begin_atself.closed_atの値には、それぞれ以下のような形でアクセスできます。

self.begin_at
#=> Mon, 1 Jan 0001 00:10:00 JST +09:00
self.begin_at.year
#=> 1
self.begin_at.month
#=> 1
self.begin_at.day
#=> 1

self.begin_at.change(year: 2020, month: 9, day: 4)
#=> Fri, 4 Sep 2020 00:10:00 JST +09:00

ただし、最後にself.begin_atに、self.begin_at.change(引数)で定義した内容を代入しないと、カラムの値は更新されません。(←ここに気づかなくて結構ハマった)

# dateフィールドの値が '2020-09-04'の場合
year = self.date.year
month = self.date.month
day = self.date.day

self.begin_at.change(year: year, month: month, day: day)
#=> Fri, 4 Sep 2020 00:10:00 JST +09:00
self.begin_at
#=> Mon, 1 Jan 0001 00:10:00 JST +09:00
# 西暦1年に戻っている

self.begin_at = self.begin_at.change(year: year, month: month, day: day)
self.begin_at
#=> Fri, 4 Sep 2020 00:10:00 JST +09:00
# 代入するとカラムの値が更新される

さて、これで無事欲しいデータを手に入れることができました。

最終的にビューで表示されるのはこんなデータです。(カッコ内の数字は、別のコールバックで作成した「かかった時間」です。)

今後活用が予想されるデータなので、正しい日付のデータがきちんと入って満足なのでした
引き続きお仕事頑張っていきます♪