エンジニアになりたい日記

新卒2年目で休職した文系男子がエンジニアに転職するブログ

Procオブジェクトを使い, 指定された期間における各カラムの値の配列を生成する

アプリ制作

実現したいこと

体調グラフ機能

各カラムに1つボタンを作成し, ボタンのON/OFFでカラムの表示/非表示を切り替えられるようにする。

どのように実現するか

指定された期間における各カラムの値の配列を生成し, Viewに渡す。

本日はrails consoleで考えたコードが動作するのかを試した。

# Diaryレコードをグループ化
records = User.diaries.group_by_week(week_start: :mon) { |n| n.diary_date }

# 期間内のDiaryレコードを1つずつ取り出し, カラムの値を配列で返す
get_column_value_proc = Proc.new do |column, records|
   records.map do |record|
    record.send(column)
   end
 end
#=> #<Proc:0x00007ff445dba088@(irb):47>

# Y軸の値のハッシュ(キー: カラム, バリュー: カラムの値の配列)を返す
 columns.each do |column|
    y_axis_value[column] = get_column_value_proc.call(column, records[date_begin])
 end
#=> [:activity, :mood, :appetite]

 y_axis_value
#=> {:activity=>[1, -1, 2, 2, 2, -1, 2], :mood=>[2, -2, -2, 1, 0, 2, 0], :appetite=>[2, 2, -2, 2, 2, 1, 1]}

プロを目指す人のためのRuby入門

Chart.jsにToggle機能を追加しカラムの表示/非表示を切り替える


実現したいこと

体調グラフ機能, ボタンのON/OFFでカラムの表示/非表示を切り替える

メンターから, Ajaxは学習コストが高いのでjQueryでやった方が早くできるとアドバイスを頂いた。

各カラムのボタンのtoggleによって, 対応するdatasetを追加したり、削除したりできるようにしたい。

どう実装するのか?

この記事についているデモの通りに実装していく。

How to add a dataset toggle to Chart.js?

大まかな流れは、

  • ToggleするリンクをHTML作成し, class名をカラム名 + enabledとする
  • JavascriptenableCheck関数を作成
    • 各カラムのクラス名にenabledが付いているかを判定
    • trueなら変数にグラフのdatasetを代入, falseならnilを代入
  • jQuery.readyenableCheckを実行
  • リンクをクリック時にグラフを削除し, enableCheckを再度実行した上でグラフ描画を行う

結果

Image from Gyazo

datasetとlabelなどの値は一旦固定にして, Toiggle機能のみ実装した。

次回はRailsで全カラムのデータをブラウザにレスポンスできるようにする。

その他の作業

jQuery 要素を表示/非表示にする(toggle) を参考にボタンのtoggleを試しに作成する

プロを目指す人のためのRuby入門

体調グラフ機能で複数カラムを動的に表示する方法を調べる

アプリ制作

実現したいこと(1)

複数カラムを選択して同時に描画する.

activity, appetite, moodの3カラムのボタンを設置する. 3つのボタンはON/OFFにすることができ、ONの状態にあるカラムをまとめて表示したい。

試したこと

Rails Chart.js Dynamically change columnで調べると、Ajaxを使った方法が多く見られた。

Ajaxを使用したことがないので、学習方法をメンターに質問。(進捗報告も兼ねて)

実現したいこと(2)

Diaryのカラムごとに色分けをしてグラフを表示する

どのように実装するか

Chartjs-ror gem を導入し, グラフのUIをRailsで設定できるようにする。

# example
data = {
  labels: ["January", "February", "March", "April", "May", "June", "July"],
  datasets: [
    {
        label: "My First dataset",
        backgroundColor: "rgba(220,220,220,0.2)",
        borderColor: "rgba(220,220,220,1)",
        data: [65, 59, 80, 81, 56, 55, 40]
    },
    {
        label: "My Second dataset",
        backgroundColor: "rgba(151,187,205,0.2)",
        borderColor: "rgba(151,187,205,1)",
        data: [28, 48, 40, 19, 86, 27, 90]
    }
  ]
}
options = { ... }

<%= line_chart data, options %>

コードを読んでみる。

  • dataハッシュ
    • labelsキー: X軸の値 (Array)
    • datasetsキー:単独または複数の datasetハッシュ (Array)
  • datasetハッシュ (ある期間における1カラムのグラフを表す)

    • labelキー : legend name (String)
    • backgroundcolorキー: グラフの背景色, rgba (String)
    • bordercolorキー: グラフの枠線の色, rgba(String)
    • dataキー: Y軸の値(Array)
  • Viewでline_chartメソッドを呼び出し, 引数にdata, optionを代入する

  • Viewでline_chartメソッドを呼び出し, 引数にdata, optionを代入する

使用したい色

#51a370 rgb(81,162,112)  Green  Mood
#ffeb8a rgb(255,235,138) YELLOW Appetite
#e86c4e rgb(232,108,77)  Orange Activity

その他の作業

  • 期間やカラムの移動に関するリンクをテンプレート化し、app/views/layoutsへ置いた

プロを目指す人のためのRuby入門

  • 10章 Procとラムダ(2時間)

Weeklyグラフで前後の期間へのリンク生成で発生するエラーをデバッグ


アプリ制作

実現したいこと1

リクエストが下の2つのパターン両方でグラフ描画を行えるようにする。前者はコントローラのreportbydateアクション, 後者はreportbyperiodアクションにルーティングされている。

statistics/activity/from/2019-10-07/to/2019-10-13, statistics/activity/period/prevWeek

課題1

statistics/activity/period/prevWeekで先週のグラフを表示する。これは2019-10-07/to/2019-10-13の期間のグラフが表示できている。

(1) 次に前週へのリンクを押し, 2019-09-30 ~ 2019-10-06の期間のグラフを表示する。しかし、この期間のレコードは存在するのにグラフには該当日付のY軸の値が表示されない。

(2) そのまたさらに前週へのリンクを押すと, 本来は2019-09-23 ~ 2019-09-29の期間がリクエストされるはずにもかかわらず, 2019-08-01 ~ 2019-09-29の期間がリクエストされる。

課題1がなぜ起こるのか?・修正すべき点と結果

binding.pryでコントローラの@reportが正しく生成されているか確認をした。

Image from Gyazo

2019-09-30 ~ 2019-10-06の期間, つまり1週間でリクエストをしているのに、MonthlyReportクラスが呼ばれていた。(期間が1ヶ月単位の時に呼び出すように設計したつもりだった)

リクエストされた期間に対して, コントローラ内でWeeklyReportかMonthlyReportかを条件分岐する部分が意図した通りに機能していない。その部分を修正する必要がある。

コントローラではis_date_weekly?メソッドでtrueならWeeklyReport, falseならMonthlyReportを生成するように分岐させていた。2019-09-30 ~ 2019-10-06のリクエストに対して, このメソッドでfalseを返してしまい, MonthlyReportが生成されてしまっていた。

def is_date_weekly?(date_begin, date_end)
  (date_begin..date_end).to_a.length <= 7 ? true : false
end

なぜそうなるかというと、メソッドの引数date_begin, date_endがStringクラスのまま代入されていた。Dateオブジェクトに変換するように修正すると, WeeklyReportを生成できた。

さらに発生した懸念点

MonthlyReportクラスではgroup_by_monthで月初の日付をキーにしてグループ化したハッシュを作成している。そのためdate_beginが月初の日付でないと, 該当期間のY軸の値をすべてnilにして配列で返してしまう.

(この,keyが見つからない場合にnilの配列を返す処理は, Diaryレコードがない期間のリクエストの際にエラー発生を防ぐ目的で実装していた。)

def data_generator(user, column, date_begin)
  records = user.diaries.group_by_month(&:diary_date)
  if records[date_begin].present?
    records[date_begin].map do |diary|
      diary.send(column)
    end
  elsif records[date_begin].nil?
    date_range.length.times.map { |n| nil }
  else
    raise "Error"
 end

グループしたHashのキーが存在しなかった場合, 該当期間のY軸がすべてnilになる可能性がある。group_byメソッドの処理を確認したり、Reportクラスの処理を修正する必要があるなと思った。

実現したいこと2

同じカラムで週間→月間, 月間→週間の遷移をする

どのように実装したか?

  1. 週間→月間
    • WeeklyReportクラスにto_monthlyメソッドを作成
    • date_beginのbeginning_of_month, end_of_monthを取得して, URI文字列を返す
  2. 月間→週間
    • MonthlyReportクラスにto_weeklyメソッドを作成
    • date_beginとdate_endの日数の差の2分の1の日付を基準日にし、基準日の週の月曜, 日曜をそれぞれdate_begin, date_endにセット
    • URI文字列を返す

Viewで@reportがWeekly, Monthlyどちらのクラスのインスタンかを確かめる

#app/views/statistics/report_by_date.html.erb
<% if @report.class.method_defined? :to_monthly %>
  <%= link_to "月間", "#{@report.to_monthly}" %>
<% end %>
<% if @report.class.method_defined? :to_weekly %>
  <%= link_to "週間", "#{@report.to_weekly}" %>
<% end %>

実装できた。

ここまでに実装した機能(体調グラフ)

  • URIの形式

    • 期間を表す単語で期間を指定する

      /statistics/:column/period/thisWeek

      • 単語は4種類 [thisWeek, thisMonth, prevWeek, prevMonth]

      • PeriodConstaraintクラスでリクエストのURIをこれら4種類のみに限定

    • 日付で期間を指定

      /statistics/:column/from/:date_begin/to/:date_end

      • DateConstraintクラスで日付を表す文字列かどうかを検証
  • 期間は週間、月間の2週類

  • それぞれの期間について、以下のことができる.

    • 前後の期間へのリンクを生成する機能(同カラム内)
    • 週間→月間、月間→週間へのリンクを生成する機能(同カラム内)
  • 同期間内で異なるカラムへのリンクを生成する機能

プロを目指す人のためのRuby入門

  • 第10章 Procとyieldを理解する(2時間)

キーワード引数にデフォルト値を設定し, 異なるパターンの引数を取れるようにする


アプリ制作

実現したいこと

リクエストが statistics/activity/from/2019-10-07/to/2019-10-13, statistics/activity/period/prevWeekの2つのパターン両方でグラフ描画を行えるようにする。前者はコントローラのreport_by_dateアクション, 後者はreport_by_periodアクションにルーティングされています。

実装方法

lib下のDateConstraintクラスでURIの/:datebegin, /:dateendを Date.valid_date?メソッドで検証する(year, month, dayでマッチデータオブジェクトを生成してメソッドに代入)

コントローラでparams[:datebegin], params[:dateend]をインスタンス変数にセット

コントローラでweekly, monthlyで場合分けしてReportクラスへ渡す

PERIODSに該当する場合はリダイレクト?

詰まった部分

app/controllers/statistics_controller.rbreport_by_dateアクションで、

GenerateReport::WeeklyReport.newした際にArgumentErrorが発生する。

Image from Gyazo

  • どのように実装してエラーが発生したのか?
#lib/generate_report/report.rb
module GenerateReport
  class Report
    def initialize(user:, column:, period: nil)
      @user = user
      @column = column
      @period = period ? period : nil 
    end
  end 
end


#lib/generate_report/monthly_report.rb
module GenerateReport
  class WeeklyReport < GenerateReport::Report

    attr_reader :column, :date_begin, :date_end

    def initialize(user:, column:, period: nil, date_begin: nil, date_end: nil)
      super(user, column, period)
      @date_begin = date_begin ? date_begin : generate_date_begin(period)
      @date_end = date_end ? date_end : date_begin.end_of_week
    end
    # 省略
  end 
end 

#StatisticsController
class StatisticsController < ApplicationController
  def report_by_date
    @COLUMNS = { activity: "活動量", mood: "気分", appetite: "食欲" }
    set_params_when_report_by_date_action(statistics_params)
    if is_date_weekly?(@date_begin, @date_end)
      @report = GenerateReport::WeeklyReport.new(user: current_user, column: @column, date_begin: @date_begin, date_end: @date_end)
    else
      @report = GenerateReport::MonthlyReport.new(user: current_user, column: @column, date_begin: @date_begin, date_end: @date_end)
    end
    binding.pry
  end
end 
  • ここで実現したいこと
    • statisticsコントローラのreport_by_periodアクションではWeeklyReport.newの引数がuser, column, period の3つ
    • 同コントローラのreport_by_dateアクションではWeeklyReport.newの引数がuser, column, date_begin, date_end の4つ
    • user, columnは両アクションに共通していて、period, date_begin, date_endは存在すれば代入し、無ければinitialize時に別途セットするようにしたい。
  • Report, WeeklyReportクラスの初期化時の引数を修正する
#lib/generate_report/report.rb
class Report
  attr_reader :column, :period, :date_begin, :date_end

  def initialize(user:, column:, period: nil, date_begin: nil, date_end: nil)
    @user = user
    @column = column
    @period = period ? period : nil
  end
  #省略
end 

#lib/generate_report/monthly_report.rb
class WeeklyReport < GenerateReport::Report

  attr_reader :column, :date_begin, :date_end

  def initialize(user:, column:, period: nil, date_begin: nil, date_end: nil)
    super
    @date_begin = date_begin ? date_begin : generate_date_begin(period)
    @date_end = date_end ? date_end : date_begin.end_of_week
  end
  #省略
end

/statistics/activity/prevWeekのリクエスト時

Image from Gyazo

/statistics/activity/from/2019-10-07/to/2019-10-13のリクエスト時

Image from Gyazo

WeeklyReportクラスでintialize時に引数が異なる場合でも, 共通の処理を提供できるようになった。

体調グラフ機能, 該当期間のy軸の値を吐き出すメソッドに例外処理を実装する(Rails, Ruby)

アプリ制作

実現したいこと(1)

Diaryレコードが存在しない期間を指定した際に、app/lib/generate_report/weekly_report.rbdata_generatorメソッドでエラーが発生してしまう。レコードが存在しない場合でも値を0としてグラフの描画を行えるようにしたい。

def data_generator(user, column, date_begin)
  records = user.diaries.group_by_week(week_start: :mon) { |n| n.diary_date }
  ## レコードが存在しない期間を指定すると, Hashで存在していないキー指定するためnilが返る
  ## nilの場合にエラー, 
  records[date_begin].map do |diary|
    diary.send(column)
  end
end

どのように実装するか?

records[date_begin]nilかどうか確かめ、nilの時は配列のnilの要素を7個作成して返す。

def data_generator(user, column, date_begin)
  records = user.diaries.group_by_week(week_start: :mon) { |n| n.diary_date }
  if recordes[date_begin].present?
    records[date_begin].map do |diary|
      diary.send(column)
    end
  elsif records[date_begin].nil?
    7.times.map { |n| nil }
  else
    raise "Error"
  end
end

手動でテスト

  • 今週(10/14-10/20)のレコードを削除し, 今週の体調グラフにアクセスし以下の項目を確認
    • エラーが発生しないか?
    • グラフの縦軸の値が何も表示されていないか?
  • テスト成功

実現したいこと(2)

statistics/:column/from/:date_begin/to/:date_endでグラフ描画をする

どのように実装するか?

lib下のDateConstraintクラスでURIの/:date_begin, /:date_endを Date.valid_date?メソッドで検証する(year, month, dayでマッチデータオブジェクトを生成してメソッドに代入)

コントローラでparams[:date_begin], params[:date_end]をインスタンス変数にセット

コントローラでweekly, monthlyで場合分けしてReportクラスへ渡す

PERIODSに該当する場合はリダイレクト?

その他に実装したこと

/statistics/:column/period/thisMonth(or prevMonth)でグラフ描画できるようにした。

プロを目指す人のためのRuby入門

  • 例外処理について

サービスクラスにグラフ描画に関する処理を移す

アプリ制作

昨日のエラー

Constraint::PeriodConstraintクラスで, /statistics/activity/period/thisWeekの"thisWeek"の部分が正しいURIになっているか検証するように実装した。

しかしno route matchedエラーが出てしまい、うまく作動していない。原因がわからなかった。

module Constraint
  class SelectPeriodConstraint
    PERIODS = ['thisWeek', 'thisMonth', 'prevWeek', 'prevMonth']

    def matches?(request)
      PERIODS.include?(request.query_parameters['period'])
    end
  end
end

binding.pryで確認してみるとrequest.query_parameters['week_or_month']nilになっていた。requestはActionDispatch::Requestクラスだと分かったので、ドキュメントを確認するとparamsメソッドでURIのセグメントにアクセスできると分かり、解決。

本日実装した機能

  • /statistics/:column/period/:periodで期間を指定してグラフを描画する機能
  • :periodセグメントをRouting時に検証する(RailsGuideのAdvanced Constraintsを使用)
  • Weeklyのグラフ描画時に前後1週間へのリンクを生成する機能
  • 表示しているカラム以外の、2つのカラムへのリンクを生成する機能
    • activity, mood, appetiteのうち、 activityを表示している時に残り2つのカラムへの, 同期間のリンクを生成する

Image from Gyazo

どのように実装したか?

サービスクラスとしてGenerateReport::Reportクラス, さらにReportクラスを継承するWeeklyReport, MonthlyReportクラスを作成した。

コントローラでparamsを解析してWeekyかMonthlyで条件分岐をして、それぞれに処理を移した。

それによって、コントローラの肥大化を抑えた。

  • Reportクラスの主な処理
    • リクエスURI:periodセグメントの文字列から始点, 終点の日付をセットする
    • それらの日付とカラムから各日付のカラムの値を取り出す
    • 前週、次週へのURI文字列を生成する
    • 同期間の別カラムへのURI文字列を生成する

参考にしたもの

[SimpleCalender](<https://github.com/excid3/simple_calendar)というgemのコードをGithubで観察した。

「Reportクラスを継承して、Weekly, Monthlyでクラスを継承する」というアイデアはこのgemからトレースしてみた。

このGemはRailsの深いところのメソッドをたくさん使っていて、理解できないところは多かった。

コードを観察したことで、Railsの仕組みを理解したい!みたいなモチベーションが生まれた。