Rails の active_support

2026-08-31 に見直した。constantize の例が Fixnum を使っていたが、 Fixnum は Ruby から削除されている(手元の Ruby 3.4.8 で確かめたところ NameError: uninitialized constant Fixnum)。Integer に直した。 整数は Fixnum / Bignum の区別がなくなり、Integer に統合されている。

読み込み

  • デフォルトでは最小限の依存関係のみを読み込む
  • require時に全てメモリに読み込まれるわけではない
  • 一部はautoloadとして設定されており、実際に使うときだけ読み込まれる

requireされる拡張機能だけを読み込む

  • 本当に必要な依存ファイルだけが同時に読み込まれる
require 'active_support'
require "active_support/core_ext/hash/indifferent_access"

特定のグループのみを読み込む

require 'active_support'
require 'active_support/core_ext/hash'

全ての拡張機能を読み込む

require 'active_support'
require 'active_support/core_ext'

存在確認

blank?

trueを返すもの

  • nil
  • false
  • ホワイトスペース(U+2029(段落区切り文字)はホワイトスペース)だけで構成された文字列
  • 空配列
  • 空ハッシュ
  • その他、empty?メソッドに応答してtrueを返すオブジェクト

数字の場合

  • 0および0.0は空白ではないのでfalse

present?

  • !blank?

exists?

User.exists?(email: params[:email])
 
# メソッドが受け取る引数の数が固定されておらず、メソッド宣言で*が使われていると、そのような波かっこなしのオプションハッシュは引数の配列の末尾要素になってしまい、ハッシュとして認識されなくなる場合
# extract_options!メソッドを使うと、配列の末尾項目の型をチェックできます。それがハッシュの場合、そのハッシュを取り出して返し、それ以外の場合は空のハッシュを返す
def caches_action(*actions)
  return unless cache_configured?
  options = actions.extract_options!
  # ...
end
 
 

many?

  • collection.size > 1の短縮形
  • many?は、ブロックがオプションとして与えられると、trueを返す要素だけを扱います。
<% if pages.many? %>
  <%= pagination_links %>
<% end %>
@see_more = videos.many? { |video| video.category == params[:category] }

exclude?

  • include?の逆
to_visit << node if visited.exclude?(node)

delegate

class User < ApplicationRecord
  has_one :profile
 
  def name
    profile.name
  end
end
class User < ApplicationRecord
  has_one :profile
  delegate :name, :age, :address, :twitter, to: :profile
end

その他

# Rails定数を委譲する
delegate :logger, to: :Rails
 
# レシーバのクラスに委譲する
delegate :table_name, to: :class
 
# NoMethodError時にはnilを返す
delegate :name, to: :profile, allow_nil: true
 
# profile_name, profile_age, profile_address, profile_twitter
delegate :name, :age, :address, :twitter, to: :profile, prefix: true
 
# avatar_size
delegate :size, to: :attachment, prefix: :avatar
 
# private
delegate :date_of_birth, to: :profile, private: true

文字列

エスケープ

  • 基本的にこれらのメソッドは、通常のビューでは使わない
  • 現在のRailsのビューでは、安全でない値は自動的にエスケープされる
s = "".html_safe
s.html_safe? # => true

エスケープされていない文字列をそのままにしたい

  • html_safeメソッドは不要
<%= raw @cms.current_template %>
def raw(stringish)
  stringish.to_s.html_safe
end

注意

  • どんなメソッドでも潜在的には文字列を安全でないものに変換してしまう可能性があることに常に注意を払う
  • downcase、gsub、strip、chomp、underscoreなどの変換メソッドがこれに該当
  • gsub!のような破壊的な変換を行なうメソッドを使うと、レシーバ自体が安全でなくなってしまう
  • こうしたメソッドを実行すると、実際に変換が行われたかどうかにかかわらず、安全を表すビットは常にオフになる
  • 安全な文字列に対してto_sを実行した場合は、安全な文字列を返すが、。to_strによる強制変換を実行した場合には安全でない文字列を返す

remove

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

squish

  • 空白文字を除去
  • このメソッドでは、ASCIIとUnicodeのホワイトスペースを扱える
" \n  foo   bar \t ".squish # => "foo bar"

truncate

  • 文字列を指定した長さに切り詰める
"Once upon a time in a world far far away".truncate(27) # => "Once upon a time in a wo..."

strip_heredoc, indent

  • ヒアドキュメントのインデントを除去する
  • レシーバの行にインデントを追加する
if options[:usage]
  puts <<-USAGE.strip_heredoc
    This command does such and such.
 
    Supported options are:
      -h         This message
      ...
  USAGE
end
 
<<EOS.indent(2)
def some_method
  some_code
end
EOS
# =>
  def some_method
    some_code
  end

at, from, to, first, last

  • 部分文字列を返す
# at
"hello".at(4)  # => "o"
"hello".at(10) # => nil
 
# from
"hello".from(-2) # => "lo"
"hello".from(10) # => nil
 
# to
"hello".to(2)  # => "hel"
"hello".to(10) # => "hello"
 
# first
str = "hello"
str.first    # => "h"
str.first(1) # => "h"
str.first(2) # => "he"
str.first(0) # => ""
str.first(6) # => "hello"
 
# last
str.last    # => "o"
str.last(1) # => "o"
str.last(2) # => "lo"
str.last(0) # => ""
str.last(6) # => "hello"

pluralize, singularize, camelize, underscore, titleize, dasherize, demodulize, deconstantize, parameterize, tableize, classify, constantize, humanize, foreign_key, upcase_first, downcase_first

# pluralize
"table".pluralize     # => "tables"
"ruby".pluralize      # => "rubies"
"equipment".pluralize # => "equipment"
 
# singularize
"tables".singularize    # => "table"
"rubies".singularize    # => "ruby"
"equipment".singularize # => "equipment"
 
# camelize
"product".camelize    # => "Product"
"admin_user".camelize # => "AdminUser"
"backoffice/session".camelize # => "Backoffice::Session"
"visual_effect".camelize(:lower) # => "visualEffect"
# "SSLError".underscore.camelizeを実行した結果は"SslError"になり、元に戻らないとき
ActiveSupport::Inflector.inflections do |inflect|
  inflect.acronym "SSL"
end
"SSLError".underscore.camelize # => "SSLError"
 
# underscore
"Product".underscore   # => "product"
"AdminUser".underscore # => "admin_user"
"Backoffice::Session".underscore # => "backoffice/session"
"visualEffect".underscore # => "visual_effect"
 
# titleize
"alice in wonderland".titleize # => "Alice In Wonderland"
"fermat's enigma".titleize     # => "Fermat's Enigma"
 
# dasherize
"name".dasherize         # => "name"
"contact_data".dasherize # => "contact-data"
 
# demodulize
"Product".demodulize                        # => "Product"
"Backoffice::UsersController".demodulize     # => "UsersController"
"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
"::Inflections".demodulize                   # => "Inflections"
"".demodulize                               # => ""
 
# deconstantize
"Product".deconstantize                        # => ""
"Backoffice::UsersController".deconstantize    # => "Backoffice"
"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
 
# parameterize
"John Smith".parameterize # => "john-smith"
"Kurt Gödel".parameterize # => "kurt-godel"
"John Smith".parameterize(preserve_case: true) # => "John-Smith"
"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"
"John Smith".parameterize(separator: "_") # => "john_smith"
"Kurt Gödel".parameterize(separator: "_") # => "kurt_godel"
 
# tableize
"Person".tableize      # => "people"
"Invoice".tableize     # => "invoices"
"InvoiceLine".tableize # => "invoice_lines"
 
# classify
"people".classify        # => "Person"
"invoices".classify      # => "Invoice"
"invoice_lines".classify # => "InvoiceLine"
"highrise_production.companies".classify # => "Company"
 
# constantize
"Integer".constantize # => Integer
 
module M
  X = 1
end
"M::X".constantize # => 1
 
X = :in_Object
module M
  X = :in_M
 
  X                 # => :in_M
  "::X".constantize # => :in_Object
  "X".constantize   # => :in_Object (!)
end
 
# humanize
"name".humanize                         # => "Name"
"author_id".humanize                    # => "Author"
"author_id".humanize(capitalize: false) # => "author"
"comments_count".humanize               # => "Comments count"
"_id".humanize                          # => "Id"
"ssl_error".humanize # => "SSL error"
 
# foreign_key
"User".foreign_key           # => "user_id"
"InvoiceLine".foreign_key    # => "invoice_line_id"
"Admin::Session".foreign_key # => "session_id"
 
# upcase_first
"employee salary".upcase_first # => "Employee salary"
"".upcase_first                # => ""
 
# downcase_first
"If I had read Alice in Wonderland".downcase_first # => "if I had read Alice in Wonderland"
"".downcase_first                                  # => ""

to_xml,

  • コレクションが空の場合、root要素はデフォルトで「nilクラス」になるので、:rootオプションを使って、root要素を統一することもできる
  • 子ノードの名前は、デフォルトではrootノードを単数形にしたものが使われます。上の例で言うと「contributor」や「object」です。:childrenオプションを使うと、これらをノード名として設定できる
# ハッシュでないことが前提
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>
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>
 
# 最初の要素と同じ型に属さない要素が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>

日付・時間

to_date、to_time、to_datetime

  • 3つのメソッドはいずれも、レシーバが空の場合はnilを返す
"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
 
# デフォルトは: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

seconds, minutes, hours, days, weeks, months, years, fortnights

  • from_nowやagoなどと組み合わせる
# Time.current.advance(months: 1)と等価
1.month.from_now
# Time.current.advance(weeks: 2)と等価
2.weeks.from_now
# Time.current.advance(years: 2)と同等
2.years.from_now
 
# Time.current.advance(months: 4, weeks: 5)と等価
(4.months + 5.weeks).from_now
# Time.current.advance(months: 4, years: 5)と同等
(4.months + 5.years).from_now

数字

bytes

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

to_fs, to_s

# phone
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
 
# currency
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
 
# percentage
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%
 
# delimited
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
 
# rounded
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
 
# human_size
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
 
# human
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"
 
# 配列の中にidに応答する項目がある場合
[].to_fs(:db)            # => "null"
[user].to_fs(:db)        # => "8456"
invoice.lines.to_fs(:db) # => "23,567,556,12"
 
# to_s
BigDecimal(5.00, 6).to_s       # => "5.0"
BigDecimal(5.00, 6).to_s("e")  # => "0.5E1"

multiple_of?, ordinal, ordinalize

# multiple_of?
2.multiple_of?(1) # => true
1.multiple_of?(2) # => false
 
# ordinal
1.ordinal    # => "st"
2.ordinal    # => "nd"
53.ordinal   # => "rd"
2009.ordinal # => "th"
-21.ordinal  # => "st"
-134.ordinal # => "th"
 
# ordinalize
1.ordinalize    # => "1st"
2.ordinalize    # => "2nd"
53.ordinalize   # => "53rd"
2009.ordinalize # => "2009th"
-21.ordinalize  # => "-21st"
-134.ordinalize # => "-134th"

Enumerableの拡張

index_by, index_with

# index_by
invoices.index_by(&:number)
# => {"2009-032" => <Invoice ...>, "2009-008" => <Invoice ...>, ...}
 
# index_with
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 ], … }

including, excluding, to, from, third, fifth

  • 渡された要素を含む新しいenumerableを返す
  • 渡された要素を除いた新しいenumerableのコピーを返す
  • withoutはexcludingのエイリアス
# including
[ 1, 2, 3 ].including(4)                      # => [ 1, 2, 3, 4 ]
[ 1, 2, 3 ].including(4, 5)                   # => [ 1, 2, 3, 4, 5 ]
["David", "Rafael"].including %w[ Aaron Todd ] # => ["David", "Rafael", "Aaron", "Todd"]
[ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]
 
# excluding
[ 1, 2, 3 ].excluding(2)                      # => [ 1, 3 ]
[ 1, 2, 3 ].excluding(2, 3)                   # => [ 1 ]
["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ] # => ["David", "Rafael"]
["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
[ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ])                  # => [ [ 0, 1 ] ]
# 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)           # => []
 
# second、third、fourth、fifthは、second_to_lastやthird_to_lastと同様に、対応する位置の要素を返す
%w(a b c d).third # => "c"
%w(a b c d).fifth # => nil

pluck, pick

  • 最初の要素から指定のキーで値を取り出す
# pluck
[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"]
 
# pick
[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pick(:name) # => "David"
[{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pick(:id, :name) # => [1, "David"]

extract!

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]

wrap

Array.wrap(nil)       # => []
Array.wrap([1, 2, 3]) # => [1, 2, 3]
Array.wrap(0)         # => [0]
 
Array.wrap(foo: :bar) # => [{:foo=>:bar}]
Array(foo: :bar)      # => [[:foo, :bar]]