【ruby】rubyで2進数を扱う
AtCorer Beginner Contest 147 C問題
毎度おなじみAtCorder Beginner Contest C問題でつまづいた。
解き方含めて学んだことをメモしておく。
問題のポイント
今回の問題では、正直者を1, 嘘つきを0で表記していく。
例えば3人が順に
[正直者, 嘘つき, 正直者] = [1, 0, 1]
[正直者, 嘘つき, 嘘つき] = [1, 0, 0]
といった形になる。
正直者と嘘つきの組み合わせは 2 ** 3で8通りあるわけだが、どうやって表記するかがポイントとなる。
ここで、eachメソッドとtimesメソッドを使うと一気に楽になる。
eachとtimesで2進数表記
3人の時は2 ** 3通り考えるのでn人の時はコレで8回繰り返せる。
(0..2 ** n - 1).each do |k| puts k # 表示されるのは0 ~ 7 end
コレを2進数化するにはtimesを使ってやる。
n = 3 (0..2 ** n - 1).each do |k| n.times do |j| # 数字kを2進数表記した時、n桁の数列のj番目の数字 puts k[j] end end # 結果出てくるのはこんな数字 # 0 0 0 1 0 0 0 1 0 1 1 0 0 0 1 1 0 1 0 1 1 1 1 1 # 3つずつくくるとこんな感じに2進数に変換される # [0 0 0][1 0 0][0 1 0][1 1 0][0 0 1][1 0 1][0 1 1][1 1 1]
解き方
とりあえず、入力する内容を受け取る準備をしておく。
n = gets.to_i # 全員が言ったことをまとめるsays配列を作成 says = Array.new n.times do i = gets.to_i # 各人が言ったとをまとめるwords配列を作成 words = Array.new i.times do person, truth = gets.split.map(&:to_i) words << [person - 1, truth] end says << words end
続いて、先ほどの2進数の考え方を使ってそれぞれのパターンで成立するかどうかを確認していく。
(0..2 ** n - 1).each do |mask| # まずは組み合わせが正しいと仮定しておく success = true n.times do |k| # 0は嘘つきなので評価せず次に飛ばす next if mask[k] == 0 # j番目の人が言っていることをピックアップ words = says[k] words.each do |person, truth| # mask[k]=1なので、wordsのtruthが正しくなければ仮定をsuccess = falseとする if truth == 1 success = mask[person] == 0 ? false : true else success = mask[person] == 1 ? false : true end # success = falseならそのあと調べる必要がない break unless success end end
コレでどの組み合わせが成立しているかがわかるので、最後に一番正直者が多い組み合わせを調べる。
普通の数字を2進数表記に変えるにはto_s(2)
を使ってやれば良い。
ans = 0 (0..2 ** n - 1).each do |mask| success = true n.times do |k| next if mask[k] == 0 words = says[k] words.each do |person, truth| if truth == 1 success = mask[person] == 0 ? false : true else success = mask[person] == 1 ? false : true end break unless success end if success # 成立していたら、2進数化して1の数を数えて、大きい方をansに置き換える。 ans = [ans, mask.to_s(2).count('1')].max end end
まとめ
今までrubyの問題で2進数を使った事がなかったのでがっつり引っかかってしまった。
プロコンだとよく使う手法のようなので、覚えておいて損はないはず。
【rails】widthではなくデバイス判定の時のRspec
browser gem
ボタンの表示・非表示の制御をcssのwidthでやることも可能だが、gemを使って判別させることも可能。
そこで使うのがbrowser
というgem。
gem 'browser'
コレでPCかスマホかの判別はもちろん、使用しているブラウザが何かも判別できる。
# スマホかどうかの確認。 browser.device.mobile? # ieか確認 browser.ie?
最近はchromをメインに開発しているのが、ieやedgeは挙動が異なることもあるのでコレを使うことで挙動の制御をしやすくなる。
Rspecをどうかくか問題
cssのwidthで制御しているのであればfeatureスペックを書くときにはサイズの変更をする必要がある。
Capybara.current_session.driver.browser.manage.window.resize_to(1200, 500) # 1200がwidth, 500がheght
ところが、browser
を使っているとサイズを変更したところでテストしているブラウザがPCだと変更する事ができない。
なので、User Agent
を上書きする必要がある。
context 'ボタンの表示' do before do # 今回はひとまずiPhoneで設定 user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/38.0.2125.67 Mobile/12B411 Safari/600.1.4' Capybara.current_session.driver.header('User-Agent', user_agent) end context 'ボタンの表示' do ブラウザによって出し分ける確認 end end
まとめ
最近はレスポンシブデザインが基本になってきているからブラウザ判断はあまり必要ないかもしれないけど、写真撮影のボタン出し分けとかはスマホ特有の機能だったりするのでfeatureスペックを書くときには必要になるかも。
【rails】rspecでcallbackにハメられたお話
callbackとは
DBにデータを保存する前やバリデーションをかける前に処理を行うこと。
基本的にmodelの中に書かれる。
callbackにはcontrollerで持たせる必要はないが、保存処理などを行う前に必要な処理をまとめて書いておく。
実際の例
まずはOrderModel
を書いてみる。
大体こんな書きっぷりになるはず。
class Order < ApplicationRecord before_save: set_status, set_applied_at enum status: { applied: 0, canceled: 1, modified: 2 } def set_status status = 0 end def set_applied_at applied_at = Time.current end # この下にschemaが書かれる end
その時のOrdersController
は大体こんな書きっぷり。
class OrderController < ApplicationController def create @order = Order.new(order_params) @order.save end private def order_params params.require(:order).permit(:name, :phone_number, :email) end end
callbackの分割
callbackにもたせたいメソッドが増えてくるとmodelが見辛くなるのでmoduleに分割しておく。
上の例を分割するとこんな感じ。また、statusの項目を少し増やしてみる。
まずはOrderModel
から。
class Order < ApplicationRecord include OrderCallback enum status: { applied: 0, canceled: 1, modified: 2, in_progress: 3, auto_canceled: 4 } # この下にschemaが書かれる end
続いて分割したcallbackをOrderCallback
moduleで記載する。
module OrderCallback include do before_save: set_status, :set_applied_at end def set_status status = phone_number.present? ? 0 : 3 end def set_applied_at applied_at = (status = 0) ? Time.current : "" end end
今回はphone_numberが記入されていたらstatusをapplied、それ以外はin_progressとした。
さらにstatusによってcreateで描画するviewの出し分けを設定しておく。
class OrderController < ApplicationController before_action :check_status def create @order = Order.new(order_params) @order.save # appliedとin_progressというviewがあるとする render @order.status = 0 ? "applied" : "in_progress" end def show end private def check_status @order = Order.find(id) case status when 3 redirect_to order_show_path else redirect_to order_index_path end end def order_params params.require(:order).permit(:name, :phone_number, :email) end end
ハマりポイント、skip_callback
と、ここまではいたって普通なこと。ハマる部分は見あたらない。
(実はcallbackに記載した設定を忘れてハマることも多々あるが…)
ハマったのはrspecで使うskip_callback
だった。
今回のメソッドの中に、statusをauto_canceledにするものがないので強制的に設定してあげる必要がある。
しかし、phone_numberを空にしてupdateかけるとbefore_save
でset_status
が流れてしまいstatusはin_progressになってしまう。
そこで使うのがskip_callbackメソッド
。
書き方はこんな感じ。
describe OrderController, type: :controller do let(:order) do Factorybot.create(:order, { name: "hoge" phone_number : "090-0000-0000" email: "hoge@hoge.com" }) end describe "orderのstatusが" do context "in_progress" do order.update(phone_number: "") get :show expect(response).to redirect_to order_show_path end context "auto_canceled" do # 引数はアクション(save, validation)、タイミング(before, after)、メソッド名の順番で記載 order.skip_callback(:save, :before, :set_status) order.update(phone_number: "", status: 4) order.set_callback(:save, :before, :set_status) expect(response).to redirect_to order_index_path end end end
set_callback
の後でbinding.pryをかけてorderを見ればstatusがauto_canceledになっているのがわかる。
なので、本来はここでもハマることはない。
ヤサゴリがハマったのは別のrspecテスト
現場では山のようなテストコードが存在しており、rspecを全て流すとそこそこの時間がかる。
そして、そのrspecが流れる順番は上から順番とは限らない。
実際、ヤサゴリがハマったのは全く別のfeature_specだった。
skip_callback
の設定が他のfeature_specに影響を及ぼしており、そのせいでテストがコケていたのだった。
skip_callbackは極力使わない方針で
テストの量が多いと、流れる順番がわからないのでskip_callback
がどこで悪さをするかわからない。
また、今回通ったとしても次に修正を加えた時に悪さを起こす可能性が否めないので、skip_callback
は使わないことにした。
今回のコードでいうとset_status
に1行追加することで回避。
def set_status return if status == 4 status = phone_number.present? ? 0: 3 end
というか、1行追加するだけで回避できるなら最初からそっちにせいという話でもある…
【rails】rspecで例外処理をテストする方法
例外処理とは
例外処理は通常のフローでエラーが発生した時にどうするか、ということ。
通常のsaveとかでは以下のようにif節
でsaveできない時を指定できる。
def index @orders = Order.all end def new @order = Order.new end def create if @order.save redirect_to order_index_path else flash[:error] = "正しく入力してください" redirect_to order_new_path end end
ところが、Mailerとかだと基本的に遅れることが前提になっているためエラーの検知が難しい。 そこで、例外処理を入れておく。
例外処理の書き方
例えばOrderModel
でorderをcreateできたらthanksメールを送るとする。
その際のコードはこんな感じ。
class OrdersController < ApplicationController def index @orders = Order.all end def new @order = Order.new end def create if @order.save ::OrderMailer.thanks(@order).deliver redirect_to order_index_path else flash[:error] = "正しく入力してください" redirect_to order_new_path end end end class OrderMailer < ApplicationMailer def thanks(order) ... # ここはメールのオプションとかを記載(今回は省略) end end
ここで、メールを送る動作の時にエラーが起こると本来index
にリダイレクトするところがエラー描写になってしまう。
そこで、ここに例外処理を追加しておく。
def create if @order.save begin ::OrderMailer.thanks(@order).deliver rescue => e # eはエラーの内容を表示。 flash[:error] = "メールの送信に失敗しました。登録は完了しています。" end redirect_to order_index_path else flash[:error] = "正しく入力してください" redirect_to order_new_path end end
上ではメールが送れなかった時にindexページで送信失敗のフラッシュを表示させている。
rescue => e
ではエラーの内容をrescue
内で使えるようにしている。Bugsnagとかでエラー検知している場合はそれを送ることも可能。
例外を起こす
通常ローカル環境でメール送信のエラーを起こすことはできないので、確認のためには強制的に発生させる必要がある。
そこで使うのがraise
コマンド。
def create if @order.save begin raise ::OrderMailer.thanks(@order).deliver rescue => e # eはエラーの内容を表示。 flash[:error] = "メールの送信に失敗しました。登録は完了しています。" end redirect_to order_index_path else flash[:error] = "正しく入力してください" redirect_to order_new_path end end
これでbegin節内で擬似的にエラーを起こすことができる。
rspecで例外処理を起こしたい
さて本題。ローカル環境でエラーを起こせないということはrspecでも通常はメール送信エラーは起こらない。
そこで、強制的にエラーを発生させる必要がある。
RSpec.describe OrdersController, type: :controller do context 'createページに関して' do let(:order) = { FactoryBot.attributes_for(:order) } describe 'thanksメールの送信成功' do before do order_mailer = double("order_mailer") expect(OrderMailer).to receive(:thanks).with(instance_of(Order)).once.and_return(order_mailer) expect(order_mailer).to receive(:deliver).once end post: create expect(response).to redirect_to(order_index_path) end # 例外処理 describe 'thanksメールの送信失敗' do before do order_mailer = double("order_mailer") allow(OrderMailer).to receive(:thanks).and_raise(RuntimeError) end post: create expect(response).to redirect_to(order_index_path) end end end
通常はモックを作ってexpectでreceiveさせるところをallow.toでand_raise
で強制的にraise
コマンドを発生させている。
Mailerで例外処理が発生した場合e.class = RuntimeError
となるので、今回はそれを記載しておいた。
まとめ
今回はメールで例外処理を起こしたが、URLのパラメータなんかはユーザー操作で変えることができてしまうので想定していない挙動を起こすこともあり得る。 そんな時は例外処理で対応を記載しておくとユーザーにエラーページを見せずにページ遷移ができる。
【ruby】数学問題でよく使うメソッドその1
AtCorderの問題を解いていると数学問題がよく出てくる。そこで使うメソッド類をまとめておく。
primeモジュール
素数を扱う時に使うのがprimeモジュール
。コードの最上段にこれを書いておく。
require 'prime'
素数
# 小さい方からn個の素数 Prime.take(5) => [2, 3, 5, 7, 11] # n以下の素数を列挙 Prime.each(15).to_a => [2, 3, 5, 7, 11, 13] # n以下の最大の素数 Prime.take_while{|v| v < 100}.last => 97 # n以上の最小の素数 Prime.find{|v| v >= 100} => 101 # nが素数かチェック Prime.prime?(11) => true 124.prime? => false
素因数分解
# nを素因数分解 Prime.prime_division(36) => [[2, 2], [3, 2]] # 2^2, 3^2 # nの約数の個数 # n = a^x * b^y * c^z なら # 個数は (x + 1)(y + 1)(z + 1) Prime.prime_division(36).map{|k, v| v + 1 }.inject(:*) => 9
injectメソッド
num = [4, 3, 2, 1] num.inject(:+) => 10 # 要素を1つ目から順に足していく num.inject(5, :+) => 15 # injectで渡された値に要素を1つ目から順に足していく
上では足し算のみでやったが、四則演算は全て同じ。
約数の列挙
Qiitaで@scivolaさんの解がコンパクトで良い感じでした。
require "prime" def divisors(n) n.prime_division.inject([1]) do |ary, (p, e)| (0..e).map{ |e1| p ** e1 }.product(ary).map{ |a, b| a * b } end.sort end division(36) => [1, 2, 3, 4, 6, 9, 12, 18, 36]
三角関数
# 円周率 MATH::PI => 3.141592653589793 # 角度をラジアンに deg = gets.to_i puts deg * Math::PI / 180 # 三角関数 Math.sin(rad) Math.cos(rad) Math::tan(rad) # tanだけ違うから注意 # 逆三角関数 Math::asin(rad) # asinだけ違うから注意 Math.acos(rad) Math.atan(rad)
まとめ
滅多に使うことはないけど(それこそプロコンくらい…)、いざ出てきた時に困るのでまとめてみた。
【ruby】第2回全国プログラミング王決定戦予選で出てきた謎問題
全国プログラミング王決定戦予選とは
ヤサゴリがいつもやってるAtCorderで行われるコンテストの1つ。
産経主催のコンテストで通常のAtCorderと少しテイストが異なる。
初チャレンジで見事どハマりしたから、忘れないようにメモ。