Active Support コア拡張機能

Active SupportはRuby on Railsのコンポーネントであり、Ruby言語の拡張やユーティリティを提供します。

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

このガイドの内容:

  • コア拡張機能(Core Extensions)について
  • すべての拡張機能を読み込む方法
  • 必要な拡張機能だけを利用する方法
  • Active Supportが提供する拡張機能一覧

目次

  1. コア拡張機能を読み込む方法
  2. すべてのオブジェクトで使える拡張機能
  3. Moduleの拡張
  4. Classの拡張
  5. Stringの拡張
  6. Numericの拡張
  7. Integerの拡張
  8. BigDecimalの拡張
  9. Enumerableの拡張
  10. Arrayの拡張
  11. Hashの拡張
  12. Regexpの拡張
  13. Rangeの拡張
  14. Dateの拡張
  15. DateTimeの拡張
  16. Timeの拡張
  17. Fileの拡張
  18. NameErrorの拡張
  19. LoadErrorの拡張
  20. Pathnameの拡張
  21. 参考資料(日本語)

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

1.1 単体のActive Support

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

したがって、以下のようなrequire文を実行すると、Active Supportによってrequireされる拡張機能だけが読み込まれます。

require 'active_support'
1.1.1 必要な定義だけを選ぶ

この例では、Hash#with_indifferent_accessの読み込み方を説明します。この拡張機能は、HashActiveSupport::HashWithIndifferentAccessに変換して、以下のように文字列とシンボルのどちらをキーに指定してもアクセスできるようにします。

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

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

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

require "active_support"
require "active_support/core_ext/hash/indifferent_access"

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

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

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

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

require 'active_support'
require "active_support/core_ext/hash"
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?メソッドに応答してtrueを返すオブジェクトはすべて空(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!
  unless self["Cache-Control"].present?
    # ...
  end
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?をお使いください。

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

2.5 try

nilでない場合にのみオブジェクトのメソッドを呼び出したい場合、最も単純な方法は条件文を追加することですが、どこか冗長になってしまいます。そこでtryメソッドを使うという手があります。tryObject#public_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

2.7 acts_like?(duck)

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

def acts_like_string?
end

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

some_klass.acts_like?(:string)

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

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」になるからです。

2.9 to_query

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"

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

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}
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"]

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

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

3 Moduleの拡張

3.1 属性

3.1.1 alias_attribute

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

class User < ApplicationRecord
  # emailカラムを"login"という名前でも参照したい
  # そうすることで認証のコードがわかりやすくなる
  alias_attribute :login, :email
end
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
3.1.3 モジュール属性

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

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

module ActiveStorage
  mattr_accessor :logger
end

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を返します。

module_parent_nameはこの場合にnilを返します。

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を返します。

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]

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

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

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の場合、NoMethodErrorが伝搬します。: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を渡すことで変更できます。

3.4.2 delegate_missing_to

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

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

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

3.5 メソッドの再定義

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

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

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

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を指定すれば定義されなくなります。

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に設定すると、モデルの属性設定時にマスアサインメントを防止するのに便利です。

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]

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

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]

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

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
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!もあります。

5.3 squish

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

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

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

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

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によって防いでいます。

5.5 truncate_bytes

truncate_bytesメソッドは、最大でbytesizeバイトに切り詰められたレシーバーのコピーを返します。

"👍👍👍👍".truncate_bytes(15)
# => "👍👍👍…"

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

"👍👍👍👍".truncate_bytes(15, omission: "🖖")
# => "👍👍🖖"

5.6 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!..."

5.7 inquiry

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

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

5.8 starts_with?ends_with?

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

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

5.9 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.10 indent

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.11 アクセス

5.11.1 at(position)

atメソッドは、対象となる文字列のうち、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.11.2 from(position)

fromメソッドは、文字列のうち、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.11.3 to(position)

toメソッドは、文字列のうち、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.11.4 first(limit = 1)

firstメソッドは、文字列冒頭からlimit文字分の部分文字列を返します。

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

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

5.11.5 last(limit = 1)

lastメソッドは、文字列末尾からlimit文字分の部分文字列を返します。

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

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

5.12 活用形

5.12.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(model_name)
  table_name = model_name.to_s.demodulize.underscore
  pluralize_table_names ? table_name.pluralize : table_name
end
5.12.2 singularize

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
5.12.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など)で使う名前を求めるのに便利です。

camelizeメソッドの動作は、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"

camelcasecamelizeのエイリアスです。

5.12.4 underscore

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

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

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

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

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

"visualEffect".underscore # => "visual_effect"

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

Railsでは、コントローラのクラス名を小文字化するのにunderscoreを使っています。

# actionpack/lib/abstract_controller/base.rb
def controller_path
  @controller_path ||= name.delete_suffix("Controller").underscore
end

たとえば、上の値はparams[:controller]で取得できます。

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

5.12.5 titleize

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

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

titlecaseメソッドはtitleizeのエイリアスです。

5.12.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
5.12.7 demodulize

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

"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
5.12.8 deconstantize

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

"Product".deconstantize                        # => ""
"Backoffice::UsersController".deconstantize    # => "Backoffice"
"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
5.12.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"
5.12.10 tableize

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

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

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

5.12.11 classify

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

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

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

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

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

5.12.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
5.12.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
5.12.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"

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

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

upcase_firstメソッドはレシーバの冒頭の文字を大文字にします。

"employee salary".upcase_first # => "Employee salary"
"".upcase_first                # => ""

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

5.12.16 downcase_first

downcase_firstメソッドはレシーバの冒頭の文字を小文字にします。

"If I had read Alice in Wonderland".downcase_first # => "if I had read Alice in Wonderland"
"".downcase_first                                  # => ""

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

5.13 各種変換

5.13.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を返します。

6 Numericの拡張

6.1 バイト

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

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

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

これらのメソッドには単数形のエイリアスもあります。

1.megabyte # => 1048576

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

6.2 Time

以下のメソッドがあります。

たとえば45.minutes + 2.hours + 4.weeksのように時間の計算や宣言を行なえます。これらの戻り値は、Timeオブジェクトに加算することも、Timeオブジェクトから減算することもできます。

これらのメソッドをfrom_nowagoなどと組み合わせることで、以下のように精密に日付を計算できます。

# 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_fs(:phone)
# => 555-1234
1235551234.to_fs(:phone)
# => 123-555-1234
1235551234.to_fs(:phone, area_code: true)
# => (123) 555-1234
1235551234.to_fs(:phone, delimiter: " ")
# => 123 555 1234
1235551234.to_fs(:phone, area_code: true, extension: 555)
# => (123) 555-1234 x 555
1235551234.to_fs(:phone, country_code: 1)
# => +1-123-555-1234

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

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

以下のように、数値をパーセント形式の文字列に変換できます。

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

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

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

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

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

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

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

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

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

7 Integerの拡張

7.1 multiple_of?

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

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

7.2 ordinal

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

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

7.3 ordinalize

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

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

7.4 Time

以下のメソッドがあります。

4.months + 5.yearsのような形式での時間の計算や宣言を行えるようにします。これらの戻り値をTimeオブジェクトに足したりTimeオブジェクトから引いたりすることも可能です。

これらのメソッドをfrom_nowagoなどと組み合わせることで、以下のように精密に日付を計算できます。

# 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("e")  # => "0.5E1"

9 Enumerableの拡張

9.1 index_by

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

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

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

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

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

9.2 index_with

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

post = Post.new(title: "hey there", body: "what's up?")

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

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

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

9.3 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.4 exclude?

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

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

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

9.5 including

includingメソッドは、渡された要素を含む新しいenumerableを返します。

[ 1, 2, 3 ].including(4, 5)                    # => [ 1, 2, 3, 4, 5 ]
["David", "Rafael"].including %w[ Aaron Todd ] # => ["David", "Rafael", "Aaron", "Todd"]

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

9.6 excluding

excludingメソッドは、渡された要素を除いた新しいenumerableのコピーを返します。

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

withoutexcludingのエイリアスです。

定義は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にあります。

9.8 pick

pickメソッドは、最初の要素から指定のキーで値を取り出します。

[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pick(:name) # => "David"
[{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pick(:id, :name) # => [1, "David"]

定義は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)           # => []

includingメソッドは、渡された要素を含む新しい配列を返します。

[ 1, 2, 3 ].including(4, 5)          # => [ 1, 2, 3, 4, 5 ]
[ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]

excludingメソッドは、渡された要素を除外した新しい配列のコピーを返します。 これは、パフォーマンス上の理由でArray#rejectの代わりにArray#-を用いたEnumerable#excludingの最適化です。

["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
[ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ])                  # => [ [ 0, 1 ] ]

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からの除去を簡単かつ明示的に行えます。

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
10.4.2 to_fs

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

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

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

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

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をすべての要素に送信し、結果をrootノードの下に集めます。すべての要素がto_xmlに応答する必要があります。そうでない場合は例外が発生します。

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

最初の要素と同じ型に属さない要素が1つでもある場合、rootノードには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>

レシーバがハッシュの配列である場合、root要素はデフォルトで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>

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

子ノードの名前は、デフォルトでは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>

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

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は空きを埋める値としては利用できません。

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は空きを埋める値としては利用できません。

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"]]

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

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>

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

  • valueがハッシュの場合、key:rootとして再帰的な呼び出しを行います。

  • valueが配列の場合、key:rootとして、keyを単数形化(singularize)したものを:childrenとして再帰的な呼び出しを行います。

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

  • valueto_xmlメソッドに応答する場合、key:rootとしてメソッドを呼び出します。

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

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

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

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

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

11.2.2 reverse_update

reverse_updateメソッドは、上で説明したreverse_merge!のエイリアスです。

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

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!メソッドはディープマージを破壊的に実行します。

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

11.4 ハッシュキーの操作

11.4.1 except!

except!メソッドは、組み込みのexceptと同一ですが、キーを取り除いてselfを返す点が異なります。

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

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

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

定義は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_checkbox_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}

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

def rich_textarea_tag(name, value = nil, options = {})
  options = options.symbolize_keys

  options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
  # ...
end

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

レシーバーのキーを直接シンボルに変換する破壊的な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_optionsto_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 値を扱う

11.5.1 deep_transform_valuesdeep_transform_values!

deep_transform_valuesメソッドは、ブロック操作で変換されたすべての値を持つ新しいハッシュを返します。その中には、rootハッシュと、ネストしたハッシュや配列のすべての値も含まれます。

hash = { person: { name: "Rob", age: "28" } }

hash.deep_transform_values { |value| value.to_s.upcase }
# => {person: {name: "ROB", age: "28"}}

ブロック操作を用いてすべての値を破壊的に変更するdeep_transform_values!もあります。

11.6 スライス

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

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

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

11.7 抽出

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.8 ハッシュキーのシンボルと文字列を同様に扱う(indifferent access)

with_indifferent_accessメソッドは、レシーバから得たActiveSupport::HashWithIndifferentAccessを返します。

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

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はこのメソッドをルーティングコードでも1箇所だけ利用しています。ルーティングでは正規表現で複数行を扱うことを許していないので、このフラグで制限を加えています。

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_fs

Active Supportでは、オプションのフォーマット引数を理解するto_sの代替としてRange#to_fsを定義しています。執筆時点では、デフォルトでないフォーマットとしてサポートされているのは:dbのみです。

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

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

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

13.2 ===include?

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

(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

13.3 overlap?

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

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

定義はactive_support/core_ext/range/overlap.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?tomorrow?next_day?yesterday?prev_day?future?on_weekday?on_weekend?を利用でき、これらはすべてDate.currentを起点として導かれます。

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

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

at_beginning_of_weekbeginning_of_weekのエイリアス、at_end_of_weekend_of_weekのエイリアスです。

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
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

last_weekprev_weekのエイリアスです。

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

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

at_beginning_of_monthbeginning_of_monthのエイリアス、at_end_of_monthend_of_monthのエイリアスです。

14.1.2.5 quarter, beginning_of_quarter, end_of_quarter

quarterは、レシーバのカレンダー年における四半期を返します。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.quarter                # => 2

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

at_beginning_of_quarterbeginning_of_quarterのエイリアス、at_end_of_quarterend_of_quarterのエイリアスです。

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

at_beginning_of_yearbeginning_of_yearのエイリアス、at_end_of_yearend_of_yearのエイリアスです。

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)のショートハンドです。

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)のショートハンドです。

14.1.3.3 weeks_agoweeks_since

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

Date.new(2010, 5, 24).weeks_ago(1)   # => Mon, 17 May 2010
Date.new(2010, 5, 24).weeks_since(2) # => Mon, 07 Jun 2010
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

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

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
14.1.5 期間(duration)

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のエイリアスです。

14.1.6.2 beginning_of_hourend_of_hour

beginning_of_hourメソッドは、その時(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

at_beginning_of_hourbeginning_of_hourのエイリアスです。

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

at_beginning_of_minutebeginning_of_minuteのエイリアスです。

beginning_of_hourend_of_hourbeginning_of_minuteend_of_minuteは、TimeおよびDateTime向けの実装ですが、Date向けの実装ではありません。時刻情報を含まないDateインスタンスに対して時間や分の最初や最後を問い合わせる意味はありません。

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

15 DateTimeの拡張

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

15.1 計算

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

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

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

以下のメソッドはactive_support/core_ext/date_time/calculations.rbにのみ実装されています。これらはDateTimeインスタンスに対して使わないと意味がないためです。

15.1.1 名前付き日付時刻
15.1.1.1 DateTime.current

Active Supportでは、DateTime.currentTime.now.to_datetimeと同様に定義しています。ただし、DateTime.currentはユーザータイムゾーンが定義されている場合に対応する点が異なります。インスタンスではpast?およびfuture?という述語メソッドを利用でき、これらの定義はDate.currentを起点としています。

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
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

getutcutcのエイリアスです。

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
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)を考慮しません。算出された時間が最終的に存在しない時間になっても警告やエラーは発生しません。

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
15.1.4 期間(duration)

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?tomorrow?next_day?yesterday?prev_day?future?を調べるインスタンス述語メソッドも定義されており、これらはすべてこのTime.currentを起点にしています。

ユーザー定義のタイムゾーンを考慮するメソッドを用いて時刻を比較したい場合、Time.nowではなく必ずTime.currentを使ってください。ユーザー定義のタイムゾーンは、システムのタイムゾーンより未来になる可能性があります(Time.nowはデフォルトでシステムのタイムゾーンを使います)。つまり、Time.now.to_dateDate.yesterdayと等しくなる可能性があるということです。

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_week(その週の期間)、all_month(その月の期間)、all_quarter(その四半期の期間)、all_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
16.1.3 prev_daynext_day

prev_dayメソッドは指定の日の「前日」の日時を返し、next_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
16.1.4 prev_monthnext_month

prev_monthメソッドは指定の日の「前月」の同じ日の日時を返し、next_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
16.1.5 prev_yearnext_year

prev_yearメソッドは指定の日の「前年」の同月同日の日時を返し、next_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
16.1.6 prev_quarternext_quarter

prev_quarterメソッドは指定の日付の「前の四半期」の同じ日の日時を返し、next_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

last_quarterprev_quarterのエイリアスです。

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)

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(Access Control List)が設定されている場合は、ACLが再計算/変更されます。

atomic_writeは追記を行えません。

この補助ファイルは標準の一時ファイル用ディレクトリに書き込まれますが、第2引数でディレクトリを直接指定することもできます。

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

18 NameErrorの拡張

Active SupportはNameErrormissing_name?メソッドを追加します。このメソッドは、引数として渡された名前が原因で例外が発生するかどうかをテストします。

渡される名前はシンボルまたは文字列です。シンボルを渡した場合は単なる定数名をテストし、文字列を渡した場合はフルパス(完全修飾)の定数名をテストします。

シンボルは、:"ActiveRecord::Base"で行なっているのと同じようにフルパスの定数として表せます。シンボルがそのように動作するのは利便性のためであり、技術的に必要だからではありません。

たとえば、ArticlesControllerのアクションが呼び出されると、Railsはその名前からすぐに推測できるArticlesHelperを使おうとします。ここではこのヘルパーモジュールが存在していなくても問題はないので、この定数名で例外が発生しても例外として扱わずに黙殺する必要があります。しかし、実際に不明な定数が原因で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にあります。

19 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にあります。

20 Pathnameの拡張

20.1 existence

existenceメソッドは、名前付きファイルが存在する場合はレシーバーを返し、存在しない場合はnilを返します。これは、以下のような定番のファイル読み出しで便利です。

content = Pathname.new("file").existence&.read

21 参考資料(日本語)

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加