【決断くん】※こだわりポイント※ 重みづけを用いた抽選機能を実装
今回実装したいこと
1、「最終確認」画面と「決断結果」画面の間に、「武士の情け」画面を挟む
2、「武士の情け」画面で選ばれた選択肢は、決断くんによって選ばれやすくなる(70%の確率で選ばれるようにする)
実装方法
今回の作戦
1、new→confirm画面への遷移と同様に、hidden_fieldを使って、武士の情け(compassion)ページにタイトルと選択肢のパラメータを渡す。
2、compassionページには、お馴染みのhidden_fieldとf.radio_buttonでラジオボタンを設け、お題(:title)と5つの選択肢(:option_1~5)、そしてラジオボタンで選択された選択肢(:primary_option)を全て取得できるように。
3、createアクションとは別に、重みづけ専用のcompassion_createアクションを用意。 [option_1, option_2, …, primary_option]の形の配列にして、 gem ‘weighted_randomizer’を使って重みづけを行い抽選する。
1、「武士の情け(compassion)」ページへのルーティングとコントローラーを実装
# route.rb resources :choices, only: [:new, :create, :edit, :update, :index, :show] do collection do post :confirm, :compassion get :alert end
confirmと同様に、collectionを用いてネストさせる。
new→confirmと同様に、confirmからcompassionにパラメータを渡すのでリクエストはpostに。
# choices_controller class ChoicesController < ApplicationController before_action :set_choice_instance, only: [:confirm, :create, :edit, :compassion, :compassion_create] . . def compassion; end . . private def set_choice_instance return if current_user.nil? @choice = current_user.choices.build(choice_params) end
ここは関してはnew→confirmへの遷移と一緒。
# choices/confirm.html.slim = form_with model: @choice, local: true do |f| = f.hidden_field :title = f.hidden_field :option_1 = f.hidden_field :option_2 = f.hidden_field :option_3 = f.hidden_field :option_4 = f.hidden_field :option_5 <br><br> = f.submit 'いざ!', class: 'ui secondary button' = f.submit '選択肢を考え直す', name: 'back', class: 'ui secondary button' = link_to root_path, class: 'ui secondary button' | #{t '.back'} = form_with model: @choice, url: compassion_choices_path, local: true do |f| = f.hidden_field :title = f.hidden_field :option_1 = f.hidden_field :option_2 = f.hidden_field :option_3 = f.hidden_field :option_4 = f.hidden_field :option_5 = f.submit '情けをかける', class: 'ui secondary button'
1つのフォームにまとめたかったのだが、form_withでsubmitボタンによって遷移先を複数に分割させることが出来ない?みたいなので2つのフォームに分割。 だいぶ冗長なので、partial化した方がいいかもしれない。
次からが本番
2、compassionページのビューを実装
# choices/compassion.html.slim # 通常通りにpathを使ってurlを指定すると、createアクションが発動してしまうのでcontrollerとactionを明示的に渡す。 = form_with model: @choice, url: {controller: 'choices', action: 'compassion_create'}, local: true do |f| # 下の6行は、:titleおよび5つの選択肢をパラメータとして取得するためのものなのでhidden属性にする。 = f.hidden_field :title = f.hidden_field :option_1 = f.hidden_field :option_2 = f.hidden_field :option_3 = f.hidden_field :option_4 = f.hidden_field :option_5 <br><br> <div class='grouped fields'> # f.radio_buttonで各選択肢ごとにラジオボタンを設け、5つの選択肢から1つが選べるように。 .field .ui.radio.checkbox = f.radio_button :primary_option, @choice.option_1, checked: true = f.label @choice.option_1 <br> .field .ui.radio.checkbox = f.radio_button :primary_option, @choice.option_2 = f.label @choice.option_2 <br> - if @choice.option_3.present? .field .ui.radio.checkbox = f.radio_button :primary_option, @choice.option_3 = f.label @choice.option_3 <br> - if @choice.option_4.present? .field .ui.radio.checkbox = f.radio_button :primary_option, @choice.option_4 = f.label @choice.option_4 <br> - if @choice.option_5.present? .field .ui.radio.checkbox = f.radio_button :primary_option, @choice.option_5 = f.label @choice.option_5 <br> = f.submit 'いざ!', class: 'ui secondary button'
どうやって実装するか非常に悩んだが、5つの選択肢(:option_1~5)とラジオボタンで選択した選択肢(:primary_option)を配列にして、gemを使って重みづけを行うのが最適だと考えた。
Choicesテーブルとresultテーブルを分けて実装しておけばもっと簡単だったと後悔。テーブル設計ほどほどに実装始めてしまったのが仇となった。
これにより、submitボタンを押した際に以下のパラメータがchoices_controllerのcompassion_createアクションに渡されるようになる。
# 情けをかける画面で「ラーメン」を選択している Parameters: {"authenticity_token"=>"ANYnfC7SwwI97vg4wd6EcH/WyRbVjCou0+WkZF6UETpoI+TfVts+7d24eM+86SCb2p7JjdyK0kk5n+4+/nIRfQ==", "choice"=>{"title"=>"今日の晩ご飯", "option_1"=>"カレー", "option_2"=>"パスタ", "option_3"=>"ラーメン", "option_4"=>"鍋", "option_5"=>"お寿司", "primary_option"=>"ラーメン"}, "commit"=>"いざ!"}
3、compassion_createアクションを定義する
class ChoicesController < ApplicationController before_action :set_choice_instance, only: [:confirm, :create, :edit, :compassion, :compassion_create] . . def compassion_create decide_with_compassion if params[:back] render :new elsif @choice.save flash[:success] = t 'choices.flash.new decide with compassion' redirect_to result_choice_path(@choice.id) end end private def set_choice_instance return if current_user.nil? @choice = current_user.choices.build(choice_params) end def decide_with_compassion queues = if @choice.option_5.present? { @choice.option_1.to_s => 7, @choice.option_2.to_s => 7, @choice.option_3.to_s => 7, @choice.option_4.to_s => 7, @choice.option_5.to_s => 7, (params[:choice][:primary_option]).to_s => 65 } elsif @choice.option_4.present? { @choice.option_1.to_s => 10, @choice.option_2.to_s => 10, @choice.option_3.to_s => 10, @choice.option_4.to_s => 10, (params[:choice][:primary_option]).to_s => 60 } elsif @choice.option_3.present? { @choice.option_1.to_s => 15, @choice.option_2.to_s => 15, @choice.option_3.to_s => 15, (params[:choice][:primary_option]).to_s => 55 } else { @choice.option_1.to_s => 30, @choice.option_2.to_s => 30, (params[:choice][:primary_option]).to_s => 40 } end randomizer = WeightedRandomizer.new(queues) @choice.result = randomizer.sample end
見辛すぎて申し訳ない。
decide_with_compassion以外は通常のcreateアクション(ランダム抽選機能)とほぼ同じなので割愛。
今回は、if @choice.option_4.present?(選択肢が4つある場合)を例に簡単に解説する。
def decide_with_compassion #ハッシュの中で各選択肢を重みづけし、queuesに格納する queues = { @choice.option_1.to_s => 10, @choice.option_2.to_s => 10, @choice.option_3.to_s => 10, @choice.option_4.to_s => 10, (params[:choice][:primary_option]).to_s => 60 } # gem:WeightedRandomizerが自動的に、queuesを元にして重み付けされた配列を作ってくれる。 randomizer = WeightedRandomizer.new(queues) # 後はそれをsampleメソッドによって抽選し、@choice.resultに格納。 @choice.result = randomizer.sample end
params[:choice][:primary_option]).to_s => 60
が、70ではないのが腑に落ちないかもしれないが、@choice.option_1~4のうちのどれか1つがparams[:choice][:primary_option])
と重複しているため、合計数を70にすることが重要。
コード自体は非常に冗長になってしまうが、これを応用すれば選択肢が何十個あっても好きな確率に設定可能。
今回お世話になったgem: Weighted Randomizerについては公式のGitHubをご参照ください。
GitHub - ryanlecompte/weighted_randomizer: Provides a common utility for weighted randomization
まとめ
実はこの機能、このサービスを作ることを決意した時からもっとも実装したかった機能です。
日常生活で、「こっちの商品の方がいいんだけど、こっちも少し捨てがたいな〜」って時が結構あるからそこを補いたいなと思っていました。
似たようなサービスはいくつかあるものの、この機能を実装しているサービスは1つもなかったですし、ここが決断くんのこだわりポイントです。
どうやってパラメータ渡そうかとか、渡した後はどうやって重み付けしようかとか、正直かなり悩みました。
テーブル設計の甘さと技術不足が祟り、コードは結構ぐちゃぐちゃになってしまいましたが自分の力だけで実装できて良かったし、自信に繋がりました。
もしもっといい実装方法あればコメントなどで教えていただけると幸いです。