【rails】ほとんど見当たらないdelayed_jobのrspecまとめ
ことの始まり
前回の記事でdelayed_job
を導入してみた。
yasagori-programing.hatenablog.jp
導入したのはいいものの、rspecの記事がほとんど見つからない…
これは非常に困ったので、そのやり方をまとめておく。
テストの考え方
まずは考え方を整理しておく。
今まではcontrollerでメールの送信を行っていたので、当然controllerのrspecでは次のような確認をしていた。
class OrdersController < ApplicationController def submit ~~~ #ここにはorderを保存する処理とかがいるはず OrderMailer.send(order).deliver end end
RSpec.describe OrdersController, type: :controller do describe 'メールが送信できること' do before do allow(OrderMailer).to receive_message_chain(:send, :deliver) end it 'success' do ~~~ # 前処理は省略 expect(OrderMailer.send).to have_received(:deliver).once end it 'fail' do ~~~ # 前処理は省略 expect(OrderMailer.send).not_to have_received(:deliver) end end end
しかし、delayed_job
でメール送信が非同期化されたのでcontrollerではジョブを登録するメソッドが呼び出されたこと確認をすれば良い。
メールの送信に関する確認はジョブのテストで確認することにする。
controllerのrspecでジョブメソッドの呼び出し確認
delayed_job
を使ったcontrollerのコードはこうなっている。
def submit OrderMailDeliverJob.perform_later(order) end
ここからメソッドの呼び出し確認テストに変更する。
RSpec.describe OrdersController, type: :controller do describe 'メール送信ジョブが呼ばれている' do before do allow(OrderMailDeliverJob).to receive(:perform_later) end it 'success' do ~~~ # 前処理は省略 expect(OrderMailDeliverJob).to have_received(:perform_later) end it 'fail' do ~~~ # 前処理は省略 expect(OrderMailDeliverJob).not_to have_received(:perform_later) end end end
jobのrspec
そして難関。jobのrspecのやり方。
事前にjobのコードがどうなってるかを確認。
class OrderMailDeliverJob < ApplicationJob queue_as :default def perform(order) OrderMailer.send(order).deliver_now end end
class ApplicationJob attr_reader :attempt_number RETRY_LIMIT = 3 rescue_from(StandardError) do |e| if retry_limit_exceeded? Bugsnag.notify(e) else # 上限以下ならリトライ retry_job(wait: wait_time) end end def serialize super.merge("attempt_number" => (@attempt_number || 0) + 1) end def deserialize(job_data) super @attempt_number = job_data["attempt_number"] end private def retry_limit_exceeded? @attempt_number >= RETRY_LIMIT end def wait_time attempt_number**2 end end
確認したいのはひとまずメール送信の成功と、リトライ含めて失敗したら通知が飛ぶの2パターン。
下準備としてrails_helper
に次のコードを追加しておく。
RSpec.configure do |config| config.include ActiveJob::TestHelper end
まずはメール送信成功のパターンから。
require 'rails_helper' RSpec.describe OrderMailDeliverJob, type: :job do after do Delayed::Job.delete_all # test用のテーブルを空にしておく end context 'メール送信' do before do allow(OrderMailer).to receive_message_chain(:send, :deliver_now) end it '成功' do expect(OrderMailer.send).to receive(:deliver_now).once OrderMailDeliverJob.perform_later(order) # orderはFactoryBotとかで先に作っておく必要あり end end end
いよいよメール送信失敗のパターン。
確認したいのはリトライが設定回数行われていることと通知が飛んでいること。
context '規定回数メール送信失敗' do let(:retry_count) { ApplicationJob::RETRY_LIMIT } before do allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError) allow(Bugsnag).to receive(:notify) allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0) end it 'bugsnagに通知' do expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times expect(Bugsnag).to receive(:notify).once OrderMailDeliverJob.perform_later(order) end end
ジョブの登録・実行を自分で設定したい
rails_helper
に追加したActiveJob::TestHelper
は賢いのでperform_laterでジョブの実行までやってくれる。
逆にいうとジョブが実行されてしまうので、ジョブが登録されているかの確認ができない。
そこであえてActiveJob::TestHelper
を使わない場合は自分でジョブの実行まで行う必要がある。
context 'メール送信' do before do allow(OrderMailer).to receive_message_chain(:send, :deliver_now) end it '成功' do expect(OrderMailer.send).to receive(:deliver_now).once # jobが登録されていることの確認 expect { OrderMailDeliverJob.perform_later(order) }.to change(Delayed::Job, :count).by(1) # jobを実行 Delayed::Worker.new.run(Delayed::Job.first) end end context '規定回数メール送信失敗' do let(:retry_count) { ApplicationJob::RETRY_LIMIT } before do allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError) allow(Bugsnag).to receive(:notify) allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0) end it 'bugsnagに通知' do expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times expect(Bugsnag).to receive(:notify).once OrderMailDeliverJob.perform_later(order) # jobをリトライ回数実行 retry_count.times do job = Delayed::Job.first Delayed::Worker.new.run(job) end end end
まとめ
今回ハマったのはActiveJob::TestHelper
を導入しているのに登録ジョブのカウントを取ろうとしたからだった。
導入しないメリットは例えば「2回失敗したけど、3回目で成功した」みたいなパターンのテストを作ることができる。
とはいえ、そこまでやるのはかなり骨が折れるので、実際は最初に書いたやり方で十分だと思われる。
【rails】delayed_jobを使って非同期化をする方法
ことの始まり
現在運用しているサービスは、メール送信に失敗した際Bugsnagで通知を飛ばすようにしていた。
通知を受けたら手動でメールの再送を行っていたが、利用者数が増えるにつれてエラーもかなりの数発生するようになってきて、手動で対応するのが辛くなってきた。
もうこれは非同期化するしかない!
というわけで、イチから調べて実装することにした。
delayed_jobの導入
非同期化をするには様々な手法があるけど、よくあるdelayed_job
を使うことにした。
まずはGemfile
に追加する。
gem 'delayed_job_active_record' gem 'daemons'
demons
は実際にサーバーで稼働させるのに必要なgem。
delayed_job
はDBを使って動くので、以下のコマンドでテーブルを作ってあげる。
$ bundle install $ bundle exec rails generate delayed_job:active_record $ bundle exec rake db:migrate
続いてconfig/application.rb
でdelayed_job
のキューを登録する。
今回はActive Jobを使うのでdelayed_job
にすれば良い。
class Application < Rails::Application config.active_job.queue_adapter = :delayed_job end
これでdelayed_job
が導入できた。
delayed_jobの実装
例えばorder_mailer.rb
のsend
メソッドでメールを送信する場合、controller側はこんなコードになっている。
class OrdersController < ApplicationController def submit ~~~ #ここにはorderを保存する処理とかがいるはず # メールを送る処理 OrderMailer.send(order).deliver end end
これを非同期化させるには以下のようにすれば良い
def submit OrderMailer.send(order).deliver_later end
delayed_jobを使った繰り返し処理
deliver_later
を使うことで非同期化はできたものの、今のままだと1回失敗したらメールが送られないままになってしまう。
なので、以下の2つを実装していくことにする。
- 1回失敗しても何回かリトライ。
- 最終的に失敗した場合は通知。
そのためにはジョブファイルを作る必要がある。
今回はorderメールのジョブなので、order_mail_deliver_job
とかにしておく。
$ rails g job order_mail_deliver create spec/jobs/order_mail_deliver_job_spec.rb create app/jobs/application_job.rb # これは最初だけ create app/jobs/order_mail_deliver_job.rb
まずはorder_mail_deliver_job.rb
の中身から実装していく。
上で作ったメールを送る処理をこの中に移動。
class OrderMailDeliverJob < ApplicationJob queue_as :default def perform(order) OrderMailer.send(order).deliver_now # ジョブに登録されるのでdeliver_nowを使う end end
controller側に作ったコードを適用しておく。
def submit OrderMailDeliverJob.perform_later(order) # 非同期化なのでperform_laterを使う end
続いてリトライ機能を作成する。
ruby 5.1以上であればretry_on
を使えば楽なのだが、今回はruby 5.0だったので自分で設定が必要だった…
他のジョブを作った場合も考えてapplication_job.rb
にリトライ機能を実装する。
class ApplicationJob attr_reader :attempt_number # 実行回数確認用の変数 RETRY_LIMIT = 3 # リトライさせたい回数 rescue_from(StandardError) do |e| if retry_limit_exceeded? # リトライ回数上限を超えたら通知 Bugsnag.notify(e) else # 上限以下ならリトライ retry_job(wait: wait_time) # 何秒後にリトライするかをwaitに記載 end end def serialize super.merge("attempt_number" => (@attempt_number || 0) + 1) end def deserialize(job_data) super @attempt_number = job_data["attempt_number"] end private def retry_limit_exceeded? @attempt_number >= RETRY_LIMIT end def wait_time attempt_number**2 end end
分割する必要もないので、Bugsnagへの通知もまとめて書いてしまった。
ローカル環境で動作確認
delayed_job
をローカルで確認するにはワーカーを起動させる必要がある。
bin/rake jobs:work
Elastic Beanstalk環境でワーカーの起動
みなさん大好きELB環境ではdeployと同時にワーカーの起動が必要になる。
そのためには..ebextensions
にワーカー起動用のconfigファイルを追加する必要がある。
files: "/opt/elasticbeanstalk/hooks/appdeploy/post/99_restart_delayed_job.sh": mode: "000755" owner: root group: root content: | #!/usr/bin/env bash # Using similar syntax as the appdeploy pre hooks that is managed by AWS # Loading environment data EB_SCRIPT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k script_dir) EB_SUPPORT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k support_dir) EB_APP_USER=$(/opt/elasticbeanstalk/bin/get-config container -k app_user) EB_APP_CURRENT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_deploy_dir) EB_APP_PIDS_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_pid_dir) # Setting up correct environment and ruby version so that bundle can load all gems . $EB_SUPPORT_DIR/envvars . $EB_SCRIPT_DIR/use-app-ruby.sh # Now we can do the actual restart of the worker. Make sure to have double quotes when using env vars in the command. cd $EB_APP_CURRENT_DIR su -s /bin/bash -c "bin/delayed_job -n 2 --pid-dir=$EB_APP_PIDS_DIR restart" $EB_APP_USER
一番最後のコードで-n 2
としているのはワーカーを2つ起動させるオプション。
サーバー上のワーカー起動確認
ジョブのワーカーが動いているかは配布した環境で以下のコードを叩く必要がある。
叩いて何かしら出てきていれば無事ワーカーが稼働していることがわかる。
$ ps aux | grep -v grep | grep delay app ~~~
まとめ
これでdelayed_job
を無事導入することができた。
初めてだったので環境配布は特にビビったけど、問題なく動いてくれた。
メール以外にもファイルのアップロードとかは非同期化ができるので、使い道はひろそうな予感。
なお、導入よりもrspecの方が100倍苦労したけど…長くなったのでそれは次の記事でまとめることにする。
【Rails】rspecでmodelのconcernを単体テストする方法
ことの始まり
結構大きめの複数modelで使うメソッドを作る必要があったから、concernにまとめちゃおうと思ったものの、メソッドの単体テストどうしよう…
ということで、調べたら一応できた。
最近rspec周りの記事が多いなぁ
modelのconcern作成
例えばuser model
があり、first_name
とlast_name
をテーブルに持っているとする。
class User < ApplicationRecord # == Schema Information # # Table name: users # # id :integer # first_name :string # last_name :string end
first_name
とlast_name
を別々に保存はしたいものの、実際に出力する際は一緒にしたfull_name
を使いたい。
他のmodel
でも使うことがありそうだから、concern
に切り分けておく。
module FullNameCreator extend ActiveSupport::Concern private def full_name(str_1:, str_2:) "#{str_1} #{str_2}" # 半角スペースを入れたnameを返す end end
作ったconcern
をmodel
で使う。
class User < ApplicationRecord include FullNameCreator def name full_name(str_1: first_name, str_2: last_name) end end
これで、他のクラス(例えばCustomer model
とか)でも作成したconcern
を使うことができるようになる。
concernの単体テスト
rspecでconcern
のメソッドが期待値通りの動きをしているかの確認をするには、User model
のテストでname
メソッドが期待値を返しているかで確認をすることもできるが、他のmodel
でも確認が必要になる。
複数model
で使っていると面倒なので、concern
で単体テストを実行したい。
分けて書くのは面倒なのでコメントアウトで解説を入れながらコードを書いてみる。
require 'rails_helper' require 'spec_helper' RSpec.describe FullNameCreator, type: :model do # 今回確認したいメソッド describe '.full_name' do # まずはテストに必要なテーブルを作成する before(:all) do m = ActiveRecord::Migration.new m.verbose = false m.create_table :full_name_tests do |t| t.string :first_name t.string :last_name end end # テストが終わったらテーブルを削除 after(:all) do m = ActiveRecord::Migration.new m.verbose = false m.drop_table :full_name_tests end # テスト用のモデルを作成(対象のconcernをincludeしておく) class FullNameTest < ApplicationRecord include FullNameCreator end # テスト用のクラスを作成(入力値は後で指定するのでインスタンス変数にしておく) let(:user) { FullNameTest.new(first_name: first_str, last_name: second_str) } # 作成したクラスをテスト用に保存 before {user.save} # テストしたいメソッドをsubjectにする(引数はメソッド名に続けて記載しておく) subject { user.send(:full_name, str_1: first_str, str_2: second_str) } context 'return full name' do # 先ほど使ったインスタンス変数に代入 let(:first_str) { "yasa" } let(:second_str) { "gori" } it { is_expected.to eq("yasa gori") } end end end
これで、concern
の単体テストができる。
今回はメソッドが1つしかないのでメソッドの中でテーブルを作成したが、複数のメソッドを確認する場合は各メソッドごとにテーブルをcreate, dropすると時間がかかるので、まとめてやった方が良い。
まとめ
concern
の単体テストは下準備が非常に多いので実際やる思った以上に面倒だった…
【Rails】取得したlogデータを元にデバッグをする方法
前回のメモ
前回の記事で、logファイルから該当するデータの抽出方法をまとめた。
正確には該当する行の抽出方法だけども。
抽出したデータを元にrailでデバッグする方法をメモしておく。
yasagori-programing.hatenablog.jp
ファイルの読み込み
grep
を使ってデータを抽出しても、それをDBに一つずつ移していくのは面倒。
そこで、rails console
を使ってファイルの読み込みができないか調べたら、出てきたのがFile.Open
。
例えば前回同様yasagori.txt
ファイルを使ってみる。
ファイルの中身はこんな感じ。
hoge fuga piyo
このファイルをrails console
で開くにはこうすれば良い。
File.open('yasagori.txt') do |f| puts f.gets # ファイルの1行目を出力 puts f.gets # ファイルの2行目を出力 end => hoge fuga
それぞれの行に対して何かをしたいときはeach_line
メソッドを使えば良い。
File.open('yasagori.txt') do |f| f.each_line do |line| puts line end end => hoge fuga piyo
rails consoleでアクションを実行
で、本題。
取得したデータを上のFile.open
を使って読めるようになったら、そのデータを元にcontrollerのアクションを動かす必要がある。
そこで使うのが次の方法。
例えばhoge_controller.rb
がこんな内容だったとする。
class HogeController < ApplicationController def index # getアクション, pathは"/hoge" # 省略 end def create # postアクション、paramsが必要。pathは"/hoge/create" # 省略 end end
それぞれのアクションを実行するにはrails console
で次のコマンドを入力すれば良い
# get app.get '/hoge' # post app.post '/hoge/create', {params: ~~~ # 入力したいパラメータ}
コレで指定したアクションの実行が可能となる。
注意ポイント
注意すべきポイントは2つ。
- そのままのコードだとgetの前にidが必要だったりするので、その処理を忘れずに追加しないといけない。
- viewファイルを開こうとするとエラーになるので、
render :nothing => true
を入れておく
ファイルを開いて実行するまで
例えば以下のようなyasagori.txt
ファイルがあったとする。
/hoge/production.log [2020-03-29-00:00:00] INFO -- : [piyopiyo] Parameters : {id: 1, name: "Yasagori"} /hoge/production.log [2020-03-29-00:00:05] ~~~ ~~~
このパラメーター以下のデータをパラメータとしてcreateアクションに渡して叩く場合、こんな感じにすれば良い。
# rails console上で行う File.open('yasagori.txt') do |f| f.each_line do |line| reg = /Parameters : /.match(line) # 正規表現で"Parameters : "を含むそれ以降の文字列を取得 rebuild_params = eval(reg.post_match) # post_matchで"Parameters : "以降の文字列を選びevalでhash化 app.post '/hoge/create', {params: rebuild_params} end end
コレにより、yasagori.txt
に入っていた取得したログデータの回数分createアクションを実行することができる。
使い所
本来ならlogデータやDBの値から抽出できれば良いのだが、アクションの途中で出てきて保存していないデータなどを復元したいときなどに使える。
ちなみに、DBに保存しないデータは新たにlogファイルを作ってそこに入れるようにしてあげれば良い。
# config/development.rbに追記 # another_loggerメソッドを定義。yasagori.logに吐き出す config.another_logger = Logger.new(Rails.root.join('log/yasagori.log')) # hoge_controller.rbに追記 def create ~~~ piyo = ~~~ # DBに保存されないpiyo変数を見たい Rails.application.config.another_logger.info(piyo) end
上の場合はpiyo変数に入った値がyasagori.log
にごっそり出てくる。
まとめ
DBやlogに残らないデータを復元しようとすると、
- logから入力データを復活
- アクションを再び実行
- DBに保存されないから、logファイルを別に作って吐き出す
という手間がかかるので、初期段階で必要になりそうなものはDBに保存するようにするべき。
【Linux】該当文字列を含むファイルを探すgrepコマンド
ことの始まり
サーバー上に残っているLOGから該当の処理を探す必要が出た。
普段サーバーの中からデータを漁ることなんてしなかったのでめちゃくちゃ手間取ったので、忘れないようにメモを残しておく。
grepコマンド
grep
の基本的な使い方はコレ。
grep 正規表現文字列 ファイル名
例えば~/Projects/hoge
ディレクトリ内にあるyasagori.txt
ファイルからfuga
という文字列を含む行探したければこうすればいい。
grep 'fuga' ~/Projects/hoge/yasagori.txt
ここでのfuga
は正規表現で、文字列を含む行を検索している。
1行に書かれている文字列がわかっているなら、シングルクォートは不要。
grepには色々なオプションが存在するので組み合わせると便利。
複数条件で検索
fuga
とpiyo
の2つの文字列を含む行を探すには、パイプで繋いであげれば良い
grep 'fuga' yasagori.txt | grep 'piyo'
含まない検索
'fuga'は含むがpiyo
は含まない行を探すには-v
オプションを使う。
grep 'fuga' yasagori.txt | grep -v 'piyo'
複数ファイルから検索
指定のディレクトリ以下にあるファイル・ディレクトリから検索をしたいときは、次のように書いてあげれば良い。
# Projects以下にあるファイルから検索 grep 'fuga' ~/Projects/*
grepと一緒に使うと便利なコマンド
コレだけでもかなり便利なコマンドだが、組み合わせるとさらに便利な使い方ができる。
findコマンド
find
はファイルを検索するコマンド。
grep
と組み合わせることで「ディレクトリ以下にある一致する文字列を含むファイルから該当の行」をそれぞれ出すことができるようになる。
# Projectsディレクトリ内にある全てのファイル(さらに一階層したのディレクトリ内とかも)から探す grep 'fuga' `find ~/Projects/* -type f`
検索条件が1つならfind
を先頭に持ってきたこの書き方の方が便利。
find ~/Projects -type f -exec grep "fuga" {} \;
便利な理由は、「出力結果にフォルダ名とかを出さないで済む」から。
wcコマンド
wc
コマンドは行数・単語数・バイト数を数えるコマンド。
grep
した結果が多い場合、コレを使うことで該当箇所がいくつあるのかわかる。
grep 'fuga' `find ~/Projects/* -type f` | grep -v 'piyo' | wc => 33695 2462550 53006581 # この場合該当箇所が33695行あるということ。
teeコマンド
tee
は出力しながら指定したファイルに書き出すコマンド。
grep
で複数条件検索すると通常の>
で書き出せないのでコレを使った。
pwd => ~/Projects grep 'fuga' `find ~/Projects/* -type f` | grep -v 'piyo' | tee yasagori_2.txt # ~/Projects/yasagori_2.txtが新たに作成されてgrepの検索結果が中に保存される
まとめ
普段あまり使ってこなかったgrepコマンド、調査とかを行うときはめっちゃ便利。
というか使えないと話にならない…
【rails】rspecでcontrollerのconcern単体テストをやる方法
ことの始まり
巨大なcontrollerを分割した際、共通で使うメソッドはconcernにまとめておこうと考え。
controllerの単体テストでわざわざconcernの内容も一緒にやるのは馬鹿らしいので、concernの単体テストをやりたくなったが、ちょっと困ったので色々調べた。
せっかくなので、忘れないようにまとめておこう。
ベースとなるconcern
今回作ったのはこんな感じのconcern。
module HogeConcern extend ActiveSupport::Concern def fuga(order) # ハッシュを返し、nameキーに'maccho_'をつける end def piyo piyo_params = params.permit(~~) # permitの中は省略 # 頭に'maccho_'をつけたstrを返す end end
fugaメソッド
は引数にorder
を持っている。
一方で、piyoメソッド
はpost時に渡されたparamsをそのまま使っている。
引数がある場合
まずは引数がある場合から。
とりあえずテストのベースとなる部分を作っておく。
require 'rails_helper' describe HogeConcern do context '.fuga' do let(:order) do FactoryBot.buid(:order, {name: hoge_name}) end subject { fuga(order) } context 'hoge_name' do let(:hoge_name) { 'yasagori' } it { is_expected.to eq({ name: 'maccho_yasagori' }) } end end end
当然、このままだと通らない。
まずは、struct
を使ってテスト用のクラスを作る。
クラスを作る際に、HogeConcern
を忘れずにincludeしておく。
そして、作成したクラスをnew
する。
context '.fuga' do let(:test_class) { Struct.new(:concern) { include HogeConcern } } let(:concern) { test_class.new } let(:order) do FactoryBot.buid(:order, {name: hoge_name}) end subject { concern.fuga(order) } context 'hoge_name' do let(:hoge_name) { 'yasagori' } it { is_expected.to eq({ name: 'maccho_yasagori' }) } end end
concern自体はcontrollerにincludeして使われているので、単純にメソッドを呼び出しても使えない、ということ。
なのでstruct
を使って擬似的にクラスを作ってあげてからincludeすることでメソッドを呼び出せるようになる。
呼び出すときはsubject
のような書きっぷりが必要(メソッドの呼び出しだから)
post parameterを使う場合
問題はこっち。
controllerがないのにどうやってpost parameterを作ればいいんだ…
結論を先に言うと、擬似的にcontrollerを作ってあげる必要があった。
describe HogeConcern, type: :controller do # typeをcontrollerにする context '.piyo' do controller(ApplicationController) do include HogeConcern def fake_action @piyo = piyo end end before { routes.draw { post 'fake_action' => 'anonymous#fake_action' } } subject { controller.instance_variable_get("@piyo") } context 'piyo_str' do before { post :fake_action, params: { name: 'yasagori' } } it { is_expected.to eq('maccho_yasagori') } end end end
まずcontrollerを設定する。
concernのテストをしたいので、includeを忘れずに。
その中で、呼び出すメソッドを定義しておく。
定義したメソッドはroutes.draw
でrouteの定義付をしてあげる。
subject
はメソッドの中で定義したインスタンス変数を呼び出すためcontroller.instance_variable_get
を使った。
これで、controllerテストの中でconcernのテストを行う必要がなくなる。
まとめ
結果的にconcernの単体テストでpost parameterを呼び出そうとすると、controllerの設定が必要になってしまった。
が、それぞれのcontrollerのなかでテストを行う必要がなくなったので、最終的なコードは短くまとめられ、すっきりさせられた。
【ruby】rubyでよく使うメソッドまとめ その3
よく使うメソッド
実業務でもたまに使うけど、どっちかっていうとAtCorderの問題でよく使うやつをまとめておく。
今までのまとめはこちら
yasagori-programing.hatenablog.jp
文字の出現回数を数えたい
countメソッド
文字列の中から特定の文字をの出現回数を数えるなら'count'メソッドで十分。
s = 'hogehoge' p s.count('h') => 2
じゃあ文字列もってやるとおかしなことになる。
s = 'hogehoge' p s.count('hoge') => 8 # 絶対違う
そんな時はscanメソッド
を使う。
scanメソッド
文字列の中から特定の文字列を抽出し、その配列を返す。
返されるのが配列なので、出現回数を数えるならlength
を使えばOK。
s = 'hogehoge' p s.scan('ho') => ["ho", "ho"] p s.scan('ho').length => 2
配列の処理
任意の数字の配列
1からnまでの数字が入った配列を作るにはこんな書き方ができる。
arry = (1..n).to_a arry = [*1..n]
下の方がシンプルで良いかも。
ダブりをなくす
配列の中から重複をなくすにはuniqメソッド
を使えば良い。
arry = [1, 1, 2, 2, 3] p arry.uniq => [1, 2, 3]
順列・組合せ
permutationメソッド
ある配列の中身の順列を求めるにはpermutationメソッド
を使うと便利。
そのままだとEnumeratorオブジェクトが返ってきてしまうので、.to_a
で返り値を配列に入れる等の処理が必要。
arry = [1, 2, 3] p arry.permutation.to_a => [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]] p arry.permutation(2).to_a #順列の個数を指定できる => [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
combinationメソッド
順列ではなく組合せを求めるにはcombinationメソッド
を使えば良い。
こちらもEnumeratorオブジェクトが返ってきてしまうので、.to_a
等をつける必要がある。
arry = [1, 2, 3, 4] p arry.combination(3).to_a => [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]
配列の何番目かを知りたい
indexメソッド
条件に一致するものが指定の配列の何番目(index)かを調べるメソッド。
arry = [1, 2, 3] lists = arry.permutation.to_a # lists = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]] p lists.index([2,1,3]) =>2 # index番号を返している
数が増えてくるとindexは時間がかかるので、bsearch_indexを使うと良い。
bsearch_indexメソッド
やることはindexと同じだが、二部探索を行うので処理時間が短く済む。
一方で、bsearchするため事前にsortしておく必要がある。
arry = [1, 2, 3] lists = arry.permutation.to_a p lists.bsearch_index([2,1,3]) =>2
繰り返し処理
injectメソッド
rubyでよく使うメソッドまとめ その1でも出てきたが、繰返し処理をスタイリッシュに書くならinjectメソッド
が便利
# 配列オブジェクト.inject {|初期値, 要素| ブロック処理 } nums = [1, 2, 3, 4] p nums.inject {|sum, num| p sum += num } => 1, 3, 6, 10と順に表示 p nums.inject(5) {|sum, num| p sum += num } # 初期値を指定 => 6, 8, 11, 15と順に表示 # 最後の結果だけ取り出すならシンボル表記でOK p nums.inject(:+) => 10 p nums.inject(5, :+) # 初期値を指定 => 15
最小公倍数・最大公約数
最小公倍数
プロコンでよく出てくるが、lcmメソッド
を使えば一発。
複数の値の最小公倍数を使いたい時はinjectメソッド
と組み合わせれば良い。
p 3.lcm(7) => 21 # 3と7の最小公倍数 nums = [2, 3, 7] p nums.inject(:lcm) => 42
最大公約数
gcdメソッド
を使えば一発。
p 25.gcd(30) => 5 nums = [1000, 20000, 200] p nums.inject(:gcd) => 200
まとめ
複数の値の最小公倍数を求めたいときにinjectと組合せると、めちゃくちゃ楽に出すことができる。
処理時間もそこまでかからないし、これは便利。