たんたんとがんばる

ruby, Ruby on Rails, React etc.

シリーズGemを読む(Timecop)

Timecopとは?

TimecopはRuby/Railsで時間関連のテストをする場合に便利なGemで、例えば、freezeメソッドを使うとブロック内かTimecop.returnが呼ばれるまで「指定した時間に停止させておく」ことができます。
他にも、指定した時間に進めるtravelメソッドや時間を速めたり(2倍速とか)できるscaleメソッドを持ちます。
今日はそのGemをざっくりと読み進めていきます。

Timecop.freeze(Date.today + 30) do
  assert joe.mortgage_due?
end

ファイル構成

  • lib/timecop.rb(Timecopクラス) ... インターフェースクラス
  • lib/time_extensions.rb(TimeExtensionクラス) ... TimeやDateTime、Dateのモンキーパッチクラス
  • lib/time_stack_item.rb(TimeStackItemクラス) ... ダミーのTimeやDateTime、Dateインスタンスの情報を保持するクラス
  • lib/version.rb ... Gemのバージョン情報

Timecopクラス

さっそく読んでゆきます。
まずは、TimecopクラスはSingletonモジュールをインクルードしており、シングルトンであることが保証されています※

Timecop.travelを例にとって説明してゆきます(他メソッドも同じ流れです)。
Timecop.travelは:travelを引数にsend_travelメソッドを呼び出し(freezeの場合は:freezeが引数になる)、send_travelメソッドはprivateのtravelメソッドを呼び出します。
privateのtravelメソッドは、TimeStackItemクラスのインスタンスを初期化します。
そしてそのインスタンスを自身で配列として管理します(その配列でTimecopのブロックをネストして呼べるようにです)。

  def travel(*args, &block)
    send_travel(:travel, *args, &block)
  end

  private
  (省略)
  def travel(mock_type, *args, &block) #:nodoc:
    raise SafeModeException if Timecop.safe_mode? && !block_given?

    stack_item = TimeStackItem.new(mock_type, *args)

    stack_backup = @_stack.dup
    @_stack << stack_item

    if block_given?
      begin
        yield stack_item.time
      ensure
        @_stack.replace stack_backup
      end
    end
  end

TimeStackItemクラス

TimeStackItemクラスの冒頭に、コメントでこうあります。

# A data class for carrying around "time movement" objects. Makes it easy to keep track of the time
# movements on a simple stack.

自分なりに解釈すると、ダミーのTime/DateTime/Dateインスタンスを作るためのオブジェクトやメソッドが集められているクラスです。
Time.travelの例で言うと、

  • Time.travel(10.days.ago)が与えられたら10.days.agoのTimeオブジェクトを作り(@time)
  • それが「本当の」時間とどれから離れているかを計算し(@travel_offset)
  • @timeと@travel_offsetからダミーのTimeオブジェクトを返すメソッドを作ったりします(def time)

ここではTimeの操作だけを例にとりましたが、DateTime、Dateも同様です。

     def initialize(mock_type, *args)
        raise "Unknown mock_type #{mock_type}" unless [:freeze, :travel, :scale].include?(mock_type)
        @travel_offset  = @scaling_factor = nil
        @scaling_factor = args.shift if mock_type == :scale
        @mock_type      = mock_type
        @time           = parse_time(*args)
        @time_was       = Time.now_without_mock_time
        @travel_offset  = compute_travel_offset
      end

      def time(time_klass = Time) #:nodoc:
        if @time.respond_to?(:in_time_zone)
          time = time_klass.at(@time.dup.localtime)
        else
          time = time_klass.at(@time)
        end

        if travel_offset.nil?
          time
        elsif scaling_factor.nil?
          time_klass.at(Time.now_without_mock_time + travel_offset)
        else
          time_klass.at(scaled_time)
        end
      end

TimeExtensionクラス

さて、TimeStackItemクラスでダミーのTimeやTimeオブジェクトを生成するメソッドを作ったので、あとはこのクラスでTimeなどのnowやnewやparseメソッドをTimeStackItemクラスのメソッドを呼び出すようにパッチしてしまえば良いだけです!

class Time #:nodoc:
  class << self
    def mock_time
      mocked_time_stack_item = Timecop.top_stack_item
      mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.time(self)
    end

    alias_method :now_without_mock_time, :now

    def now_with_mock_time
      mock_time || now_without_mock_time
    end

    alias_method :now, :now_with_mock_time

    alias_method :new_without_mock_time, :new

    def new_with_mock_time(*args)
      args.size <= 0 ? now : new_without_mock_time(*args)
    end

    alias_method :new, :new_with_mock_time
  end
end
>||

timecopを読む(1)

今日はもう遅いのでざっくりとメモ

libの下のファイル構成は以下になっていて、以下の役割を持つ

  1. time_extensions.rb は、Timeのモンキーパッチクラス
  2. time_stack_item.rbは、freezeやtravelの操作にしたがって、Timeを操作するクラス
  3. timecop.rbは、インターフェースのクラス

moduloとdivisionの意味

active_support/core_extを読み進めているが、今日はあまりに眠いのでこれだけ。

  • modulo => 割り算の余り、剰余 メソッドはNumeric#modulo
  • division => 割り算の結果、商 メソッドはNumeric#div

active_support/core_ext/array/extract_options.rbを読む

こちらもメソッドは2つだけ(実質はexctract_options!のみ)なので軽い。
extractable_options?メソッドがあることで現在は厳密にHashのみがoptionとして利用できるとわかる(Hashのサブクラスは不可)。
直接Hashかどうかを訊かないことで、Hash以外もオプションとして利用できることを将来的に想定されているのだろうか。
instance_ofは厳密にそのクラスのインスタンスであることを確認するメソッド、対してis_aはサブクラス、もしくはモジュールをインクルードしたクラス(そのサブクラス)も許容する。
popは最後の要素を取り出し、ちなみに逆はshift。

  def extractable_options?
    instance_of?(Hash)
  end
...
  def extract_options!
    if last.is_a?(Hash) && last.extractable_options?
      pop
    else
      {}
    end
  end

ActiveSupport/core_ext/array/access.rbを読む

ActiveSupport/core_extをarrayに限らずいくつかのファイルを読んで気づいたことは、どのメソッドも短く、簡潔に書かれており、またコメントで使用例が丁寧に記載されているため、とても読みやすいこと。そのため学習としてはちょっと歯応えが足りないような気もするのだけど、まぁ、とりあえずは暫く読み進めて見よう。
50音順で、まずは、array/access.rbから。

コードについては特筆すべきことはないのだけど、forty_twoメソッドという変わったメソッドがあったためメモしておく。
これはコード通り、配列の42番目の要素を返すメソッドなのだけど、どうしてfifthメソッドの次がsixthではなくてforty_twoなのかというと、
「生命、宇宙、そして万物についての究極の疑問の答え」が42だから、とのことです。

人生、宇宙、すべての答え - Google 検索

# Equal to <tt>self[4]</tt>.
#
# %w( a b c d e ).fifth # => "e"
def fifth
self[4]
end

# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
#
# (1..42).to_a.forty_two # => 42
def forty_two
self[41]
end

settingslogic を読む(2)

Gemを読み始めようと思って書き始めたシリーズ
概要はこの方がまとめてくださっているので、自分が読んで悩んだところやパッとわからなかったことをメモする

qiita.com

  • 前者はArrayやHashのgetter、後者はsetterメソッド
def [](key)
  ...
end

def []=(key, val)
  ...
end
  • 呼び出し方
hoge[:hoge.to_s]
=> "fuga"

hoge[:hoge] = "fuga"
  • define_methodより、class_evalの方が良いらしい(パフォーマンス?)
  • if文と代入を一緒にする

こういうのちょっとかっこいい

@#{key} = if value.is_a?(Hash)
  self.class.new(value, "'#{key}' section in #{@section}")
elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
  value.map{|v| self.class.new(v)}
else
  value
end
irb(main):019:0* hoge = if true
irb(main):020:1>   "true!"
irb(main):021:1> else
irb(main):022:1*   "false!"
irb(main):023:1> end
=> "true!"
irb(main):024:0> hoge
=> "true!"
  • Hashのメソッドを覚えていなかった、replaceとかfetchとか

settingslogic を読む(1)

コードを一望してパッとわからないこと

@instance = new

落ち着いて読むと、これはクラスメソッドなので、レシーバはクラス。
そのため、@instance = new は @instance = self.new と同じ意味。

class << self
...
    private
      def instance
        return @instance if @instance
        @instance = new
        create_accessors!
        @instance
      end
end
</pre>