【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をOrderCallbackmoduleで記載する。

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_saveset_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と少しテイストが異なる。
初チャレンジで見事どハマりしたから、忘れないようにメモ。

続きを読む