Active Support コア拡張機能

Active SupportはRuby on Railsのコンポーネントであり、Ruby言語の拡張、ユーティリティ、その他横断的な作業を担っています。

Active Supportは言語レベルで基本部分を底上げして豊かなものにし、Railsアプリケーションの開発とRuby on Railsそれ自体の開発に役立てるべく作られています。

このガイドの内容:

1 コア拡張機能を読み込む方法

1.1 単体のActive Support

フットプリントをほぼ残さないようにするため、Active Supportはデフォルトでは何も読み込みません。Active Supportは細かく分割され、必要な拡張機能だけが読み込まれるようになっています。また、関連する拡張機能(場合によってはすべての拡張機能)も同時に読み込むのに便利なエントリポイントもあります。

したがって、以下のようなrequire文を実行しただけでは、オブジェクトはblank?にすら応答してくれません。

require 'active_support'

この定義がどのように読み込まれるかを見てみましょう。

1.1.1 必要な定義だけを選ぶ

blank?メソッドを使えるようにする最も「軽量な」方法は、そのメソッドが定義されているファイルだけを指定して読み込むことです。

本ガイドでは、コア拡張機能として定義されているすべてのメソッドについて、その定義ファイルの置き場所も示してあります。たとえばblank?の場合、以下のようなメモを追加してあります。

定義はactive_support/core_ext/object/blank.rbにあります。

つまり、以下のようにピンポイントでrequireを実行できます。

require 'active_support'
require 'active_support/core_ext/object/blank'

Active Supportの改訂は注意深く行われていますので、あるファイルを選んだ場合、本当に必要な依存ファイルだけが同時に読み込まれます(依存関係がある場合)。

1.1.2 コア拡張機能をグループ化して読み込む

次の段階として、Objectに対するすべての拡張機能を単に読み込んでみましょう。経験則として、SomeClassというクラスがあれば、active_support/core_ext/some_classというパスを指定することで一度に読み込めます。

従って、(blank?を含む)Objectに対するすべての拡張機能を読み込む場合には以下のようにします。

require 'active_support'
require 'active_support/core_ext/object'

1.1.3 すべてのコア拡張機能を読み込む

すべてのコア拡張機能を単に読み込みたい場合は、以下のようにrequireします。

require 'active_support'
require 'active_support/core_ext'

1.1.4 すべてのActive Supportを読み込む

最後に、利用可能なActive Supportをすべて読み込みたい場合は以下のようにします。

require 'active_support/all'

ただし、これを実行してもActive Support全体がメモリに読み込まれるわけではないことにご注意ください。一部はautoloadとして設定されており、実際に使うまで読み込まれません。

1.2 Ruby on RailsアプリケーションでActive Supportを使う

Ruby on Railsアプリケーションでは、基本的にすべてのActive Supportを読み込みます。例外はconfig.active_support.baretrueに設定した場合です。このオプションをtrueにすると、フレームワーク自体が必要とするまでアプリケーションは拡張機能を読み込みません。また上で解説したように、読み込まれる拡張機能はあらゆる粒度で選択されます。

2 すべてのオブジェクトで使える拡張機能

2.1 blank?present?

Railsアプリケーションは以下の値を空白(blank)とみなします。

  • nilfalse

  • 空白文字 (whitespace) だけで構成された文字列 (以下の注釈を参照)

  • 空欄の配列とハッシュ

  • その他、empty?メソッドに応答するオブジェクトはすべて空白として扱われます。

文字列を判定する述語として、Unicode対応した文字クラスである[:space:]が使われています。そのため、たとえばU+2029 (段落区切り文字)は空白文字と判断されます。

数字については空白であるかどうかは判断されません。特に0および0.0は空白ではありませんのでご注意ください。

たとえば、ActionController::HttpAuthentication::Token::ControllerMethodsにある以下のメソッドではトークンが存在しているかどうかをblank?でチェックしています。

def authenticate(controller, &login_procedure)
  token, options = token_and_options(controller.request)
  unless token.blank?
    login_procedure.call(token, options)
  end
end

present?メソッドは!blank?メソッドと同等です。以下の例はActionDispatch::Http::Cache::Responseから引用しました。

def set_conditional_cache_control!
  return if self["Cache-Control"].present?
  ...
end

定義はactive_support/core_ext/object/blank.rbにあります。

2.2 presence

presenceメソッドは、present?trueの場合は自身のレシーバを返し、falseの場合はnilを返します。このメソッドは以下のような便利な定番の用法があります。

host = config[:host].presence || 'localhost'

定義はactive_support/core_ext/object/blank.rbにあります。

2.3 duplicable?

Ruby 2.5からは、ほとんどのオブジェクトをdupcloneで複製できます。

"foo".dup           # => "foo"
"".dup              # => ""
Rational(1).dup     # => (1/1)
Complex(0).dup      # => (0+0i)
1.method(:+).dup    # => TypeError (allocator undefined for Method)

Active Supportでは、複製可能かどうかをオブジェクトに問い合わせるduplicable?が提供されています。

"foo".duplicable?           # => true
"".duplicable?              # => true
Rational(1).duplicable?     # => true
Complex(1).duplicable?      # => true
1.method(:+).duplicable?    # => false

どんなクラスでも、dupメソッドとcloneメソッドを除去することでこれらのメソッドを無効にできます。このとき、これらのメソッドが実行されると例外が発生します。このような状態では、どんなオブジェクトについてもそれが複製可能かどうかを確認するにはrescueを使う以外に方法はありません。duplicable?メソッドは、上のハードコードされたリストに依存しますが、その代わりrescueよりずっと高速です。実際のユースケースでハードコードされたリストで十分であることがわかっている場合には、duplicable?をお使いください。

定義はactive_support/core_ext/object/duplicable.rbにあります。

2.4 deep_dup

deep_dupメソッドは、与えられたオブジェクトの「ディープコピー」を返します。Rubyは通常の場合、他のオブジェクトを含むオブジェクトをdupしても、含まれている他のオブジェクトを複製しません。このようなコピーは「浅いコピー (shallow copy)」と呼ばれます。たとえば、以下のように文字列を含む配列があるとします。

array     = ['string']
duplicate = array.dup

duplicate.push 'another-string'

# このオブジェクトは複製されたので、複製された方にだけ要素が追加された
array     # => ['string']
duplicate # => ['string', 'another-string']

duplicate.first.gsub!('string', 'foo')

# 1つ目の要素は複製されていないので、一方を変更するとどちらの配列も変更される
array     # => ['foo']
duplicate # => ['foo', 'another-string']

上で見たとおり、Arrayのインスタンスを複製して別のオブジェクトができたことにより、一方を変更しても他方は変更されないようになりました。ただし、配列は複製されましたが、配列の要素はそうではありません。dupメソッドはディープコピーを行わないので、配列の中にある文字列は複製後も同一オブジェクトのままです。

オブジェクトをディープコピーする必要がある場合は次のようにdeep_dupをお使いください。

array     = ['string']
duplicate = array.deep_dup

duplicate.first.gsub!('string', 'foo')

array     # => ['string']
duplicate # => ['foo']

オブジェクトが複製可能でない場合、deep_dupは単にそのオブジェクトを返します。

number = 1
duplicate = number.deep_dup
number.object_id == duplicate.object_id   # => true

定義はactive_support/core_ext/object/deep_dup.rbにあります。

2.5 try

nilでない場合にのみオブジェクトのメソッドを呼び出したい場合、最も単純な方法は条件文を追加することですが、どこか冗長になってしまいます。そこでtryメソッドを使うという手があります。tryObject#sendと似ていますが、nilに送信された場合にはnilを返す点が異なります。

例:

# tryメソッドを使わない場合
unless @number.nil?
  @number.next
end

# tryメソッドを使った場合
@number.try(:next)

ActiveRecord::ConnectionAdapters::AbstractAdapterから別の例として以下をご紹介します。ここでは@loggernilになることがあります。このコードではtryを使ったことで余分なチェックを行わずに済んでいます。

def log_info(sql, name, ms)
  if @logger.try(:debug?)
    name = '%s (%.1fms)' % [name || 'SQL', ms]
    @logger.debug(format_log_entry(name, sql.squeeze(' ')))
  end
end

tryメソッドは引数の代りにブロックを与えて呼び出すこともできます。この場合オブジェクトがnilでない場合にのみブロックが実行されます。

@person.try { |p| "#{p.first_name} #{p.last_name}" }

tryメソッドは、NoMethodErrorを握りつぶして代わりにnilを返す点に注意が必要です。メソッド名の誤りを防ぎたい場合はtry!を使います。

@number.try(:nest)  # => nil
@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer

定義はactive_support/core_ext/object/try.rbにあります。

2.6 class_eval(*args, &block)

class_evalメソッドを使って、あらゆるオブジェクトのsingletonクラスのコンテキストでコードを評価できます。

class Proc
  def bind(object)
    block, time = self, Time.current
    object.class_eval do
      method_name = "__bind_#{time.to_i}_#{time.usec}"
      define_method(method_name, &block)
      method = instance_method(method_name)
      remove_method(method_name)
      method
    end.bind(object)
  end
end

定義はactive_support/core_ext/kernel/singleton_class.rbにあります。

2.7 acts_like?(duck)

acts_like?メソッドは、一部のクラスがその他のクラスと同様に振る舞うかどうかを、ある慣例に沿ってチェックします。Stringクラスと同じインターフェイスを提供するクラスがあり、その中で以下のメソッドを定義しておくとします。

def acts_like_string?
end

このメソッドは単なる目印であり、メソッドの本体と戻り値の間に関連はありません。これにより、クライアントコードで以下のようなダックタイピングチェックを行えます。

some_klass.acts_like?(:string)

RailsにはDateクラスやTimeクラスと同様に振る舞うクラスがいくつかあり、この手法を使えます。

定義はactive_support/core_ext/object/acts_like.rbにあります。

2.8 to_param

Railsのあらゆるオブジェクトはto_paramメソッドに応答します。これは、オブジェクトを値として表現するものを返すということです。返された値はクエリ文字列やURLの一部で利用できます。

デフォルトでは、to_paramメソッドは単にto_sメソッドを呼び出します。

7.to_param # => "7"

to_paramによって返された値を エスケープしてはいけません

"Tom & Jerry".to_param # => "Tom & Jerry"

このメソッドは、Railsの多くのクラスで上書きされています。

たとえば、niltruefalseの場合は自分自身を返します。Array#to_paramを実行すると、to_paramが配列内の各要素に対して実行され、結果が「/」でjoinされます。

[0, true, String].to_param # => "0/true/String"

特に、Railsのルーティングシステムはモデルに対してto_paramメソッドを実行することで、:idプレースホルダの値を取得しています。ActiveRecord::Base#to_paramはモデルのidを返しますが、このメソッドをモデル内で再定義することもできます。以下のコード例があるとします。

class User
  def to_param
    "#{id}-#{name.parameterize}"
  end
end

上のコードから以下の結果を得られます。

user_path(@user) # => "/users/357-john-smith"

コントローラ側では、to_paramメソッドがモデル側で再定義されている可能性があることに常に注意しておく必要があります。上のようなリクエストを受信した場合、params[:id]の値が「357-john-smith」になるからです。

定義はactive_support/core_ext/object/to_param.rbにあります。

2.9 to_query

このメソッドは、エスケープされていないkeyを受け取ると、そのキーをto_paramが返す値に対応させるクエリ文字列の一部を生成します。ただしハッシュは例外です(後述)。以下のコード例があるとします。

class User
  def to_param
    "#{id}-#{name.parameterize}"
  end
end

上のコードから以下の結果を得られます。

current_user.to_query('user') # => "user=357-john-smith"

このメソッドは、キーと値のいずれについても、必要な箇所をすべてエスケープします。

account.to_query('company[name]')
# => "company%5Bname%5D=Johnson+%26+Johnson"

これにより、この結果をそのままクエリ文字列として利用できます。

配列にto_queryメソッドを適用した場合、to_queryを配列の各要素に適用してkey[]をキーとして追加し、それらを「&」で連結したものを返します。

[3.4, -45.6].to_query('sample')
# => "sample%5B%5D=3.4&sample%5B%5D=-45.6"

ハッシュもto_queryに応答しますが、使われるシグネチャが異なります。メソッドに引数が渡されない場合、このメソッド呼び出しは、一連のキー/値ペアをソート済みの形で生成し、それぞれの値に対してto_query(key)を呼び出し、結果を「&」で連結します。

{c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3"

Hash#to_queryメソッドは、それらのキーに対して名前空間をオプションで与えることもできます。

{id: 89, name: "John Smith"}.to_query('user')
# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"

定義はactive_support/core_ext/object/to_query.rbにあります。

2.10 with_options

with_optionsメソッドは、連続した複数のメソッド呼び出しに対して共通して与えられるオプションを解釈するための手段を提供します。

デフォルトのオプションがハッシュで与えられると、with_optionsはブロックに対するプロキシオブジェクトを生成します。そのブロック内では、プロキシに対して呼び出されたメソッドにオプションを追加したうえで、そのメソッドをレシーバに転送します。たとえば、以下のように同じオプションを繰り返さないで済むようになります。

class Account < ApplicationRecord
  has_many :customers, dependent: :destroy
  has_many :products,  dependent: :destroy
  has_many :invoices,  dependent: :destroy
  has_many :expenses,  dependent: :destroy
end

上のコードを以下のように書けます。

class Account < ApplicationRecord
  with_options dependent: :destroy do |assoc|
    assoc.has_many :customers
    assoc.has_many :products
    assoc.has_many :invoices
    assoc.has_many :expenses
  end
end

この手法を使って、たとえばニュースレターの読者を言語ごとに「グループ化」できます。読者が話す言語に応じて異なるニュースレターを送信したいとします。メール送信用のコードのどこかで、以下のような感じでロケール依存ビットをグループ化できます。

I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
  subject i18n.t :subject
  body    i18n.t :body, user_name: user.name
end

with_optionsはメソッドをレシーバに転送しているので、呼び出しをネストすることもできます。各ネスティングレベルでは、自身の呼び出しに、継承したデフォルト呼び出しをマージします。

定義はactive_support/core_ext/object/with_options.rbにあります。

2.11 JSONのサポート

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashOrderedHashProcess::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。

定義はactive_support/core_ext/object/json.rbにあります。

2.12 インスタンス変数

Active Supportは、インスタンス変数に簡単にアクセスするためのメソッドを多数提供しています。

2.12.1 instance_values

instance_valuesメソッドはハッシュを返します。インスタンス変数名から「@」を除いたものがハッシュのキーに、インスタンス変数の値がハッシュの値にマップされます。キーは文字列です。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end

C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}

定義はactive_support/core_ext/object/instance_variables.rbにあります。

2.12.2 instance_variable_names

instance_variable_namesメソッドは配列を返します。配列のインスタンス名には「@」記号が含まれます。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end

C.new(0, 1).instance_variable_names # => ["@x", "@y"]

定義はactive_support/core_ext/object/instance_variables.rbにあります。

2.13 警告や例外の抑制

silence_warningsメソッドとenable_warningsメソッドは、ブロックが継続する間$VERBOSEの値を変更し、その後リセットします。

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

suppressメソッドを使って例外の発生を止めることもできます。このメソッドは、例外クラスを表す任意の数値を受け取ります。suppressは、あるブロックの実行時に例外が発生し、その例外が(kind_of?による判定で)いずれかの引数に一致する場合、それをキャプチャして例外を発生せずに戻ります。一致しない場合、例外はキャプチャされません。

# ユーザーがロックされていればインクリメントは失われるが、重要ではない
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

定義はactive_support/core_ext/kernel/reporting.rbにあります。

2.14 in?

述語in?は、あるオブジェクトが他のオブジェクトに含まれているかどうかをテストします。渡された引数がinclude?に応答しない場合はArgumentError例外が発生します。

in?の例を示します。

1.in?([1,2])        # => true
"lo".in?("hello")   # => true
25.in?(30..50)      # => false
1.in?(1)            # => ArgumentError

定義はactive_support/core_ext/object/inclusion.rbにあります。

3 Moduleの拡張

3.1 属性

3.1.1 alias_attribute

モデルの属性には、リーダー (reader)、ライター (writer)、述語 (predicate) があります。これらに対応する3つのメソッドを持つ、モデルの属性の別名 (alias) を一度に作成できます。他の別名作成メソッドと同様、1つ目の引数には新しい名前、2つ目の引数には元の名前を指定します (変数に代入するときと同じ順序、と覚えておく手もあります)。

class User < ApplicationRecord
  # emailカラムを"login"という名前でも参照したい
  # そうすることで認証のコードがわかりやすくなる
  alias_attribute :login, :email
end

定義はactive_support/core_ext/module/aliasing.rbにあります。

3.1.2 内部属性

あるクラスで属性を定義すると、後でそのクラスのサブクラスが作成されるときに名前が衝突するリスクが生じます。これはライブラリにおいては特に重要な問題です。

Active Supportでは、attr_internal_readerattr_internal_writerattr_internal_accessorというマクロが定義されています。これらのマクロは、Rubyにビルトインされているattr_*と同様に振る舞いますが、内部のインスタンス変数の名前が衝突しにくいように配慮される点が異なります。

attr_internalマクロはattr_internal_accessorと同義です。

# ライブラリ
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# クライアントコード
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

先の例では、:log_levelはライブラリのパブリックインターフェイスに属さず、開発中以外は使われません。クライアント側のコードでは衝突の可能性について考慮せずに独自に:log_levelをサブクラスで定義しています。ライブラリ側でattr_internalを使っているおかげで衝突が生じずに済んでいます。

このとき、内部インスタンス変数の名前にはデフォルトで冒頭にアンダースコアが追加されます。上の例であれば@_log_levelとなります。この動作はModule.attr_internal_naming_formatで変更することもできます。sprintfと同様のフォーマット文字列を与え、冒頭に@を置き、それ以外の名前を置きたい場所に%sを置きます。デフォルト値は"@_%s"です。

Railsではこの内部属性を他の場所でも若干使っています。たとえばビューでは以下のように使われています。

module ActionView
  class Base
    attr_internal :captures
    attr_internal :request, :layout
    attr_internal :controller, :template
  end
end

定義はactive_support/core_ext/module/attr_internal.rbにあります。

3.1.3 モジュール属性

mattr_readermattr_writermattr_accessorという3つのマクロは、クラス用に定義されるcattr_*マクロと同じです。実際、cattr_*マクロは単なるmattr_*マクロの別名です。クラス属性も参照してください。

たとえば、これらのマクロは以下のDependenciesモジュールで使われています。

module ActiveSupport
  module Dependencies
    mattr_accessor :warnings_on_first_load
    mattr_accessor :history
    mattr_accessor :loaded
    mattr_accessor :mechanism
    mattr_accessor :load_paths
    mattr_accessor :load_once_paths
    mattr_accessor :autoloaded_constants
    mattr_accessor :explicitly_unloadable_constants
    mattr_accessor :constant_watch_stack
    mattr_accessor :constant_watch_stack_mutex
  end
end

定義はactive_support/core_ext/module/attribute_accessors.rbにあります。

3.2 親

3.2.1 module_parent

module_parentメソッドは、名前がネストしたモジュールに対して実行でき、対応する定数を持つモジュールを返します。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parent # => X::Y
M.module_parent       # => X::Y

モジュールが無名またはトップレベルの場合、module_parentObjectを返します。

parent_nameは上の場合でもnilを返します。

定義はactive_support/core_ext/module/introspection.rbにあります。

3.2.2 module_parent_name

名前がネストしたモジュールに対してmodule_parent_nameメソッドを実行すると、対応する定数を持つモジュールを返します。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parent_name # => "X::Y"
M.module_parent_name       # => "X::Y"

モジュールが無名またはトップレベルの場合、module_parent_namenilを返します。

module_parentはこの場合Objectを返します。

定義はactive_support/core_ext/module/introspection.rbにあります。

3.2.3 module_parents

module_parentsメソッドは、レシーバに対してmodule_parentを呼び出し、Objectに到着するまでパスをさかのぼります。連鎖したモジュールは、階層の下から上の順に配列として返されます。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parents # => [X::Y, X, Object]
M.module_parents       # => [X::Y, X, Object]

定義はactive_support/core_ext/module/introspection.rbにあります。

3.3 無名モジュール

モジュールには名前がある場合とない場合があります。

module M
end
M.name # => "M"

N = Module.new
N.name # => "N"

Module.new.name # => nil

モジュールに名前があるかどうかを述語メソッドanonymous?でチェックできます。

module M
end
M.anonymous? # => false

Module.new.anonymous? # => true

到達不能な(unreachable)モジュールが必ずしも無名(anonymous)とは限りません。

module M
end

m = Object.send(:remove_const, :M)

m.anonymous? # => false

逆に無名モジュールは、定義上必ず到達不能になります。

定義はactive_support/core_ext/module/anonymous.rbにあります。

3.4 メソッドの委譲

3.4.1 delegate

delegateマクロを使って、メソッドを簡単に委譲できます。

あるアプリケーションのUserモデルにログイン情報があり、それに関連する名前などの情報はProfileモデルにあるとします。

class User < ApplicationRecord
  has_one :profile
end

この構成では、user.profile.nameのようにプロファイル越しにユーザー名を取得することになります。これらの属性に直接アクセスできたらもっと便利になるでしょう。

class User < ApplicationRecord
  has_one :profile

  def name
    profile.name
  end
end

これはdelegateでできます。

class User < ApplicationRecord
  has_one :profile

  delegate :name, to: :profile
end

この方法なら記述が短くて済み、意味も明快です。

委譲するメソッドは対象クラス内でpublicでなければなりません。

delegateマクロには複数のメソッドを指定できます。

delegate :name, :age, :address, :twitter, to: :profile

:toオプションが文字列に変換されると、メソッドの委譲先となるオブジェクトに評価される式になります。通常は文字列またはシンボルになります。そのような式は、レシーバのコンテキストで評価されます。

# Rails定数を委譲する
delegate :logger, to: :Rails

# レシーバのクラスに委譲する
delegate :table_name, to: :class

:prefixオプションがtrueの場合、一般性が低下します (後述)。

委譲時にNoMethodErrorが発生して対象がnilの場合、例外が発生します。:allow_nilオプションを使うと、例外の代りにnilを返すようにすることができます。

delegate :name, to: :profile, allow_nil: true

:allow_nilを指定すると、ユーザーのプロファイルがない場合にuser.name呼び出しはnilを返します。

:prefixオプションをtrueにすると、生成されたメソッドの名前にプレフィックスを追加します。これは、たとえばよりよい名前にしたい場合に便利です。

delegate :street, to: :address, prefix: true

上の例では、streetではなくaddress_streetが生成されます。

この場合、生成されるメソッドの名前では、対象となるオブジェクト名とメソッド名が使われます。:toオプションで指定するのはメソッド名でなければなりません。

プレフィックスをカスタマイズすることもできます。

delegate :size, to: :attachment, prefix: :avatar

上の例では、マクロによってsizeの代わりにavatar_sizeが生成されます。

:privateオプションはメソッドのスコープを変更します。

delegate :date_of_birth, to: :profile, private: true

委譲されたメソッドはデフォルトでpublicになりますが、private: trueを渡すことで変更できます。

定義はactive_support/core_ext/module/delegation.rbにあります。

3.4.2 delegate_missing_to

UserオブジェクトにないものをProfileにあるものにすべて委譲したいとしましょう。delegate_missing_toマクロを使えばこれを簡単に実装できます。

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

オブジェクト内にある呼び出し可能なもの(インスタンス変数、メソッド、定数など)なら何でも対象にできます。対象のうち、publicなメソッドだけが委譲されます。

定義はactive_support/core_ext/module/delegation.rbにあります。

3.5 メソッドの再定義

メソッドをdefine_methodで再定義する必要があるが、その名前が既にあるかどうかがわからないとことがあります。有効な名前が既にあれば警告が表示されます。警告が表示されても大したことはありませんが、邪魔に思えることもあります。

redefine_methodメソッドを使うと、必要に応じて既存のメソッドが削除されるので、このような警告表示を抑制できます。

delegateを使っているなどの理由で)メソッド自身の置き換えを定義する必要がある場合は、silence_redefinition_of_methodを使うこともできます。

定義はactive_support/core_ext/module/redefine_method.rbにあります。

4 Classの拡張

4.1 Class属性

4.1.1 class_attribute

class_attributeメソッドは、1つ以上の継承可能なクラスの属性を宣言します。そのクラス属性は、その下のどの階層でも上書き可能です。

class A
  class_attribute :x
end

class B < A; end

class C < B; end

A.x = :a
B.x # => :a
C.x # => :a

B.x = :b
A.x # => :a
C.x # => :b

C.x = :c
A.x # => :a
B.x # => :b

たとえば、ActionMailer::Baseに以下の定義があるとします。

class_attribute :default_params
self.default_params = {
  mime_version: "1.0",
  charset: "UTF-8",
  content_type: "text/plain",
  parts_order: [ "text/plain", "text/enriched", "text/html" ]
}.freeze

これらの属性はインスタンスのレベルでアクセスまたはオーバーライドできます。

A.x = 1

a1 = A.new
a2 = A.new
a2.x = 2

a1.x # => 1 (Aが使われる)
a2.x # => 2 (a2でオーバーライドされる)

:instance_writerfalseに設定すれば、writerインスタンスメソッドは生成されません。

module ActiveRecord
  class Base
    class_attribute :table_name_prefix, instance_writer: false, default: "my"
  end
end

上のオプションは、モデルの属性設定時にマスアサインメントを防止するのに便利です。

:instance_readerfalseに設定すれば、readerインスタンスメソッドは生成されません。

class A
  class_attribute :x, instance_reader: false
end

A.new.x = 1
A.new.x # NoMethodError

利便性のために、class_attributeは、インスタンスのreaderが返すものを「二重否定」するインスタンス述語も定義されます。上の例の場合、x?となります。

:instance_readerfalseの場合、インスタンス述語はreaderメソッドと同様にNoMethodErrorを返します。

インスタンス述語が不要な場合、instance_predicate: falseを指定すれば定義されなくなります。

定義はactive_support/core_ext/class/attribute.rbにあります。

4.1.2 cattr_readercattr_writercattr_accessor

cattr_readercattr_writercattr_accessorマクロは、attr_*と似ていますが、クラス用である点が異なります。これらのメソッドは、クラス変数をnilに設定し (クラス変数が既にある場合を除く)、対応するクラスメソッドを生成してアクセスできるようにします。

class MysqlAdapter < AbstractAdapter
  # @@emulate_booleansにアクセスできるクラスメソッドを生成する
  cattr_accessor :emulate_booleans
end

同様に、cattr_*にブロックを渡して属性にデフォルト値を設定することもできます。

class MysqlAdapter < AbstractAdapter
  # @@emulate_booleansにアクセスしてデフォルト値をtrueにするクラスメソッドを生成する
  cattr_accessor :emulate_booleans, default: true
end

利便性のため、このときインスタンスメソッドも生成されますが、これらは実際にはクラス属性の単なるプロキシです。このため、インスタンスからクラス属性を変更することはできますが、class_attributeで行われるように上書きすることはできません(上記参照)。たとえば以下の場合、

module ActionView
  class Base
    cattr_accessor :field_error_proc, default: Proc.new { ... }
  end
end

ビューでfield_error_procにアクセスできます。

:instance_readerオプションをfalseに設定することで、readerインスタンスメソッドが生成されないようにできます。同様に、:instance_writerオプションをfalseに設定することで、writerインスタンスメソッドが生成されないようにできます。:instance_accessorオプションをfalseに設定すれば、どちらのインスタンスメソッドも生成されません。いずれの場合も、指定できる値はfalseのみです。'nil'など他のfalse値は指定できません。

module A
  class B
    # first_nameインスタンスreaderは生成されない
    cattr_accessor :first_name, instance_reader: false
    # last_name= インスタンスwriterは生成されない
    cattr_accessor :last_name, instance_writer: false
    # surnameインスタンスreaderもsurname= インスタンスwriterも生成されない
    cattr_accessor :surname, instance_accessor: false
  end
end

:instance_accessorfalseに設定すると、モデルの属性設定時にマスアサインメントを防止するのに便利です。

定義はactive_support/core_ext/module/attribute_accessors.rbにあります。

4.2 サブクラスと子孫

4.2.1 subclasses

subclassesメソッドはレシーバのサブクラスを返します。

class C; end
C.subclasses # => []

class B < C; end
C.subclasses # => [B]

class A < B; end
C.subclasses # => [B]

class D < C; end
C.subclasses # => [B, D]

返されるクラスの順序は一定ではありません。

定義はactive_support/core_ext/class/subclasses.rbにあります。

4.2.2 descendants

descendantsメソッドは、そのレシーバより下位にあるすべてのクラスを返します。

class C; end
C.descendants # => []

class B < C; end
C.descendants # => [B]

class A < B; end
C.descendants # => [B, A]

class D < C; end
C.descendants # => [B, A, D]

返されるクラスの順序は一定ではありません。

定義はactive_support/core_ext/class/subclasses.rbにあります。

5 Stringの拡張

5.1 安全な出力

5.1.1 開発の動機

HTMLテンプレートにデータを挿入する方法は、きわめて慎重に設計する必要があります。たとえば、@review.titleを何の工夫もなくそのままHTMLに式展開するようなことは絶対にすべきではありません。もしこのレビューのタイトルが仮に「Flanagan & Matz rules!」だとしたら、出力はwell-formedになりません。well-formedにするには、"&amp;"のようにエスケープしなければなりません。さらに、ユーザーがレビューのタイトルに細工をして、悪意のあるHTMLをタイトルに含めれば、巨大なセキュリティホールになることすらあります。このリスクの詳細については、セキュリティガイドのクロスサイトスクリプティングの節を参照してください。

5.1.2 安全な文字列

Active Supportには「(html的に) 安全な文字列」という概念があります。安全な文字列とは、HTMLにそのまま挿入しても問題がないというマークが付けられている文字列です。マーキングさえされていれば、「実際にエスケープされているかどうかにかかわらず」その文字列は信頼されます。

文字列はデフォルトでは unsafe とマークされます。

"".html_safe? # => false

与えられた文字列にhtml_safeメソッドを適用することで、安全な文字列を得ることができます。

s = "".html_safe
s.html_safe? # => true

ここで注意しなければならないのは、html_safeメソッドそれ自体は何らエスケープを行なっていないということです。安全であるとマーキングしているに過ぎません。

s = "<script>...</script>".html_safe
s.html_safe? # => true
s            # => "<script>...</script>"

すなわち、特定の文字列に対してhtml_safeメソッドを呼び出す際には、その文字列が本当に安全であることを確認する義務があります。

安全であると宣言された文字列に対し、安全でない文字列をconcat/<<+で破壊的に追加すると、結果は安全な文字列になります。安全でない引数は追加時にエスケープされます。

"".html_safe + "<" # => "&lt;"

安全な引数であれば、(エスケープなしで)直接追加されます。

"".html_safe + "<".html_safe # => "<"

基本的にこれらのメソッドは、通常のビューでは使わないでください。現在のRailsのビューでは、安全でない値は自動的にエスケープされるためです。

<%= @review.title %> <%# 必要に応じてエスケープされるので問題なし %>

何らかの理由で、エスケープされていない文字列を挿入したい場合は、html_safeを呼ぶのではなく、rawヘルパーをお使いください。

<%= raw @cms.current_template %> <%# @cms.current_templateをそのまま挿入 %>

あるいは、rawと同等の<%==を使います。

<%== @cms.current_template %> <%# @cms.current_templateをそのまま挿入 %>

rawヘルパーは、内部でhtml_safeを呼び出します。

def raw(stringish)
  stringish.to_s.html_safe
end

定義はactive_support/core_ext/string/output_safety.rbにあります。

5.1.3 各種変換

経験上、上で説明したような連結 (concatenation) 操作を除き、どんなメソッドでも潜在的には文字列を安全でないものに変換してしまう可能性があることに常に注意を払う必要があります。downcasegsubstripchompunderscoreなどの変換メソッドがこれに該当します。

gsub!のような破壊的な変換を行なうメソッドを使うと、レシーバ自体が安全でなくなってしまいます。

こうしたメソッドを実行すると、実際に変換が行われたかどうかにかかわらず、安全を表すビットは常にオフになります。

5.1.4 変換と強制

安全な文字列に対してto_sを実行した場合は、安全な文字列が返されます。しかし、to_strによる強制的な変換を実行した場合には安全でない文字列が返されます。

5.1.5 コピー

安全な文字列に対してdupまたはcloneを実行した場合は、安全な文字列が生成されます。

5.2 remove

removeメソッドを実行すると、すべての該当パターンが削除されます。

"Hello World".remove(/Hello /) # => "World"

このメソッドには破壊的なバージョンのString#remove!もあります。

定義はactive_support/core_ext/string/filters.rbにあります。

5.3 squish

squishメソッドは、冒頭と末尾のホワイトスペースを除去し、連続したホワイトスペースを1つに減らします。

" \n  foo\n\r \t bar \n".squish # => "foo bar"

このメソッドには破壊的なバージョンのString#squish!もあります。

このメソッドでは、ASCIIとUnicodeのホワイトスペースを扱えます。

定義はactive_support/core_ext/string/filters.rbにあります。

5.4 truncate

truncateメソッドは、指定されたlengthにまで長さを切り詰めたレシーバのコピーを返します。

"Oh dear! Oh dear! I shall be late!".truncate(20)
# => "Oh dear! Oh dear!..."

:omissionオプションを指定することで、省略文字 (…) をカスタマイズすることもできます。

"Oh dear! Oh dear! I shall be late!".truncate(20, omission: '&hellip;')
# => "Oh dear! Oh &hellip;"

文字列の切り詰めでは、省略文字列の長さも加味されることに特にご注意ください。

:separatorを指定することで、自然な区切り位置で切り詰めることができます。

"Oh dear! Oh dear! I shall be late!".truncate(18)
# => "Oh dear! Oh dea..."
"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ')
# => "Oh dear! Oh..."

:separatorオプションでは正規表現も使えます。

"Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/)
# => "Oh dear! Oh..."

上の例では、"dear"という文字で切り落とされそうになるところを、:separatorによって防いでいます。

定義はactive_support/core_ext/string/filters.rbにあります。

5.5 truncate_words

truncate_wordsメソッドは、指定されたワード数から後ろを切り落としたレシーバのコピーを返します。

"Oh dear! Oh dear! I shall be late!".truncate_words(4)
# => "Oh dear! Oh dear!..."

:omissionオプションを指定することで、省略文字 (…) をカスタマイズすることもできます。

"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '&hellip;')
# => "Oh dear! Oh dear!&hellip;"

:separatorを指定することで、自然な区切り位置で切り詰めることができます。

"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!')
# => "Oh dear! Oh dear! I shall be late..."

:separatorオプションでは正規表現も使えます。

"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/)
# => "Oh dear! Oh dear!..."

定義はactive_support/core_ext/string/filters.rbにあります。

5.6 inquiry

inquiryは、文字列をStringInquirerオブジェクトに変換します。このオブジェクトを使うと、等しいかどうかをよりスマートにチェックできます。

"production".inquiry.production? # => true
"active".inquiry.inactive?       # => false

定義はactive_support/core_ext/string/inquiry.rbにあります。****

5.7 starts_with?ends_with?

Active Supportでは、String#start_with?String#end_with?を英語的に自然な三人称(starts、ends)にした別名も定義されています。

"foo".starts_with?("f") # => true
"foo".ends_with?("o")   # => true

定義はactive_support/core_ext/string/starts_ends_with.rbにあります。

5.8 strip_heredoc

strip_heredocメソッドは、ヒアドキュメントのインデントを除去します。

以下に例を示します。

if options[:usage]
  puts <<-USAGE.strip_heredoc
    This command does such and such.

    Supported options are:
      -h         This message
      ...
  USAGE
end

このUSAGEメッセージは左寄せで表示されます。

技術的には、インデントが一番浅い行を探して、そのインデント分だけ行頭のホワイトスペースを全体から削除するという操作を行っています。

定義はactive_support/core_ext/string/strip.rbにあります。

5.9 indent

このメソッドは、レシーバの行にインデントを与えます。

<<EOS.indent(2)
def some_method
  some_code
end
EOS
# =>
  def some_method
    some_code
  end

2つめの引数indent_stringは、インデントに使う文字列を指定します。デフォルトはnilであり、この場合最初にインデントされている行のインデント文字を参照してそこからインデント文字を推測します。インデントがまったくない場合はスペース1つを使います。

"  foo".indent(2)        # => "    foo"
"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
"foo".indent(2, "\t")    # => "\t\tfoo"

indent_stringには1文字のスペースまたはタブを使うのが普通ですが、どんな文字でも使えます。

3つ目の引数indent_empty_linesは、空行もインデントするかどうかを指定するフラグです。デフォルトはfalseです。

"foo\n\nbar".indent(2)            # => "  foo\n\n  bar"
"foo\n\nbar".indent(2, nil, true) # => "  foo\n  \n  bar"

indent!メソッドはインデントをその場で (破壊的に) 行います。

定義はactive_support/core_ext/string/indent.rbにあります。

5.10 Access

5.10.1 at(position)

対象となる文字列のうち、positionで指定された位置にある文字を返します。

"hello".at(0)  # => "h"
"hello".at(4)  # => "o"
"hello".at(-1) # => "o"
"hello".at(10) # => nil

定義はactive_support/core_ext/string/access.rbにあります。

5.10.2 from(position)

文字列のうち、positionで指定された位置から始まる部分文字列を返します。

"hello".from(0)  # => "hello"
"hello".from(2)  # => "llo"
"hello".from(-2) # => "lo"
"hello".from(10) # => nil

定義はactive_support/core_ext/string/access.rbにあります。

5.10.3 to(position)

文字列のうち、positionで指定された位置を終端とする部分文字列を返します。

"hello".to(0)  # => "h"
"hello".to(2)  # => "hel"
"hello".to(-2) # => "hell"
"hello".to(10) # => "hello"

定義はactive_support/core_ext/string/access.rbにあります。

5.10.4 first(limit = 1)

str.first(n)という呼び出しは、n > 0の場合はstr.to(n-1)と等価です。n == 0の場合は空文字列を返します。

定義はactive_support/core_ext/string/access.rbにあります。

5.10.5 last(limit = 1)

str.last(n) という呼び出しは、n > 0の場合はstr.from(-n)と等価です。n == 0の場合は空文字列を返します。

定義はactive_support/core_ext/string/access.rbにあります。

5.11 活用形

5.11.1 pluralize

pluralizeメソッドは、レシーバを「複数形」にしたものを返します。

"table".pluralize     # => "tables"
"ruby".pluralize      # => "rubies"
"equipment".pluralize # => "equipment"

上の例でも示したように、Active Supportは不規則な複数形や非可算名詞をある程度扱えます。config/initializers/inflections.rbにあるビルトインのルールは拡張可能です。このファイルはrails newコマンド実行時にデフォルトで生成され、ファイルのコメントに説明が示されています。

pluralizeメソッドではオプションでcountパラメータを使えます。もしcount == 1を指定すると単数形が返されます。countがそれ以外の値の場合は複数形を返します(訳注: 英語では個数がゼロや小数の場合は複数形で表されます)。

"dude".pluralize(0) # => "dudes"
"dude".pluralize(1) # => "dude"
"dude".pluralize(2) # => "dudes"

Active Recordでは、モデル名に対応するデフォルトのテーブル名を求めるときにこのメソッドを使います。

# active_record/model_schema.rb
def undecorated_table_name(class_name = base_class.name)
  table_name = class_name.to_s.demodulize.underscore
  pluralize_table_names ? table_name.pluralize : table_name
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.2 singularize

pluralizeと逆の動作です。

"tables".singularize    # => "table"
"rubies".singularize    # => "ruby"
"equipment".singularize # => "equipment"

Railsの関連付け (association) では、関連付けられたクラスにデフォルトで対応する名前を求める時にこのメソッドを使います。

# active_record/reflection.rb
def derive_class_name
  class_name = name.to_s.camelize
  class_name = class_name.singularize if collection?
  class_name
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.3 camelize

camelizeメソッドは、レシーバをキャメルケース (冒頭を大文字にした単語をスペースなしで連結した語) にしたものを返します。

"product".camelize    # => "Product"
"admin_user".camelize # => "AdminUser"

このメソッドは、パスをRubyのクラスに変換するときにもよく使われます。スラッシュで区切られているパスは「::」で区切られます。

"backoffice/session".camelize # => "Backoffice::Session"

たとえばAction Packでは、特定のセッションストアを提供するクラスを読み込むのにこのメソッドを使います。

# action_controller/metal/session_management.rb
def session_store=(store)
  @@session_store = store.is_a?(Symbol) ?
    ActionDispatch::Session.const_get(store.to_s.camelize) :
    store
end

camelizeメソッドはオプションの引数を受け付けます。:upper(デフォルト)または:lowerを指定できます。後者を指定すると、冒頭が小文字になります。

"visual_effect".camelize(:lower) # => "visualEffect"

このメソッドは、そのような命名慣習に沿う言語(JavaScriptなど)で使う名前を求めるのに便利です。

camerizeメソッドの動作は、underscoreメソッドと逆の動作と考えるとわかりやすいでしょう。ただし完全に逆の動作ではありません。たとえば、"SSLError".underscore.camelizeを実行した結果は"SslError"になり、元に戻りません。このような場合をサポートするために、Active Supportではconfig/initializers/inflections.rbの頭字語(acronym)を次のように指定できます。

ActiveSupport::Inflector.inflections do |inflect|
  inflect.acronym 'SSL'
end

"SSLError".underscore.camelize # => "SSLError"

camelizecamelcaseの別名です。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.4 underscore

underscoreメソッドは上と逆に、キャメルケースをパスに変換します。

"Product".underscore   # => "product"
"AdminUser".underscore # => "admin_user"

"::"も"/"に逆変換されます。

"Backoffice::Session".underscore # => "backoffice/session"

小文字で始まる文字列も扱えます。

"visualEffect".underscore # => "visual_effect"

ただしunderscoreは引数を取りません。

Railsで自動的に読み込まれるクラスとモジュールは、ファイルの拡張子をunderscoreメソッドで除いた相対パスを推測し、指定された定数が失われている場合にそれを定義するのにこのメソッドを利用します。

# active_support/dependencies.rb
def load_missing_constant(from_mod, const_name)
  ...
  qualified_name = qualified_name_for from_mod, const_name
  path_suffix = qualified_name.underscore
  ...
end

underscoreメソッドの動作は、camelizeメソッドと逆の動作と考えるとわかりやすいでしょう。ただし完全に逆の動作ではありません。たとえば、"SSLError".underscore.camelizeを実行した結果は"SslError"になり、元に戻りません。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.5 titleize

titleizeメソッドは、レシーバの語の1文字目を大文字にします。

"alice in wonderland".titleize # => "Alice In Wonderland"
"fermat's enigma".titleize     # => "Fermat's Enigma"

titleizeメソッドはtitlecaseの別名です。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.6 dasherize

dasherizeメソッドは、レシーバのアンダースコア文字をダッシュに置き換えます(訳注: ここで言うダッシュは実際には「ハイフンマイナス文字」(U+002D)です)。

"name".dasherize         # => "name"
"contact_data".dasherize # => "contact-data"

モデルのXMLシリアライザではノード名をこのメソッドでダッシュ化しています。

# active_model/serializers/xml.rb
def reformat_name(name)
  name = name.camelize if camelize?
  dasherize? ? name.dasherize : name
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.7 demodulize

demodulizeメソッドは、フルパスの (qualified) 定数名を与えられると、パス部分を取り除いて右側の定数名だけにしたものを返します。

"Product".demodulize                        # => "Product"
"Backoffice::UsersController".demodulize    # => "UsersController"
"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
"::Inflections".demodulize                  # => "Inflections"
"".demodulize                               # => ""


以下のActive Recordの例では、counter_cache_columnの名前をこのメソッドで求めています。

# active_record/reflection.rb
def counter_cache_column
  if options[:counter_cache] == true
    "#{active_record.name.demodulize.underscore.pluralize}_count"
  elsif options[:counter_cache]
    options[:counter_cache]
  end
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.8 deconstantize

deconstantizeメソッドは、フルパスの定数を表す参照表現を与えられると、一番右の部分 (通常は定数名) を取り除きます。

"Product".deconstantize                        # => ""
"Backoffice::UsersController".deconstantize    # => "Backoffice"
"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.9 parameterize

parameterizeメソッドは、レシーバを正しいURLで利用可能な形式に正規化します。

"John Smith".parameterize # => "john-smith"
"Kurt Gödel".parameterize # => "kurt-godel"

文字列の大文字小文字が変わらないようにするには、preserve_case引数にtrueを指定します。preserve_caseはデフォルトではfalseです。

"John Smith".parameterize(preserve_case: true) # => "John-Smith"
"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"

独自のセパレータを使うには、separator引数をオーバーライドします。

"John Smith".parameterize(separator: "_") # => "john\_smith"
"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel"

実際に得られる文字列は、ActiveSupport::Multibyte::Charsのインスタンスでラップされています。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.10 tableize

tableizeメソッドは、underscoreの次にpluralizeを実行したものです。

"Person".tableize      # => "people"
"Invoice".tableize     # => "invoices"
"InvoiceLine".tableize # => "invoice_lines"

単純な場合であれば、モデル名にtableizeを使うとモデルのテーブル名を得られます。実際のActive Recordの実装は、単にtableizeを実行する場合よりも複雑です。Active Recordではクラス名に対してdemodulizeも行っており、返される文字列に影響する可能性のあるオプションもいくつかチェックしています。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.11 classify

classifyメソッドはtableizeと逆の動作で、与えられたテーブル名に対応するクラス名を返します。

"people".classify        # => "Person"
"invoices".classify      # => "Invoice"
"invoice_lines".classify # => "InvoiceLine"

このメソッドは、フルパスの (qualified) テーブル名も扱えます。

"highrise_production.companies".classify # => "Company"

classifyが返すクラス名は文字列であることにご注意ください。得られた文字列に対してconstantize (後述) を実行することで本当のクラスオブジェクトを得られます。

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.12 constantize

constantizeメソッドは、レシーバの定数参照表現を解決し、実際のオブジェクトを返します。

"Fixnum".constantize # => Fixnum

module M
  X = 1
end
"M::X".constantize # => 1

与えられた文字列をconstantizeメソッドで評価しても既知の定数とマッチしない、または指定された定数名が正しくない場合はNameErrorが発生します。

constantizeメソッドによる定数名解決は、常にトップレベルのObjectから開始されます。これは冒頭に「::」がない場合でも同じです。

X = :in_Object
module M
  X = :in_M

  X                 # => :in_M
  "::X".constantize # => :in_Object
  "X".constantize   # => :in_Object (!)
end

このため、このメソッドは、同じ場所でRubyが定数を評価したときの値と必ずしも等価ではありません。

メイラー (mailer) のテストケースでは、テストするクラスの名前からテスト対象のメイラーを取得するのにconstantizeメソッドを使います。

# action_mailer/test_case.rb
def determine_default_mailer(name)
  name.sub(/Test$/, '').constantize
rescue NameError => e
  raise NonInferrableMailerError.new(name)
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.13 humanize

humanizeメソッドは、属性名を (英語的に) 読みやすい表記に変換します。

具体的には以下の変換を行います。

  • 引数に (英語の) 活用ルールを適用します(inflection)。
  • 冒頭にアンダースコアがある場合は削除します。
  • 末尾に「_id」がある場合は削除します。
  • アンダースコアが他にもある場合はスペースに置き換えます。
  • 略語を除いてすべての単語を小文字にします(downcase)。
  • 最初の単語だけ冒頭の文字を大文字にします(capitalize)。

:capitalizeオプションをfalseにすると、冒頭の文字は大文字にされません(デフォルトはtrue)。

"name".humanize                         # => "Name"
"author_id".humanize                    # => "Author"
"author_id".humanize(capitalize: false) # => "author"
"comments_count".humanize               # => "Comments count"
"_id".humanize                          # => "Id"

"SSL"が頭字語と定義されている場合は以下のようになります。

'ssl_error'.humanize # => "SSL error"

ヘルパーメソッドfull_messagesでは、属性名をメッセージに含めるときにhumanizeを使います。

def full_messages
  map { |attribute, message| full_message(attribute, message) }
end

def full_message
  ...
  attr_name = attribute.to_s.tr('.', '_').humanize
  attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
  ...
end

定義はactive_support/core_ext/string/inflections.rbにあります。

5.11.14 foreign_key

foreign_keyメソッドは、クラス名から外部キーカラム名を求めるのに用いられます。具体的には、demodulizeunderscoreを実行し、末尾に「_id」を追加します。

"User".foreign_key           # => "user_id"
"InvoiceLine".foreign_key    # => "invoice_line_id"
"Admin::Session".foreign_key # => "session_id"

末尾の「_id」のアンダースコアが不要な場合は、引数にfalseを指定します。

"User".foreign_key(false) # => "userid"

関連付け (association) では、外部キーの名前を推測するときにこのメソッドを使います。たとえばhas_onehas_manyでは以下を行っています。

# active_record/associations.rb
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key

定義はactive_support/core_ext/string/inflections.rbにあります。

5.12 各種変換

5.12.1 to_dateto_timeto_datetime

to_dateto_timeto_datetimeメソッドは、Date._parseをラップして使いやすくします。

"2010-07-27".to_date              # => Tue, 27 Jul 2010
"2010-07-27 23:37:00".to_time     # => 2010-07-27 23:37:00 +0200
"2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000

to_timeはオプションで:utc:localを引数に取り、タイムゾーンを指定できます。

"2010-07-27 23:42:00".to_time(:utc)   # => 2010-07-27 23:42:00 UTC
"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200

デフォルトは:localです。

詳しくはDate._parseのドキュメントを参照してください。

3つのメソッドはいずれも、レシーバが空の場合はnilを返します。

定義はactive_support/core_ext/string/conversions.rbにあります。

6 Numericの拡張

6.1 バイト

すべての数値は、以下のメソッドに応答します。

bytes
kilobytes
megabytes
gigabytes
terabytes
petabytes
exabytes

これらのメソッドは、対応するバイト数を返すときに1024の倍数を使います。

2.kilobytes   # => 2048
3.megabytes   # => 3145728
3.5.gigabytes # => 3758096384
-4.exabytes   # => -4611686018427387904

これらのメソッドには単数形の別名もあります。

1.megabyte # => 1048576

定義はactive_support/core_ext/numeric/bytes.rbにあります。

6.2 Time

たとえば45.minutes + 2.hours + 4.weeksのように時間の計算や宣言を行なえます。

これらのメソッドでは正確な日付計算のためにTime#advanceを利用しています。Time#advanceは、from_nowagoなどの他に、Timeオブジェクトから得た結果の加減算でも使われています。以下に例を示します。

# Time.current.advance(months: 1) と等価
1.month.from_now

# Time.current.advance(weeks: 2) と等価
2.weeks.from_now

# Time.current.advance(months: 4, weeks: 5) と等価
(4.months + 5.weeks).from_now

上記以外の期間については、IntegerTime拡張を参照してください。

定義はactive_support/core_ext/numeric/time.rbにあります。

6.3 書式設定

数値はさまざまな方法でフォーマットできます。

以下のように、数値を電話番号形式の文字列に変換できます。

5551234.to_s(:phone)
# => 555-1234
1235551234.to_s(:phone)
# => 123-555-1234
1235551234.to_s(:phone, area_code: true)
# => (123) 555-1234
1235551234.to_s(:phone, delimiter: " ")
# => 123 555 1234
1235551234.to_s(:phone, area_code: true, extension: 555)
# => (123) 555-1234 x 555
1235551234.to_s(:phone, country_code: 1)
# => +1-123-555-1234

以下のように、数値を通貨形式の文字列に変換できます。

1234567890.50.to_s(:currency)                 # => $1,234,567,890.50
1234567890.506.to_s(:currency)                # => $1,234,567,890.51
1234567890.506.to_s(:currency, precision: 3)  # => $1,234,567,890.506

以下のように、数値を百分率形式の文字列に変換できます。

100.to_s(:percentage)
# => 100.000%
100.to_s(:percentage, precision: 0)
# => 100%
1000.to_s(:percentage, delimiter: '.', separator: ',')
# => 1.000,000%
302.24398923423.to_s(:percentage, precision: 5)
# => 302.24399%

以下のように、数値の桁区切りを追加して文字列形式にできます。

12345678.to_s(:delimited)                     # => 12,345,678
12345678.05.to_s(:delimited)                  # => 12,345,678.05
12345678.to_s(:delimited, delimiter: ".")     # => 12.345.678
12345678.to_s(:delimited, delimiter: ",")     # => 12,345,678
12345678.05.to_s(:delimited, separator: " ")  # => 12,345,678 05

以下のように、数字を特定の精度に丸めて文字列形式にできます。

111.2345.to_s(:rounded)                     # => 111.235
111.2345.to_s(:rounded, precision: 2)       # => 111.23
13.to_s(:rounded, precision: 5)             # => 13.00000
389.32314.to_s(:rounded, precision: 0)      # => 389
111.2345.to_s(:rounded, significant: true)  # => 111

以下のように、数値を人間にとって読みやすいバイト数形式の文字列に変換できます。

123.to_s(:human_size)                  # => 123 Bytes
1234.to_s(:human_size)                 # => 1.21 KB
12345.to_s(:human_size)                # => 12.1 KB
1234567.to_s(:human_size)              # => 1.18 MB
1234567890.to_s(:human_size)           # => 1.15 GB
1234567890123.to_s(:human_size)        # => 1.12 TB
1234567890123456.to_s(:human_size)     # => 1.1 PB
1234567890123456789.to_s(:human_size)  # => 1.07 EB

以下のように、数値を人間にとって読みやすいバイト数形式で単位が単語の文字列に変換できます。

123.to_s(:human)               # => "123"
1234.to_s(:human)              # => "1.23 Thousand"
12345.to_s(:human)             # => "12.3 Thousand"
1234567.to_s(:human)           # => "1.23 Million"
1234567890.to_s(:human)        # => "1.23 Billion"
1234567890123.to_s(:human)     # => "1.23 Trillion"
1234567890123456.to_s(:human)  # => "1.23 Quadrillion"

定義はactive_support/core_ext/numeric/conversions.rbにあります。

7 Integerの拡張

7.1 multiple_of?

multiple_of?メソッドは、レシーバの整数が引数の倍数であるかどうかをテストします。

2.multiple_of?(1) # => true
1.multiple_of?(2) # => false

定義はactive_support/core_ext/integer/multiple.rbにあります。

7.2 ordinal

ordinalメソッドは、レシーバの整数に対応する序数のサフィックス文字列を返します。

1.ordinal    # => "st"
2.ordinal    # => "nd"
53.ordinal   # => "rd"
2009.ordinal # => "th"
-21.ordinal  # => "st"
-134.ordinal # => "th"

定義はactive_support/core_ext/integer/inflections.rbにあります。

7.3 ordinalize

ordinalizeメソッドは、レシーバの整数に、対応する序数文字列を追加したものをかえします。先に紹介したordinalメソッドは、序数文字列だけを返す点が異なることにご注意ください。

1.ordinalize    # => "1st"
2.ordinalize    # => "2nd"
53.ordinalize   # => "53rd"
2009.ordinalize # => "2009th"
-21.ordinalize  # => "-21st"
-134.ordinalize # => "-134th"

定義はactive_support/core_ext/integer/inflections.rbにあります。

7.4 Time

4.months + 5.yearsのような形式での時間の計算や宣言を行えるようにします。

これらのメソッドでは正確な日付計算のためにTime#advanceを利用しています。Time#advanceは、from_nowagoなどの他に、Timeオブジェクトから得た結果の加減算でも使われています。以下に例を示します。

# Time.current.advance(months: 1)と同等
1.month.from_now

# Time.current.advance(years: 2)と同等
2.years.from_now

# Time.current.advance(months: 4, years: 5)と同等
(4.months + 5.years).from_now

上記以外の期間については、NumericTime拡張を参照してください。

定義は active_support/core_ext/integer/time.rbにあります。

8 BigDecimalの拡張

8.1 to_s

to_sメソッドは「F」のデフォルトの記法を提供します。これは、to_sを単に呼び出すと、エンジニアリング記法ではなく浮動小数点を得られるということです。

BigDecimal(5.00, 6).to_s       # => "5.0"

また、シンボルによる指定もサポートされます。

BigDecimal(5.00, 6).to_s(:db)  # => "5.0"

エンジニアリング記法も従来通りサポートされます。

BigDecimal(5.00, 6).to_s("e")  # => "0.5E1"

9 Enumerableの拡張

9.1 sum

sumメソッドはenumerableの要素を合計します。

[1, 2, 3].sum # => 6
(1..100).sum  # => 5050

+に応答する要素のみが加算の対象として前提とされます。

[[1, 2], [2, 3], [3, 4]].sum    # => [1, 2, 2, 3, 3, 4]
%w(foo bar baz).sum             # => "foobarbaz"
{a: 1, b: 2, c: 3}.sum          # => [:a, 1, :b, 2, :c, 3]

空のコレクションはデフォルトではゼロを返しますが、この動作はカスタマイズ可能です。

[].sum    # => 0
[].sum(1) # => 1

ブロックが与えられると、sumはイテレータになってコレクションの要素をyieldし、そこから返された値を合計します。

(1..5).sum {|n| n * 2 } # => 30
[2, 4, 6, 8, 10].sum    # => 30

ブロックを与える場合にも、レシーバが空のときのデフォルト値をカスタマイズできます。

[].sum(1) {|n| n**3} # => 1

定義はactive_support/core_ext/enumerable.rbにあります。

9.2 index_by

index_byメソッドは、何らかのキーによってインデックス化されたenumerableの要素を持つハッシュを生成します。

このメソッドはコレクションを列挙し、各要素をブロックに渡します。この要素は、ブロックから返された値によってインデックス化されます。

invoices.index_by(&:number)
# => {'2009-032' => <Invoice ...>, '2009-008' => <Invoice ...>, ...}

キーは通常は一意でなければなりません。異なる要素から同じ値が返されると、そのキーのコレクションは作成されません。返された項目のうち、最後の項目だけが使われます。

定義はactive_support/core_ext/enumerable.rbにあります。

9.3 index_with

index_withメソッドは、enumerableの要素をキーとして持つハッシュを生成します。値は渡されたデフォルト値か、ブロックで返されます。

The method index_with generates a hash with the elements of an enumerable as keys. The value is either a passed default or returned in a block.

%i( title body created_at ).index_with { |attr_name| post.public_send(attr_name) }
# => { title: "hey", body: "what's up?", … }

WEEKDAYS.index_with(Interval.all_day)
# => { monday: [ 0, 1440 ], … }

定義はactive_support/core_ext/enumerable.rbにあります。

9.4 many?

many?メソッドは、collection.size > 1の短縮形です。

<% if pages.many? %>
  <%= pagination_links %>
<% end %>

many?は、ブロックがオプションとして与えられると、trueを返す要素だけを扱います。

@see_more = videos.many? {|video| video.category == params[:category]}

定義はactive_support/core_ext/enumerable.rbにあります。

9.5 exclude?

exclude?述語メソッドは、与えられたオブジェクトがそのコレクションに属していないかどうかをテストします。include?の逆の動作です。

to_visit << node if visited.exclude?(node)

定義はactive_support/core_ext/enumerable.rbにあります。

9.6 without

withoutメソッドは、指定した要素を除外したenumerableのコピーを返します。

["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"]

定義はactive_support/core_ext/enumerable.rbにあります。

9.7 pluck

pluckメソッドは、指定されたキーに基づく配列を返します。

[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"]

定義はactive_support/core_ext/enumerable.rbにあります。

10 Arrayの拡張

10.1 配列へのアクセス

Active Supportには配列のAPIが多数追加されており、配列に容易にアクセスできるようになっています。たとえばtoメソッドは、配列の冒頭から、渡されたインデックスが示す箇所までの範囲を返します。

%w(a b c d).to(2) # => ["a", "b", "c"]
[].to(7)          # => []

同様にfromメソッドは、配列のうち、インデックスが指す箇所から末尾までの要素を返します。インデックスが配列のサイズより大きい場合は、空の配列を返します。

%w(a b c d).from(2)  # => ["c", "d"]
%w(a b c d).from(10) # => []
[].from(0)           # => []

secondthirdfourthfifthは、second_to_lastthird_to_lastと同様に、対応する位置の要素を返します (firstlastは元からビルトインされています)。社会の智慧と建設的な姿勢のおかげで、今ではforty_twoも使えます (訳注: Rails 2.2 以降で使えます。「42」については、Wikipediaの生命、宇宙、そして万物についての究極の疑問の答えを参照してください)。

%w(a b c d).third # => "c"
%w(a b c d).fifth # => nil

定義はactive_support/core_ext/array/access.rbにあります。

10.2 展開

extract!メソッドは、ブロックの返す値がtrueになる要素を削除して返します。ブロックが渡されない場合はEnumeratorを返します。

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
numbers # => [0, 2, 4, 6, 8]

定義はactive_support/core_ext/array/extract.rbにあります。

10.3 オプションの展開

Rubyでは、メソッドに与えられた最後の引数がハッシュの場合、ハッシュの波かっこ{}を省略できます(引数が&block引数である場合を除く)。

User.exists?(email: params[:email])

このようなシンタックスシュガーは、多数の引数が順序に依存することを避け、名前付きパラメータをエミュレートするインターフェイスを提供するためにRailsで多用されています。特に、末尾にオプションのハッシュを置くというのは定番中の定番です。

しかし、あるメソッドが受け取る引数の数が固定されておらず、メソッド宣言で*が使われていると、そのような波かっこなしのオプションハッシュは引数の配列の末尾の要素になってしまい、ハッシュとして認識されなくなってしまいます。

このような場合、extract_options!メソッドは、配列の最後の項目の型をチェックします。それがハッシュの場合、そのハッシュを取り出して返し、それ以外の場合は空のハッシュを返します。

caches_actionコントローラマクロでの定義を例にとって見てみましょう。

def caches_action(*actions)
  return unless cache_configured?
  options = actions.extract_options!
  ...
end

このメソッドは、任意の数のアクション名を引数に取ることができ、引数の末尾項目でオプションハッシュを使えます。extract_options!メソッドを使うと、このオプションハッシュの取得とactionsからの除去を簡単かつ明示的に行えます。

定義はactive_support/core_ext/array/extract_options.rbにあります。

10.4 各種変換

10.4.1 to_sentence

to_sentenceメソッドは、配列を変換して、要素を列挙する英文にします。

%w().to_sentence                # => ""
%w(Earth).to_sentence           # => "Earth"
%w(Earth Wind).to_sentence      # => "Earth and Wind"
%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire"

このメソッドは3つのオプションを受け付けます。

  • :two_words_connector: 項目数が2つの場合の接続詞を指定します。デフォルトはスペースを含む「 and 」です。
  • :words_connector: 3つ以上の要素を接続する場合、最後の2つの間以外で使われる接続詞を指定します。デフォルトはスペースを含む「, 」です。
  • :last_word_connector: 3つ以上の要素を接続する場合、最後の2つの要素で使われる接続詞を指定します。デフォルトはスペースを含む「, and 」です。

これらのオプションは標準の方法でローカライズできます。使えるキーは以下のとおりです。

オプション I18n キー
:two_words_connector support.array.two_words_connector
:words_connector support.array.words_connector
:last_word_connector support.array.last_word_connector

定義はactive_support/core_ext/array/conversions.rbにあります。

10.4.2 to_formatted_s

to_formatted_sメソッドは、デフォルトではto_sと同様に振る舞います。

ただし、配列の中にidに応答する項目がある場合は、:dbというシンボルを引数として渡すことで対応できる点が異なります。この手法は、Active Recordオブジェクトのコレクションに対してよく使われます。返される文字列は以下のとおりです。

[].to_formatted_s(:db)            # => "null"
[user].to_formatted_s(:db)        # => "8456"
invoice.lines.to_formatted_s(:db) # => "23,567,556,12"

上の例の整数は、idへの呼び出しによって取り出されたものとみなされます。

定義はactive_support/core_ext/array/conversions.rbにあります。

10.4.3 to_xml

to_xmlメソッドは、レシーバをXML表現に変換したものを含む文字列を返します。

Contributor.limit(2).order(:rank).to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <contributors type="array">
#   <contributor>
#     <id type="integer">4356</id>
#     <name>Jeremy Kemper</name>
#     <rank type="integer">1</rank>
#     <url-id>jeremy-kemper</url-id>
#   </contributor>
#   <contributor>
#     <id type="integer">4404</id>
#     <name>David Heinemeier Hansson</name>
#     <rank type="integer">2</rank>
#     <url-id>david-heinemeier-hansson</url-id>
#   </contributor>
# </contributors>

実際には、to_xmlをすべての要素に送り、結果をルートノードの下に集めます。すべての要素がto_xmlに応答する必要があります。そうでない場合は例外が発生します。

デフォルトでは、ルート要素の名前は最初の要素のクラス名を複数形にしてアンダースコア化(underscored)とダッシュ化(dasherize)を行います。残りの要素も最初の要素と同じ型 (is_a?でチェックされます) に属し、ハッシュでないことが前提となっています。上の例で言うと「contributors」です。

最初の要素と同じ型に属さない要素が1つでもある場合、ルートノードにはobjectsが使われます。

[Contributor.first, Commit.first].to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
#   <object>
#     <id type="integer">4583</id>
#     <name>Aaron Batalion</name>
#     <rank type="integer">53</rank>
#     <url-id>aaron-batalion</url-id>
#   </object>
#   <object>
#     <author>Joshua Peek</author>
#     <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp>
#     <branch>origin/master</branch>
#     <committed-timestamp type="datetime">2009-09-02T16:44:36Z</committed-timestamp>
#     <committer>Joshua Peek</committer>
#     <git-show nil="true"></git-show>
#     <id type="integer">190316</id>
#     <imported-from-svn type="boolean">false</imported-from-svn>
#     <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message>
#     <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1>
#   </object>
# </objects>

レシーバがハッシュの配列である場合、ルート要素はデフォルトでobjectsになります。

[{a: 1, b: 2}, {c: 3}].to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
#   <object>
#     <b type="integer">2</b>
#     <a type="integer">1</a>
#   </object>
#   <object>
#     <c type="integer">3</c>
#   </object>
# </objects>

コレクションが空の場合、ルート要素はデフォルトで「nilクラス」になります。ここからわかるように、たとえば上の例でのcontributorsのリストのルート要素は、コレクションがもし空であれば「contributors」ではなく「nilクラス」になってしまうということです。:rootオプションを使って、ルート要素を統一することもできます。

子ノードの名前は、デフォルトではルートノードを単数形にしたものが使われます。上の例で言うと「contributor」や「object」です。:childrenオプションを使うと、これらをノード名として設定できます。

デフォルトのXMLビルダは、Builder::XmlMarkupから直接生成されたインスタンスです。:builderオブションを使って独自のビルダを構成できます。このメソッドでは:dasherizeやその同族と同様のオプションが利用でき、指定したオプションはビルダに転送されます。

Contributor.limit(2).order(:rank).to_xml(skip_types: true)
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <contributors>
#   <contributor>
#     <id>4356</id>
#     <name>Jeremy Kemper</name>
#     <rank>1</rank>
#     <url-id>jeremy-kemper</url-id>
#   </contributor>
#   <contributor>
#     <id>4404</id>
#     <name>David Heinemeier Hansson</name>
#     <rank>2</rank>
#     <url-id>david-heinemeier-hansson</url-id>
#   </contributor>
# </contributors>

定義はactive_support/core_ext/array/conversions.rbにあります。

10.5 ラッピング

Array.wrapメソッドは、配列の中にある引数が配列 (または配列的なもの) になっていない場合に、それらを配列の中にラップします。

特徴:

  • 引数がnilの場合、空の配列が返されます。
  • 上記以外の場合で、引数がto_aryに応答する場合はto_aryが呼び出され、to_aryの値がnilでない場合はその値が返されます。
  • 上記以外の場合、引数を内側に含んだ配列 (要素が1つだけの配列) が返されます。
Array.wrap(nil)       # => []
Array.wrap([1, 2, 3]) # => [1, 2, 3]
Array.wrap(0)         # => [0]

このメソッドの目的はKernel#Arrayと似ていますが、いくつかの相違点があります。

  • 引数がto_aryに応答する場合、このメソッドが呼び出されます。nilが返された場合、Kernel#Arrayto_aを適用しようと動作を続けますが、Array.wrapはその場で、引数を単一の要素として持つ配列を返します。
  • to_aryから返された値がnilでもArrayオブジェクトでもない場合、Kernel#Arrayは例外を発生しますが、Array.wrapは例外を発生せずに単にその値を返します。
  • このメソッドは引数に対してto_aを呼び出しませんが、この引数が to_ary に応答しない場合、引数を単一の要素として持つ配列を返します。

最後の性質は、列挙型同士を比較する場合に特に便利です。

Array.wrap(foo: :bar) # => [{:foo=>:bar}]
Array(foo: :bar)      # => [[:foo, :bar]]

この動作は、スプラット演算子を用いる手法にも関連します。

[*object]

定義はactive_support/core_ext/array/wrap.rbにあります。

10.6 複製

Array#deep_dupメソッドは、自分自身を複製すると同時に、その中のすべてのオブジェクトをActive SupportのObject#deep_dupメソッドによって再帰的に複製します。この動作は、Array#mapを用いてdeep_dupメソッドを内部の各オブジェクトに適用するのと似ています。

array = [1, [2, 3]]
dup = array.deep_dup
dup[1][2] = 4
array[1][2] == nil   # => true

定義はactive_support/core_ext/object/deep_dup.rbにあります。

10.7 グループ化

10.7.1 in_groups_of(number, fill_with = nil)

in_groups_ofメソッドは、指定のサイズで配列を連続したグループに分割し、分割されたグループを含む配列を1つ返します。

[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]

ブロックが渡された場合はyieldします。

<% sample.in_groups_of(3) do |a, b, c| %>
  <tr>
    <td><%= a %></td>
    <td><%= b %></td>
    <td><%= c %></td>
  </tr>
<% end %>

最初の例では、in_groups_ofメソッドは最後のグループをなるべくnil要素で埋め、指定のサイズを満たすようにしています。空きを埋める値は2番目のオプション引数で指定できます。

[1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]]

2番目のオプション引数にfalseを渡すと、最後のグループの空きは詰められます。

[1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]]

このため、falseは空きを埋める値としては利用できません。

定義はactive_support/core_ext/array/grouping.rbにあります。

10.7.2 in_groups(number, fill_with = nil)

in_groupsは、配列を指定の個数のグループに分割し、分割されたグループを含む配列を1つ返します。

%w(1 2 3 4 5 6 7).in_groups(3)
# => [["1", "2", "3"], ["4", "5", nil], ["6", "7", nil]]

ブロックが渡された場合はyieldします。

%w(1 2 3 4 5 6 7).in_groups(3) {|group| p group}
["1", "2", "3"]
["4", "5", nil]
["6", "7", nil]

この例では、in_groupsメソッドは一部のグループの後ろを必要に応じてnil要素で埋めているのがわかります。1つのグループには、このような余分な要素がグループの一番右側に必要に応じて最大で1つ置かれる可能性があります。また、そのような値を持つグループは、常に全体の中で最後のグループになります。

空きを埋める値は2番目のオプション引数で指定できます。

%w(1 2 3 4 5 6 7).in_groups(3, "0")
# => [["1", "2", "3"], ["4", "5", "0"], ["6", "7", "0"]]

2番目のオプション引数にfalseを渡すと、要素の個数の少ないグループの空きは詰められます。

%w(1 2 3 4 5 6 7).in_groups(3, false)
# => [["1", "2", "3"], ["4", "5"], ["6", "7"]]

このため、falseは空きを埋める値としては利用できません。

定義はactive_support/core_ext/array/grouping.rbにあります。

10.7.3 split(value = nil)

splitメソッドは、指定のセパレータで配列を分割し、分割されたチャンクを返します。

ブロックを渡した場合、配列の要素のうち「ブロックがtrueを返す要素」がセパレータとして使われます。

(-5..5).to_a.split { |i| i.multiple_of?(4) }
# => [[-5], [-3, -2, -1], [1, 2, 3], [5]]

ブロックを渡さない場合、引数として受け取った値がセパレータとして使われます。デフォルトのセパレータはnilです。

[0, 1, -5, 1, 1, "foo", "bar"].split(1)
# => [[0], [-5], [], ["foo", "bar"]]

上の例からもわかるように、セパレータが連続すると空の配列になります。

定義はactive_support/core_ext/array/grouping.rbにあります。

11 Hashの拡張

11.1 各種変換

11.1.1 to_xml

to_xmlメソッドは、レシーバをXML表現に変換したものを含む文字列を返します。

{"foo" => 1, "bar" => 2}.to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <hash>
#   <foo type="integer">1</foo>
#   <bar type="integer">2</bar>
# </hash>

具体的には、このメソッドは与えられたペアからに応じてノードを作成します。キーと値のペアが与えられたとき、以下のように動作します。

  • 値がハッシュのとき、キーを:rootとして再帰的な呼び出しを行います。

  • 値が配列の場合、キーを:rootに、キーを単数形化(singularize)したものを:childrenに指定して再帰的な呼び出しを行います。

  • 値が呼び出し可能な(callable)オブジェクトの場合、引数が1つまたは2つ必要です。引数の数に応じて (arityメソッドで確認)、呼び出し可能オブジェクトを呼び出します。第1引数には:rootにキーを指定したもの、第2引数にはキーを単数形化したものが使われます。戻り値は新しいノードです。

  • valueto_xmlメソッドに応答する場合、:rootにキーが指定されます。

  • その他の場合、keyを持ち、ノードがタグとして作成されます。そのノードにはvalueを文字列形式にしたものがテキストノードとして追加されます。valuenilの場合、"nil"属性が"true"に設定されたものが追加されます。:skip_typesオプションがtrueでない (または:skip_typesオプションがない) 場合、「type」属性も以下のマッピングで追加されます。

XML_TYPE_NAMES = {
  "Symbol"     => "symbol",
  "Fixnum"     => "integer",
  "Bignum"     => "integer",
  "BigDecimal" => "decimal",
  "Float"      => "float",
  "TrueClass"  => "boolean",
  "FalseClass" => "boolean",
  "Date"       => "date",
  "DateTime"   => "datetime",
  "Time"       => "datetime"
}

ルートノードはデフォルトでは「hash」ですが、:rootオプションでカスタマイズできます。

デフォルトのXMLビルダは、Builder::XmlMarkupから直接生成されたインスタンスです。:builderオブションで独自のビルダを構成できます。このメソッドでは:dasherizeとその同族と同様のオプションが利用でき、指定したオプションはビルダに転送されます。

定義はactive_support/core_ext/hash/conversions.rbにあります。

11.2 マージ

Rubyには、2つのハッシュをマージするビルトインのHash#mergeメソッドがあります。

{a: 1, b: 1}.merge(a: 0, c: 2)
# => {:a=>0, :b=>1, :c=>2}

Active Supportでは、この他にも便利なハッシュのマージをいくつか提供しています。

11.2.1 reverse_mergereverse_merge!

mergeでキーが衝突した場合、引数のハッシュのキーが優先されます。以下のような定形の手法を利用すれば、デフォルト値付きオプションハッシュを簡潔に書けます。

options = {length: 30, omission: "..."}.merge(options)

Active Supportでは、別の記法を使いたい場合のためにreverse_mergeも定義されています。

options = options.reverse_merge(length: 30, omission: "...")

マージを対象内で行なう破壊的なバージョンのreverse_merge!もあります。

options.reverse_merge!(length: 30, omission: "...")

reverse_merge!は呼び出し元のハッシュを変更する可能性があることにご注意ください。それが意図した副作用であるかそうでないかにかかわらず、注意が必要です。

定義はactive_support/core_ext/hash/reverse_merge.rbにあります。

11.2.2 reverse_update

reverse_updateメソッドは、上で説明したreverse_merge!の別名です。

reverse_updateには!のついたバージョンはありません。

定義はactive_support/core_ext/hash/reverse_merge.rbにあります。

11.2.3 deep_mergedeep_merge!

先の例で説明したとおり、キーがレシーバと引数で重複している場合、引数の側の値が優先されます。

Active SupportではHash#deep_mergeが定義されています。ディープマージでは、レシーバと引数の両方に同じキーが出現し、さらにどちらも値がハッシュである場合に、その下位のハッシュをマージしたものが、最終的なハッシュの値として使われます。

{a: {b: 1}}.deep_merge(a: {c: 2})
# => {:a=>{:b=>1, :c=>2}}

deep_merge!メソッドはディープマージを破壊的に実行します。

定義はactive_support/core_ext/hash/deep_merge.rbにあります。

11.3 ディープ複製

Hash#deep_dupメソッドは、自分自身の複製に加えて その中のすべてのキーと値を再帰的に複製します。複製にはActive SupportのObject#deep_dupメソッドが使われます。この動作は、Enumerator#each_with_objectを用いてdeep_dupを内部の各キーバリューペアに送信するのと似ています。

hash = { a: 1, b: { c: 2, d: [3, 4] } }

dup = hash.deep_dup
dup[:b][:e] = 5
dup[:b][:d] << 5

hash[:b][:e] == nil      # => true
hash[:b][:d] == [3, 4]   # => true

定義はactive_support/core_ext/object/deep_dup.rbにあります。

11.4 ハッシュキーの操作

11.4.1 exceptexcept!

exceptメソッドは、引数で指定されたキーがあればレシーバのハッシュから取り除きます。

{a: 1, b: 2}.except(:a) # => {:b=>2}

レシーバがconvert_keyに応答する場合、このメソッドはすべての引数に対して呼び出されます。そのおかげで、たとえばハッシュのwith_indifferent_accessexceptメソッドが期待どおりに動作します。

{a: 1}.with_indifferent_access.except(:a)  # => {}
{a: 1}.with_indifferent_access.except("a") # => {}

レシーバーからキーを取り除く破壊的なexcept!もあります。

定義はactive_support/core_ext/hash/except.rbにあります。

11.4.2 stringify_keysstringify_keys!

stringify_keysメソッドは、レシーバのハッシュキーを文字列に変換したハッシュを返します。具体的には、レシーバのハッシュキーに対してto_sを送信しています。

{nil => nil, 1 => 1, a: :a}.stringify_keys
# => {"" => nil, "1" => 1, "a" => :a}

キーが重複している場合、ハッシュに最も最近挿入された値が使われます。

{"a" => 1, a: 2}.stringify_keys
# 値は以下になる
# => {"a"=>2}

このメソッドは、シンボルと文字列が両方含まれているハッシュをオプションとして受け取る場合に便利なことがあります。たとえば、ActionView::Helpers::FormHelperでは以下のように定義されています。

def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
  options = options.stringify_keys
  options["type"] = "checkbox"
  ...
end

stringify_keysメソッドのおかげで、2行目で「type」キーに安全にアクセスできます。:typeのようなシンボルでも「"type"」のような文字列でも指定できます。

レシーバーのキーを直接文字列化する破壊的なstringify_keys!もあります。

また、deep_stringify_keysdeep_stringify_keys!を使うと、与えられたハッシュのすべてのキーを文字列化し、その中にネストされているすべてのハッシュのキーを文字列化することもできます。以下に例を示します。

{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys
# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}

定義はactive_support/core_ext/hash/keys.rbにあります。

11.4.3 symbolize_keyssymbolize_keys!

symbolize_keysメソッドは、レシーバのハッシュキーをシンボルに変換したハッシュを返します。具体的には、レシーバのハッシュキーに対してto_symを送信しています。

{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
# => {nil=>nil, 1=>1, :a=>"a"}

上の例では、3つのキーのうち最後の1つしかシンボルに変換されていないことにご注意ください。数字やnilはシンボルに変換されません。

キーが重複している場合、ハッシュに最も最近挿入された値が使われます。

{"a" => 1, a: 2}.symbolize_keys
# 値は以下になる
# => {:a=>2}

このメソッドは、シンボルと文字列が両方含まれているハッシュをオプションとして受け取る場合に便利なことがあります。たとえば、ActionController::UrlRewriterでは以下のように定義されています。

def rewrite_path(options)
  options = options.symbolize_keys
  options.update(options[:params].symbolize_keys) if options[:params]
  ...
end

symbolize_keysメソッドのおかげで、2行目で:paramsキーに安全にアクセスできています。:paramsのようなシンボルでも「"params"」のような文字列でも指定できます。

レシーバーのキーを直接シンボルに変換する破壊的なsymbolize_keys!もあります。

また、deep_symbolize_keysdeep_symbolize_keys!を使うと、与えられたハッシュのすべてのキーと、その中にネストされているすべてのハッシュのキーをシンボルに変換することもできます。以下に例を示します。

{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys
# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}

定義はactive_support/core_ext/hash/keys.rbにあります。

11.4.4 to_optionsto_options!

to_optionsメソッドとto_options!メソッドは、それそれsymbolize_keysメソッドとsymbolize_keys!メソッドの別名です。

定義はactive_support/core_ext/hash/keys.rbにあります。

11.4.5 assert_valid_keys

assert_valid_keysメソッドは任意の数の引数を取ることができ、ホワイトリストに含まれていないキーがレシーバにあるかどうかをチェックします。そのようなキーが見つかった場合、ArgumentErrorが発生します。

{a: 1}.assert_valid_keys(:a)  # パスする
{a: 1}.assert_valid_keys("a") # ArgumentError

たとえばActive Recordは、関連付けをビルドするときに未知のオプションを受け付けません。Active Recordはassert_valid_keysによる制御を実装しています。

定義はactive_support/core_ext/hash/keys.rbにあります。

11.5 スライス

破壊的なスライス操作を行なうslice!メソッドは、指定のキーのみを置き換え、削除されたキー/値ペアを含むハッシュを1つ返します。

hash = {a: 1, b: 2}
rest = hash.slice!(:a) # => {:b=>2}
hash                   # => {:a=>1}

定義はactive_support/core_ext/hash/slice.rbにあります。

11.6 抽出

extract!メソッドは、与えられたキーにマッチするキー/値ペアを取り除き、取り除いたペアを返します。

hash = {a: 1, b: 2}
rest = hash.extract!(:a) # => {:a=>1}
hash                     # => {:b=>2}

extract!メソッドは、レシーバのハッシュのサブクラスと同じサブクラスを返します。

hash = {a: 1, b: 2}.with_indifferent_access
rest = hash.extract!(:a).class
# => ActiveSupport::HashWithIndifferentAccess

定義はactive_support/core_ext/hash/slice.rbにあります。

11.7 ハッシュキーがシンボルでも文字列でも同様に扱う(indifferent access)

with_indifferent_accessメソッドは、レシーバに対してActiveSupport::HashWithIndifferentAccessを実行した結果を返します。

{a: 1}.with_indifferent_access["a"] # => 1

定義はactive_support/core_ext/hash/indifferent_access.rbにあります。

12 Regexpの拡張

12.1 multiline?

multiline?メソッドは、正規表現に/mフラグが設定されているかどうかをチェックします。このフラグが設定されていると、ドット(.)が改行にマッチし、複数行を扱えるようになります。

%r{.}.multiline?  # => false
%r{.}m.multiline? # => true

Regexp.new('.').multiline?                    # => false
Regexp.new('.', Regexp::MULTILINE).multiline? # => true

Railsはこのメソッドをある一箇所、ルーティングコードで利用しています。ルーティングでは正規表現で複数行を扱うことを許していないので、このフラグで制限を加えています。

def verify_regexp_requirements(requirements)
  ...
  if requirement.multiline?
    raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
  end
  ...
end

定義はactive_support/core_ext/regexp.rbにあります。

13 Rangeの拡張

13.1 to_s

Active SupportはRange#to_sメソッドを拡張してフォーマット引数をオプションで受け付けるようにしています。執筆時点では、デフォルトでないフォーマットとしてサポートされているのは:dbのみです。

(Date.today..Date.tomorrow).to_s
# => "2009-10-25..2009-10-26"

(Date.today..Date.tomorrow).to_s(:db)
# => "BETWEEN '2009-10-25' AND '2009-10-26'"

上の例でもわかるように、フォーマットに:dbを指定するとSQLのBETWEEN句が生成されます。このフォーマットは、Active Recordで条件の値の範囲をサポートするときに使われます。

定義はactive_support/core_ext/range/conversions.rbにあります。

13.2 ===include?cover?

Range#===メソッド、Range#include?メソッド、Range#cover?メソッドは、与えられたインスタンスの範囲内に値が収まっているかどうかをチェックします。

(2..3).include?(Math::E) # => true

Active Supportではこれらのメソッドを拡張して、他の範囲指定を引数で指定できるようにしています。この場合、引数の範囲がレシーバの範囲の中に収まっているかどうかがチェックされています。

(1..10) === (3..7)  # => true
(1..10) === (0..7)  # => false
(1..10) === (3..11) # => false
(1...9) === (3..9)  # => false

(1..10).include?(3..7)  # => true
(1..10).include?(0..7)  # => false
(1..10).include?(3..11) # => false
(1...9).include?(3..9)  # => false

(1..10).cover?(3..7)  # => true
(1..10).cover?(0..7)  # => false
(1..10).cover?(3..11) # => false
(1...9).cover?(3..9)  # => false

定義はactive_support/core_ext/range/compare_range.rbにあります。

13.3 overlaps?

Range#overlaps?メソッドは、与えられた2つの範囲に(空白でない)重なりがあるかどうかをチェックします。

(1..10).overlaps?(7..11)  # => true
(1..10).overlaps?(0..7)   # => true
(1..10).overlaps?(11..27) # => false

定義はactive_support/core_ext/range/overlaps.rbにあります。

14 Dateの拡張

14.1 計算

以下の計算方法の一部では1582年10月をエッジケースとして用いています。この月にユリウス暦からグレゴリオ暦への切り替えが行われたため、10月5日から10月14日までが存在しません。本ガイドはこの「特殊な月」について長々と解説することはしませんが、メソッドがこの月でも期待どおりに動作することについては説明しておきたいと思います。具体的には、たとえばDate.new(1582, 10, 4).tomorrowを実行するとDate.new(1582, 10, 15)が返されます。期待どおりに動作することは、Active Supportのtest/core_ext/date_ext_test.rb用のテストスイートで確認できます。

14.1.1 Date.current

Active Supportでは、Date.currentを定義して現在のタイムゾーンにおける「今日」を定めています。このメソッドはDate.todayと似ていますが、ユーザー定義のタイムゾーンがある場合にそれを考慮する点が異なります。Active SupportではDate.yesterdayメソッドとDate.tomorrowも定義しています。インスタンスではpast?today?future?on_weekday?on_weekend?を利用でき、これらはすべてDate.currentを起点として導かれます。

ユーザー定義のタイムゾーンを考慮するメソッドを用いて日付を比較したい場合、Date.todayではなく必ずDate.currentを使ってください。将来、ユーザー定義のタイムゾーンがシステムのタイムゾーンと比較されることがありえます。システムのタイムゾーンではデフォルトでDate.todayが使われます。つまり、Date.todayDate.yesterdayと等しくなることがありえるということです。

定義はactive_support/core_ext/date/calculations.rbにあります。

14.1.2 名前付き日付
14.1.2.1 beginning_of_weekend_of_week

beginning_of_weekメソッドとend_of_weekメソッドは、それぞれ週の最初の日付と週の最後の日付を返します。週の始まりはデフォルトでは月曜日ですが、引数を渡して変更できます。そのときにスレッドローカルのDate.beginning_of_weekまたはconfig.beginning_of_weekを設定します。

d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
d.beginning_of_week          # => Mon, 03 May 2010
d.beginning_of_week(:sunday) # => Sun, 02 May 2010
d.end_of_week                # => Sun, 09 May 2010
d.end_of_week(:sunday)       # => Sat, 08 May 2010

beginning_of_weekat_beginning_of_weekの別名、end_of_weekat_end_of_weekの別名です。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.2.2 mondaysunday

mondayメソッドはその日の「前の月曜(の日付)」を、sundayメソッドはその日の「次の日曜(の日付)」をそれぞれ返します。

d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
d.monday                     # => Mon, 03 May 2010
d.sunday                     # => Sun, 09 May 2010

d = Date.new(2012, 9, 10)    # => Mon, 10 Sep 2012
d.monday                     # => Mon, 10 Sep 2012

d = Date.new(2012, 9, 16)    # => Sun, 16 Sep 2012
d.sunday                     # => Sun, 16 Sep 2012

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.2.3 prev_weeknext_week

next_weekメソッドは、英語表記 (デフォルトではスレッドローカルのDate.beginning_of_weekまたはconfig.beginning_of_weekまたは:monday) の日付名のシンボルを受け取り、それに対応する日付を返します。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.next_week              # => Mon, 10 May 2010
d.next_week(:saturday)   # => Sat, 15 May 2010

prev_weekも同様です。

d.prev_week              # => Mon, 26 Apr 2010
d.prev_week(:saturday)   # => Sat, 01 May 2010
d.prev_week(:friday)     # => Fri, 30 Apr 2010

prev_weeklast_weekの別名です。

Date.beginning_of_weekまたはconfig.beginning_of_weekが設定されていれば、next_weekprev_weekはどちらも正常に動作します。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.2.4 beginning_of_monthend_of_month

beginning_of_monthメソッドはその月の「最初の日」、end_of_monthメソッドはその月の「最後の日」をそれぞれ返します。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_month     # => Sat, 01 May 2010
d.end_of_month           # => Mon, 31 May 2010

beginning_of_monthat_beginning_of_monthの別名、end_of_monthat_end_of_monthの別名です。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.2.5 beginning_of_quarterend_of_quarter

beginning_of_quarterメソッドとend_of_quarterメソッドは、レシーバのカレンダーの年における四半期の「最初の日」と「最後の日」をそれぞれ返します。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_quarter   # => Thu, 01 Apr 2010
d.end_of_quarter         # => Wed, 30 Jun 2010

beginning_of_quarterat_beginning_of_quarterの別名、end_of_quarterat_end_of_quarterの別名です。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.2.6 beginning_of_yearend_of_year

beginning_of_yearメソッドとend_of_yearメソッドは、その年の「最初の日」と「最後の日」をそれぞれ返します。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_year      # => Fri, 01 Jan 2010
d.end_of_year            # => Fri, 31 Dec 2010

beginning_of_yearat_beginning_of_yearの別名、end_of_yearat_end_of_yearの別名です。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.3 その他の日付計算メソッド
14.1.3.1 years_agoyears_since

years_agoメソッドは、年数を受け取り、その年数前の同じ日付を返します。

date = Date.new(2010, 6, 7)
date.years_ago(10) # => Wed, 07 Jun 2000

years_sinceも同じ要領で、その年数後の同じ日付を返します。

date = Date.new(2010, 6, 7)
date.years_since(10) # => Sun, 07 Jun 2020

同じ日が行き先の月にない場合、その月の最後の日が返されます。

Date.new(2012, 2, 29).years_ago(3)     # => Sat, 28 Feb 2009
Date.new(2012, 2, 29).years_since(3)   # => Sat, 28 Feb 2015

last_year#years_ago(1)のショートハンドです。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.3.2 months_agomonths_since

months_agoメソッドとmonths_sinceメソッドは、上と同じ要領で月に対して行います。

Date.new(2010, 4, 30).months_ago(2)   # => Sun, 28 Feb 2010
Date.new(2010, 4, 30).months_since(2) # => Wed, 30 Jun 2010

対象の月に同じ日がない場合は、その月の最後の日が返されます。

Date.new(2010, 4, 30).months_ago(2)    # => Sun, 28 Feb 2010
Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010

last_month#months_ago(1)のショートハンドです。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.3.3 weeks_ago

weeks_agoメソッドは、同じ要領で週に対して行います。

Date.new(2010, 5, 24).weeks_ago(1)    # => Mon, 17 May 2010
Date.new(2010, 5, 24).weeks_ago(2)    # => Mon, 10 May 2010

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.3.4 advance

advanceメソッドは、日付を移動する最も一般的な方法です。このメソッドは:years:months:weeks:daysをキーに持つハッシュを受け取り、日付をできるだけ詳細な形式で、現在のキーで示されるとおりに返します。

date = Date.new(2010, 6, 6)
date.advance(years: 1, weeks: 2)  # => Mon, 20 Jun 2011
date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010

上の例にも示されているように、増分値には負の数も指定できます。

計算の順序は、最初に年を増減し、次に月、最後に日を増減します。この順序で計算していることは、特に月を計算する時に重要です。たとえば、現在が2010年2月の最後の日で、そこから1か月と1日先に進めたいとします。

advanceメソッドは最初に月を進め、それから日を進めます。それにより以下の結果を得ます。

Date.new(2010, 2, 28).advance(months: 1, days: 1)
# => Sun, 29 Mar 2010

計算の順序が異なる場合、同じ結果が得られない可能性があります。

Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
# => Thu, 01 Apr 2010

定義はactive_support/core_ext/date/calculations.rbにあります。

14.1.4 要素の変更

changeメソッドは、与えられた年、月、日に応じてレシーバの日付を変更し、与えられなかった部分はそのままにしてその日付を返します。

Date.new(2010, 12, 23).change(year: 2011, month: 11)
# => Wed, 23 Nov 2011

存在しない日付が指定されるとArgumentErrorが発生します。

Date.new(2010, 1, 31).change(month: 2)
# => ArgumentError: invalid date

定義はactive_support/core_ext/date/calculations.rbにあります。

14.1.5 期間(duration)

日付に対して期間を加減算できます。

d = Date.current
# => Mon, 09 Aug 2010
d + 1.year
# => Tue, 09 Aug 2011
d - 3.hours
# => Sun, 08 Aug 2010 21:00:00 UTC +00:00

これらの計算は、内部でsinceメソッドやadvanceメソッドに置き換えられます。たとえば、作り直したカレンダー内で正しくジャンプできます。

Date.new(1582, 10, 4) + 1.day
# => Fri, 15 Oct 1582

14.1.6 タイムスタンプ

以下のメソッドは可能であればTimeオブジェクトを返し、それ以外の場合はDateTimeを返します。ユーザーのタイムゾーンが設定されていればそれも加味されます。

14.1.6.1 beginning_of_dayend_of_day

beginning_of_dayメソッドは、その日の開始時点 (00:00:00) のタイムスタンプを返します。

date = Date.new(2010, 6, 7)
date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010

end_of_dayメソッドは、その日の最後の時点 (23:59:59) のタイムスタンプを返します。

date = Date.new(2010, 6, 7)
date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010

at_beginning_of_daymidnightat_midnightは、beginning_of_dayの別名です。

定義はactive_support/core_ext/date/calculations.rbにあります。

14.1.6.2 beginning_of_hourend_of_hour

beginning_of_hourメソッドは、その時の最初の時点 (hh:00:00) のタイムスタンプを返します。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010

end_of_hourメソッドは、その時の最後の時点 (hh:59:59) のタイムスタンプを返します。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010

beginning_of_hourat_beginning_of_hourの別名です。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.6.3 beginning_of_minuteend_of_minute

beginning_of_minuteは、その分の最初の時点 (hh:mm:00) のタイムスタンプを返します。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010

end_of_minuteメソッドは、その分の最後の時点 (hh:mm:59) のタイムスタンプを返します。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010

beginning_of_minuteat_beginning_of_minuteの別名です。

beginning_of_hourend_of_hourbeginning_of_minuteend_of_minuteTimeおよびDateTimeへの実装ですが、Dateへの実装では ありませんDateインスタンスに対して時間や分の最初や最後を問い合わせる意味はありません。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

14.1.6.4 agosince

agoメソッドは秒数を引数として受け取り、真夜中の時点からその秒数だけさかのぼった時点のタイムスタンプを返します。

date = Date.current # => Fri, 11 Jun 2010
date.ago(1)         # => Thu, 10 Jun 2010 23:59:59 EDT -04:00

sinceメソッドは、同様にその秒数だけ先に進みます。

date = Date.current # => Fri, 11 Jun 2010
date.since(1)       # => Fri, 11 Jun 2010 00:00:01 EDT -04:00

定義はactive_support/core_ext/date/calculations.rbにあります。

14.1.7 その他の時間計算

14.2 各種変換

15 DateTimeの拡張

DateTimeは夏時間 (DST) ルールについては関知しません。夏時間の変更が行われた場合、メソッドの一部がこのとおりに動作しないことがあります。たとえば、seconds_since_midnightメソッドが返す秒数が実際の総量と合わない可能性があります。

15.1 計算

DateTimeクラスはDateのサブクラスであり、active_support/core_ext/date/calculations.rbを読み込むことでこれらのメソッドと別名を継承することができます。ただしこれらは常にdatetimesを返す点が異なります。

以下のメソッドはすべて再実装されるため、これらを用いるためにactive_support/core_ext/date/calculations.rbを読み込む必要は ありません

beginning_of_day (midnight, at_midnight, at_beginning_of_day)
end_of_day
ago
since (in)

他方、advancechangeも定義されていて、さらに多くのオプションをサポートしています。これらについては後述します。

以下のメソッドはactive_support/core_ext/date_time/calculations.rbにのみ実装されています。これらはDateTimeインスタンスに対して使わないと意味がないためです。

beginning_of_hour (at_beginning_of_hour)
end_of_hour

15.1.1 名前付き日付時刻
15.1.1.1 DateTime.current

Active Supportでは、DateTime.currentTime.now.to_datetimeと同様に定義しています。ただし、DateTime.currentはユーザータイムゾーンが定義されている場合に対応する点が異なります。Active SupportではDate.yesterdayメソッドとDate.tomorrowも定義しています。インスタンスではpast?future?を利用でき、これらはDate.currentを起点として導かれます。

定義はactive_support/core_ext/date_time/calculations.rbにあります。

15.1.2 その他の拡張
15.1.2.1 seconds_since_midnight

seconds_since_midnightメソッドは、真夜中からの経過秒数を返します。

now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
now.seconds_since_midnight # => 73596

定義はactive_support/core_ext/date_time/calculations.rbにあります。

15.1.2.2 utc

utcメソッドは、レシーバの日付時刻をUTCで返します。

now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
now.utc                # => Mon, 07 Jun 2010 23:27:52 +0000

getutcはこのメソッドの別名です。

15.1.2.3 utc?

utc?述語メソッドは、レシーバがそのタイムゾーンに合ったUTC時刻を持っているかどうかをチェックします。

now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
now.utc?          # => false
now.utc.utc?      # => true

定義はactive_support/core_ext/date_time/calculations.rbにあります。

15.1.2.4 advance

advanceメソッドは、日時を移動する最も一般的な方法です。このメソッドは:years:months:weeks:days:hours:minutesおよび:secondsをキーに持つハッシュを受け取り、日時をできるだけ詳細な形式で、現在のキーで示されるとおりに返します。

d = DateTime.current
# => Thu, 05 Aug 2010 11:33:31 +0000
d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1)
# => Tue, 06 Sep 2011 12:34:32 +0000

このメソッドはまず、上で説明されているDate#advanceに対する経過年(:years)、経過月 (:months)、経過週 (:weeks)、経過日 (:days) を元に移動先の日付を算出します。続いて、算出された時点までの経過秒数を元にsinceメソッドを呼び出し、時間を補正します。この実行順序には意味があります。極端なケースでは、順序が変わると計算結果も異なる場合があります。これは上のDate#advanceで示した例で適用されます。相対的な時間の計算においても計算の順序は同様に重要です。

もし仮に日付部分を先に進め (前述したとおり、相対的な計算順序があります)、続いて時間の部分も先に進めると、以下のような計算結果が得られます。

d = DateTime.new(2010, 2, 28, 23, 59, 59)
# => Sun, 28 Feb 2010 23:59:59 +0000
d.advance(months: 1, seconds: 1)
# => Mon, 29 Mar 2010 00:00:00 +0000

今度は順序を変えて計算すると、結果が異なります。

d.advance(seconds: 1).advance(months: 1)
# => Thu, 01 Apr 2010 00:00:00 +0000

DateTimeは夏時間 (DST) を考慮しません。算出された時間が最終的に存在しない時間になっても警告やエラーは発生しません。

定義はactive_support/core_ext/date_time/calculations.rbにあります。

15.1.3 要素の変更

changeメソッドを使うと、レシーバの日時の一部の要素だけを更新した新しい日時を得られます。変更する要素として、:year:month:day:hour:min:sec:offset:startなどを指定できます。

now = DateTime.current
# => Tue, 08 Jun 2010 01:56:22 +0000
now.change(year: 2011, offset: Rational(-6, 24))
# => Wed, 08 Jun 2011 01:56:22 -0600

時 (hour) がゼロの場合、分と秒も値を与えられない限り同様にゼロになります。

now.change(hour: 0)
# => Tue, 08 Jun 2010 00:00:00 +0000

同様に、分がゼロの場合、秒も値を与えられない限りゼロになります。

now.change(min: 0)
# => Tue, 08 Jun 2010 01:00:00 +0000

存在しない日付が指定されるとArgumentErrorが発生します。

DateTime.current.change(month: 2, day: 30)
# => ArgumentError: invalid date

定義はactive_support/core_ext/date_time/calculations.rbにあります。

15.1.4 期間(duration)

日時に対して期間を加減算できます。

now = DateTime.current
# => Mon, 09 Aug 2010 23:15:17 +0000
now + 1.year
# => Tue, 09 Aug 2011 23:15:17 +0000
now - 1.week
# => Mon, 02 Aug 2010 23:15:17 +0000

これらの計算は、内部でsinceメソッドやadvanceメソッドに置き換えられます。たとえば、作り直したカレンダー内で正しくジャンプできます。

DateTime.new(1582, 10, 4, 23) + 1.hour
# => Fri, 15 Oct 1582 00:00:00 +0000

16 Timeの拡張

16.1 計算

これらは同様に動作します。関連するドキュメントを参照し、以下の相違点についても把握しておいてください。

  • changeメソッドは追加の:usecも受け付けます。
  • Timeは夏時間 (DST) を理解します。以下のように夏時間を正しく算出できます。
Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>

# バルセロナでは夏時間により2010/03/28 02:00 +0100が2010/03/28 03:00 +0200になる
t = Time.local(2010, 3, 28, 1, 59, 59)
# => Sun Mar 28 01:59:59 +0100 2010
t.advance(seconds: 1)
# => Sun Mar 28 03:00:00 +0200 2010

  • sinceagoの移動先の時間がTimeで表現できない場合、DateTimeオブジェクトが代わりに返されます。
16.1.1 Time.current

Active Supportでは、Time.currentを定義して現在のタイムゾーンにおける「今日」を定めています。このメソッドはTime.nowと似ていますが、ユーザー定義のタイムゾーンがある場合にそれを考慮する点が異なります。Active Supportではpast?today?future?を示すインスタンス述語も定義されており、これらはすべてこのTime.currentを起点にしています。

ユーザー定義のタイムゾーンを考慮するメソッドを用いて日付を比較したい場合、Time.nowではなく必ずTime.currentを使ってください。将来、ユーザー定義のタイムゾーンがシステムのタイムゾーンと比較されることがありえます。システムのタイムゾーンではデフォルトでTime#nowが使われます。つまり、Time.nowTime.currentyesterdayと等しくなることがありえるということです。

定義はactive_support/core_ext/time/calculations.rbにあります。

16.1.2 all_dayall_weekall_monthall_quarterall_year

all_dayメソッドは、現在時刻を含む「その日一日」を表す範囲を返します。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_day
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00

同様に、all_weekall_monthall_quarterall_yearも時間の範囲を生成できます。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_week
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
now.all_week(:sunday)
# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
now.all_month
# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
now.all_quarter
# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
now.all_year
# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

16.1.3 prev_daynext_day

prev_daynext_dayは、その日の「前日」または「翌日」の日時をそれぞれ返します。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_day               # => 2010-05-07 00:00:00 +0900
t.next_day               # => 2010-05-09 00:00:00 +0900

定義はactive_support/core_ext/time/calculations.rbにあります。

16.1.4 prev_monthnext_month

prev_monthnext_monthは、「前月」または「翌月」の同じ日の日時をそれぞれ返します。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_month             # => 2010-04-08 00:00:00 +0900
t.next_month             # => 2010-06-08 00:00:00 +0900

該当する日付が存在しない場合、対応する月の最終日が返されます。

Time.new(2000, 5, 31).prev_month # => 2000-04-30 00:00:00 +0900
Time.new(2000, 3, 31).prev_month # => 2000-02-29 00:00:00 +0900
Time.new(2000, 5, 31).next_month # => 2000-06-30 00:00:00 +0900
Time.new(2000, 1, 31).next_month # => 2000-02-29 00:00:00 +0900

定義はactive_support/core_ext/time/calculations.rbにあります。

16.1.5 prev_yearnext_year

prev_yearnext_yearは、「前年」または「翌年」の同じ月日の日時をそれぞれ返します。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_year              # => 2009-05-08 00:00:00 +0900
t.next_year              # => 2011-05-08 00:00:00 +0900

うるう年の2月29日の場合、28日の日付が返されます。

t = Time.new(2000, 2, 29) # => 2000-02-29 00:00:00 +0900
t.prev_year               # => 1999-02-28 00:00:00 +0900
t.next_year               # => 2001-02-28 00:00:00 +0900

定義はactive_support/core_ext/time/calculations.rbにあります。

16.1.6 prev_quarternext_quarter

prev_quarternext_quarterは、「前四半期」または「翌四半期」の同じ日の日付をそれぞれ返します。

t = Time.local(2010, 5, 8) # => 2010-05-08 00:00:00 +0300
t.prev_quarter             # => 2010-02-08 00:00:00 +0200
t.next_quarter             # => 2010-08-08 00:00:00 +0300

該当する日付が存在しない場合、対応する月の最終日が返されます。

Time.local(2000, 7, 31).prev_quarter  # => 2000-04-30 00:00:00 +0300
Time.local(2000, 5, 31).prev_quarter  # => 2000-02-29 00:00:00 +0200
Time.local(2000, 10, 31).prev_quarter # => 2000-07-31 00:00:00 +0300
Time.local(2000, 11, 31).next_quarter # => 2001-03-01 00:00:00 +0200

prev_quarterlast_quarterのエイリアスです。

定義はactive_support/core_ext/date_and_time/calculations.rbにあります。

16.2 時間コンストラクタ

ユーザータイムゾーンが定義されている場合、Active Supportが定義するTime.currentの値はTime.zone.nowの値と同じになります。ユーザータイムゾーンが定義されていない場合は、Time.nowと同じになります。

Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
Time.current
# => Fri, 06 Aug 2010 17:11:58 CEST +02:00

DateTimeと同様、述語past?future?Time.currentを起点とします。

構成される時間が、実行プラットフォームのTimeでサポートされる範囲を超えている場合は、usecは破棄され、DateTimeオブジェクトが代りに返されます。

16.2.1 期間(duration)

Timeオブジェクトに対して期間を加減算できます。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now + 1.year
# => Tue, 09 Aug 2011 23:21:11 UTC +00:00
now - 1.week
# => Mon, 02 Aug 2010 23:21:11 UTC +00:00

これらの計算は、内部でsinceメソッドやadvanceメソッドに置き換えられます。たとえば、作り直したカレンダー内で正しくジャンプできます。

Time.utc(1582, 10, 3) + 5.days
# => Mon Oct 18 00:00:00 UTC 1582

17 Fileの拡張

17.1 atomic_write

File.atomic_writeクラスメソッドを使うと、書きかけの文章を誰にも読まれないようにファイルを保存できます。

このメソッドにファイル名を引数として渡すと、書き込み用にオープンされたファイルハンドルを生成します。ブロックが完了すると、atomic_writeはファイルハンドルをクローズして処理を完了します。

Action Packは、このメソッドを利用してall.cssなどのキャッシュファイルへの書き込みを行ないます。

File.atomic_write(joined_asset_path) do |cache|
  cache.write(join_asset_file_contents(asset_paths))
end

atomic_writeは、処理を完了するために一時的なファイルを作成します。ブロック内のコードが実際に書き込むのはこのファイルです。この一時ファイルは完了時にリネームされます。リネームは、POSIXシステムのアトミック操作に基いて行われます。書き込み対象ファイルが既に存在する場合、atomic_writeはそれを上書きしてオーナーとパーミッションを保持します。ただし、atomic_writeメソッドがファイルのオーナーシップとパーミッションを変更できないケースがまれにあります。このエラーはキャッチされ、そのファイルがそれを必要とするプロセスからアクセスできるようにするために、ユーザーのファイルシステムへの信頼をスキップします。

atomic_writeが行なうchmod操作が原因で、書き込み対象ファイルがACLセットを持っているときにそのACLが再計算/変更されます。

atomic_writeで追記を行なうことはできません。

この補助ファイルは標準の一時ファイル用ディレクトリに書き込まれますが、2番目の引数でディレクトリを直接指定することもできます。

定義はactive_support/core_ext/file/atomic.rbにあります。

18 Marshalの拡張

18.1 load

Active Supportは、loadに一定の自動読み込みサポートを追加します。

たとえば、ファイルキャッシュストアでは以下のようにデシリアライズ (deserialize) します。

File.open(file_name) { |f| Marshal.load(f) }

キャッシュデータが不明な定数を参照している場合、自動読み込みがトリガされます。読み込みに成功した場合はシリアライズを透過的に再試行します。

引数がIOの場合、再試行を可能にするためにrewindに応答する必要があります。通常のファイルはrewindに応答します。

定義はactive_support/core_ext/marshal.rbにあります。

19 NameErrorの拡張

Active SupportはNameErrormissing_name?メソッドを追加します。このメソッドは、引数として渡された名前が原因で例外が発生するかどうかをテストします。

渡される名前はシンボルまたは文字列です。シンボルを渡した場合は単なる定数名をテストし、文字列を渡した場合はフルパス (fully-qualified) の定数名をテストします。

シンボルは、:"ActiveRecord::Base"で行なっているのと同じようにフルパスの定数として表せます。シンボルがそのように動作するのはそれが便利だからであり、技術的にそうしなければならないというものではありません。

たとえば、ArticlesControllerのアクションが呼び出されると、Railsはその名前からすぐに推測できるArticleHelperを使おうとします。ここではこのヘルパーモジュールが存在していなくても問題はないので、この定数名で例外が発生しても例外として扱わずに黙殺する必要があります。しかし、実際に不明な定数が原因でarticles_helper.rbNameErrorエラーを発生するという場合が考えられます。そのような場合は、改めて例外を発生させなくてはなりません。missing_name?メソッドは、この2つの場合を区別するために使われます。

def default_helper_module!
  module_name = name.sub(/Controller$/, '')
  module_path = module_name.underscore
  helper module_path
rescue LoadError => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

定義はactive_support/core_ext/name_error.rbにあります。

20 LoadErrorの拡張

Active Supportはis_missing?LoadErrorに追加します。

is_missing?は、パス名を引数に取り、特定のファイルが原因で例外が発生するかどうかをテストします (".rb"拡張子が原因と思われる場合を除きます)。

たとえば、ArticlesControllerのアクションが呼び出されると、Railsはarticles_helper.rbを読み込もうとしますが、このファイルは存在しないことがあります。ヘルパーモジュールは必須ではないので、Railsは読み込みエラーを例外扱いせずに黙殺します。しかし、ヘルパーモジュールが存在しないために別のライブラリが必要になり、それがさらに見つからないという場合が考えられます。Railsはそのような場合には例外を再発生させなければなりません。is_missing?メソッドは、この2つの場合を区別するために使われます。

def default_helper_module!
  module_name = name.sub(/Controller$/, '')
  module_path = module_name.underscore
  helper module_path
rescue LoadError => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

定義はactive_support/core_ext/load_error.rbにあります。

[PR] Railsガイドを、もっと便利に。

Railsガイドをもっと便利に使えるサービスをリリースしました! 本サービスで得られた売上はRailsガイドを継続的に更新・運営するために活用させていただきます。よければぜひご検討ください ;)

Proプラン Teamプラン

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、上記リポジトリにてお気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら、「Ruby on Rails に貢献する方法」に記されている Rails のドキュメントに貢献するを参考にしながらぜひ Rails コミュニティに貢献してみてください :)

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。よろしくお願いします。

YassLab 株式会社
https://yasslab.jp/

支援・協賛

Railsガイドは下記のサポーターから継続的な支援を受けています。Railsガイドへの出稿 (支援・協賛枠) にご興味あれば info@yasslab.jp までご連絡ください。

  1. Takeyuweb: エンジニアリングをもっと自由に楽しく
  1. このエントリーをはてなブックマークに追加
  2. Star