0
0

More than 1 year has passed since last update.

【Ruby, Rails】複数の時間から平均を算出する

Last updated at Posted at 2021-12-22

スクリーンショット 2021-12-22 13.33.54.png

はじめに

現在、Ruby on Railsにて睡眠を記録するアプリを作成しています。睡眠の平均時間を表示しようとしたら、一筋縄ではいかず苦労しました。対策を備忘録として残そうと思います。

開発環境

Mac OS Big Sur 11.6
VSCode
Ruby 3.0.2p107
Rails 6.1.4.1
MySQL 8.0.27 for macos11.6 on arm64 (Homebrew)

データ

DB(MySQL)ではSleepLogsテーブルを作成し、以下のデータを保存しています。

sleep_atカラムやwake_atカラムは、データの表示や睡眠時間の計算を行うため、datetime型で保存しています。

項目名 値の例(値の型) 説明
:user_id 1(bigint) userの外部キー
:sleep_at Thu, 02 Dec 2021 03:58:31.000000000 JST +09:00(datetime) 就寝日時
:wake_at Thu, 02 Dec 2021 09:17:00.000000000 JST +09:00(datetime) 起床日時
:satisfaction 1(bigint) 睡眠の満足度

やりたいこと

seedを使い、起床時間がランダムなデータを20件ほど作成しています。保存されている日時データから平均を算出し、平均の起床時間など時間のみのデータを表示したいと考えました。画像では完成していますが、平均の起床時間に焦点をあてて、失敗例と成功例を交えつつ紹介します。

スクリーンショット 2021-12-22 13.33.54.png
(特にCSSを当てていないため見にくいです。申し訳ありません。)

averageを使えばできるのでは?

では、平均起床時間の表示に挑戦します。Railsには指定したカラムの平均を求めるaverageメソッドがあるみたいです。「これでwake_atカラムの平均を求めればできるのでは〜〜??(^^)」と思ったのですが、できませんでした。

***_controller.rb
def show
  # 現在のユーザーの記録のみ取得
  @sleep_logs = SleepLog.where(user_id: params[:id])
  @average = @sleep_logs.average(:wake_at)
end

viewファイルについては、以降省略します。

***.html.erb
<%= @average %>

# => 20211211314945.0

そもそものデータについて考える

wake_atカラムのデータを1件取り出してみます。

***_controller.rb
def show
  @average = @sleep_logs.first.wake_at
end

# => 2021-12-21 10:27:00 +0900

datetime型で保存されているため、起床時間だけではなく、起床した日付も保存されています。また、その中身は起算時からの経過秒数を保持しています。そのため、各レコードから時間と分のみを取り出さなければならないと考えました。

時間と分を取り出す

こちらの記事を参考にしました。 Time型カラムに記録された時間インスタンスの、平均時刻を取得する(日付は無視)

***_controller.rb
def show
  # 現在のユーザーの記録のみ取得
  @sleep_logs = SleepLog.where(user_id: params[:id])

  # timesに時間と分のみを配列として記録
  times = @sleep_logs.pluck(:wake_at).map { |log| log.hour * 60 + log.min }

  # sumメソッドで合計を求め、sizeメソッドの件数で平均を出す
  time_average = times.sum / times.size

  # formatで出力の表示を調整。time_averageは単位が分なので、時間と分に変換する。
  hour = time_average / 60
  min =  time_average % 60
  @average = format('%02<hour>d:%02<min>d', hour: hour, min: min)
end

# => 09:35

とりあえず、これで平均を出力できました。(この平均には誤差があります。@scivolaさま、ありがとうございました。)
showメソッド内をトレースしながら、何をしているか紹介します。

現在のユーザーの記録のみ取得

whereを使用して、現在のユーザーが該当する記録を取得しています。

***_controller.rb
@sleep_logs = SleepLog.where(user_id: params[:id])

コンソールで確認すると中身はこんな感じです。

[#<SleepLog:0x00000001526123c8
  id: 20,
  user_id: 1,
  sleep_at: Tue, 21 Dec 2021 03:58:31.000000000 JST +09:00,
  wake_at: Tue, 21 Dec 2021 10:27:00.000000000 JST +09:00,
  created_at: Wed, 22 Dec 2021 11:58:31.545341000 JST +09:00,
  updated_at: Wed, 22 Dec 2021 11:58:31.545341000 JST +09:00,
  satisfaction: 3>,
 #<SleepLog:0x0000000152612058
  id: 19,
  user_id: 1,
  sleep_at: Mon, 20 Dec 2021 03:58:31.000000000 JST +09:00,
  wake_at: Mon, 20 Dec 2021 08:02:00.000000000 JST +09:00,
  created_at: Wed, 22 Dec 2021 11:58:31.542132000 JST +09:00,
  updated_at: Wed, 22 Dec 2021 11:58:31.542132000 JST +09:00,
  satisfaction: 1>,

  (省略)

timesに時間と分のみを配列として記録

取得したデータ全体の内、必要としているwake_atカラムのみを抽出。その後、mapメソッドで各データの時間と分のみ取り出して、配列で保存しています。

***_controller.rb
times = @sleep_logs.pluck(:wake_at).map { |log| log.hour * 60 + log.min }

# => [627, 482, 662, 492, 662, 544, 680, 552, 564, 541, 491, 565, 553, 622, 507, 662, 617, 629, 500, 557]

sumメソッドで合計を求め、sizeメソッドの件数で平均を出す

配列の合計を求め、配列の要素数で割り平均を求めます。

injectメソッドを使う方法もあるみたいですが、sumの方が早いみたいですね。
[Ruby]いろんな処理のベンチマーク選手権

***_controller.rb
time_average = times.sum / times.size

# => 575

[追記]除算の誤差について

今回は多少の誤差を許容しています。正しい平均を求める方法について@scivolaさまからご紹介いただきました
【Ruby のまずいコード】除算の落とし穴

formatで出力の表示を調整

配列の合計を求め、配列の要素数で割り平均を求めます。

formatについてはこちらを参考にしました。
Rubyのformatメソッドで名前付きのフォーマットを使う

***_controller.rb
hour = time_average / 60
# => 9
min =  time_average % 60
# => 35

@average = format('%02<hour>d:%02<min>d', hour: hour, min: min)

# => "09:35"

参考にした記事

Time型カラムに記録された時間インスタンスの、平均時刻を取得する(日付は無視)
[Ruby]いろんな処理のベンチマーク選手権
Rubyのformatメソッドで名前付きのフォーマットを使う

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0