# Basic
%!STRING! : ダブルクォート文字列
%Q!STRING! : 同上
%q!STRING! : シングルクォート文字列
%w!STRING! : 要素が文字列の配列(空白区切り)
%W!STRING! : 要素が文字列の配列(空白区切り)。式展開、バックスラッシュ記法が有効
%w!credit debit_ap fee tx_fee!.map(&:to_sym) シンボルの配列

strftime("%F") 2015-01-07
strftime("%Y/%m/%d %H:%M:%S") 2015/01/07 21:12:32

puts [a, b, c, d].join(' ')

# 文字コード
Encoding.default_external = Encoding::UTF_8

# Nokogiri
doc = Nokogiri::HTML(open(url).read)
doc.css('p.parent > text()') node text without children
node['attr']

# Mechanize
agent = Mechanize.new
# agent.log = Logger.new(STDOUT)

agent.get("https://example.com/login")
form = agent.page.forms.first
form["username"] = config["username"]
form["password"] = config["password"]
form.submit

agent.get("https://example.com/target")
doc = Nokogiri::HTML(agent.page.body)
data = parse_page(doc)

# Active support
gem 'activesupport'
require 'active_support/core_ext'
Time.now + 2.months

# mongoid
class Earning
  include Mongoid::Document
  include Mongoid::Timestamps::Created

  field :title, type: String
  field :open_at, type: Time
  field :total, type: Float

  index({ created_at: 1 }, { name: "created_at_index" })
end
Earning.create_indexes

team = Team.find_or_initialize_by(name: @team_name)
team.content = params[:content]
team.save!

# haml
%table.table{ style: 'width: 40%' }
  %tr
    %td 最終更新時刻
    %td= @last_update.strftime('%Y/%m/%d %H:%M:%S')
  %tr
    %td ツイート数
    %td= @tweet_count
  %tr
    %td 削除イベント数
    %td= @delete_count

JSON

symbolize_namesを入れるとパターンマッチングと相性がいい
キーがシンボルでも、to_jsonすると文字列に変換される

JSON.parse(File.read("hoge.json"), symbolize_names: true)

正規表現

str[regexp] 最初にマッチした部分文字列

%r|RegExp| 正規表現リテラル
%r|OPEN\s+(?<open_at>\d+:\d+)\s+/\s+START\s+(?<close_at>\d+:\d+)| 名前付きキャプチャ

# 複数のマッチを変数に分解
# 参考: https://docs.ruby-lang.org/ja/latest/method/Regexp/i/match.html
_, foo, bar, baz = */(foo)(bar)(baz)/.match("foobarbaz")

パターンマッチング

2.7と3.0で仕様変更あり。以下は3.0(inがboolを返す)

irb(main):007:1* if {a: 42, b: 24, c: 12} in {a:, b:, c:}
irb(main):008:1*   puts a, b, c
irb(main):009:0> end
42
24
12

マッチしない場合に例外を出したければ => を使う

irb(main):021:0> {a: 42, b: 24} => {a:, b:, c:}
irb(main):022:0> puts a, b, c
(irb):21:in `<main>': {:a=>42, :b=>24} (NoMatchingPatternError)

clockwork

skip_first_runおすすめ。起動直後のジョブ実行を行わない。デバッグ中に何度も起動した場合に、APIのレートリミットを使い果たすのを避けられる

every(10.minutes, "crawl_home", :skip_first_run => true)

お手軽にテストを書く

http://d.hatena.ne.jp/shingo-zukunashi/20121010/1349868645

if __FILE__ == $0
  #テストコード
end

struct

Hashよりメンバが明示的でいい
【Ruby】Struct(構造体クラス)を理解する - Qiita

外部プロセスを並列実行

require "open3"
require "thwait"

class Runner
  def initialize
    @failed_cmds = []
  end

  def run_parallel(cmds)
    contexts = []
    threads = []

    cmds.each do |cmd|
      stdin, stdout, stderr, wait_thread = *Open3.popen3(cmd)
      stdin.close
      contexts << {
        "cmd" => cmd,
        "stdout" => stdout,
        "stderr" => stderr,
        "wait_thread" => wait_thread,
      }
      threads << wait_thread
    end

    ThreadsWait.all_waits(*threads)

    ret = true

    contexts.each do |context|
      puts context["cmd"]
      puts context["stdout"].read.encode(Encoding::UTF_8, Encoding::Shift_JIS)
      puts context["stderr"].read.encode(Encoding::UTF_8, Encoding::Shift_JIS)

      unless context["wait_thread"].value.success?
        @failed_cmds << context["cmd"]
        ret = false
      end
    end

    ret
  end

  attr_reader :failed_cmds
end

runner = Runner.new
runner.run_parallel(cmds)

if runner.failed_cmds.empty?
  puts "全コマンドの実行に成功しました"
else
  puts "下記のコマンドの実行に失敗しました"
  puts runner.failed_cmds.join("\n")
end