Расширения ядра Active Support

Active Support - это компонент Ruby on Rails, отвечающий за предоставление расширений и утилит для языка Ruby.

Он предлагает более ценные функции на уровне языка, нацеленные как на разработку приложений на Rails, так и на разработку самого Ruby on Rails.

После прочтения этого руководства, вы узнаете:

  • Что такое расширения ядра.
  • Как загрузить все расширения.
  • Как подобрать только те расширения, которые вам нужны.
  • Какие расширения предоставляет Active Support.

1. Как загрузить расширения ядра

1.1. Автономный Active Support

Для обеспечения минимального влияния, Active Support по умолчанию загружает минимальные зависимости. Он разбит на маленькие части, поэтому можно загружать лишь желаемые зависимости. Он также имеет некоторые точки входа, которые по соглашению загружают все относящиеся расширения за раз, или даже все.

Таким образом, после обычного require:

require "active_support"

будут загружены только расширения, требуемые для фреймворка Active Support.

1.1.1. Подбор определений

Этот пример показывает, как загрузить Hash#with_indifferent_access. Это расширение включает преобразование Hash в ActiveSupport::HashWithIndifferentAccess, который позволяет доступ с как строковыми, так и символьными ключами.

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

Для каждого отдельного метода, определенного как расширение ядра, в этом руководстве имеется заметка, сообщающая, где такой метод определяется. В случае с with_indifferent_access заметка гласит:

Определено в active_support/core_ext/hash/indifferent_access.rb.

Это означает, что это можно затребовать следующим образом:

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

Active Support был тщательно пересмотрен и теперь подхватывает только те файлы для загрузки, которые содержат строго необходимые зависимости, если такие имеются.

1.1.2. Загрузка сгруппированных расширений ядра

Следующий уровень - это просто загрузка всех расширений к Hash. Как правило, расширения к SomeClass доступны за раз при загрузке active_support/core_ext/some_class.

Таким образом, чтобы загрузить все расширения Hash (в том числе with_indifferent_access):

require "active_support"
require "active_support/core_ext/hash"
1.1.3. Загрузка всех расширений ядра

Возможно, вы предпочтете загрузить все расширения ядра, вот файл для этого необходимо:

require "active_support"
require "active_support/core_ext"
1.1.4. Загрузка всего Active Support

И наконец, если необходимо получить доступ ко всему Active Support, просто выполните:

require "active_support/all"

В действительности это даже не поместит весь Active Support в память, так как некоторые вещи настроены через autoload, поэтому они загружаются только когда используются.

1.2. Active Support в приложении на Ruby on Rails

Приложение на Ruby on Rails загружает весь Active Support, кроме случая когда config.active_support.bare равен true. В этом случае приложение загрузит только сам фреймворк и подберет файлы для собственных нужд, и позволит подобрать вам файлы самостоятельно на любом уровне, как описано в предыдущем разделе.

2. Расширения ко всем объектам

2.1. blank? и present?

Следующие значения рассматриваются как пустые в Rails приложении:

  • nil и false,
  • строки, состоящие только из пробелов (смотрите примечание ниже),
  • пустые массивы и хэши,
  • и любые другие объекты, откликающиеся на 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?, и nil в противном случае. Он полезен для подобных идиом:

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

Определено в active_support/core_ext/object/blank.rb.

2.3. duplicable?

В Ruby 2.5 большинство объектов могут дублироваться с помощью dup или clone:

"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. Используйте его только в том случае, если знаете, что жесткий перечень достаточен в конкретном случае.

Определено в active_support/core_ext/object/duplicable.rb.

2.4. deep_dup

Метод deep_dup возвращает "глубокую" копию данного объекта. Обычно при вызове dup на объекте, содержащем другие объекты, Ruby не вызывает dup для них, поэтому он создает "мелкую" копию объекта. Если, к примеру, имеется массив со строкой, это будет выглядеть так:

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

duplicate.push "another-string"

# объект был дублирован, поэтому элемент был добавлен только в дубликат
array     # => ["string"]
duplicate # => ["string", "another-string"]

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

# первый элемент не был дублирован, он будет изменен в обоих массивах
array     # => ["foo"]
duplicate # => ["foo, "another-string"]

Как видите, после дублирования экземпляра Array, мы получили еще один объект, следовательно мы можем его модифицировать, и исходный объект останется нетронутым. Однако, это не истинно для элементов массива. Поскольку dup не делает "глубокую" копию, строка внутри массива остается тем же самым объектом.

Если нужна "глубокая" копия объекта, следует использовать deep_dup. Вот пример:

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

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

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

Если объект нельзя дублировать, deep_dup просто возвратит его:

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

Определено в active_support/core_ext/object/deep_dup.rb.

2.5. try

Когда необходимо вызвать метод на объекте, но только в том случае, если он не nil, то простейшим способом достичь этого является условное выражение, добавляющее ненужный код. Альтернативой является использование try. try похож на Object#public_send за исключением того, что он возвращает nil, если вызван на nil.

Вот пример:

# без try
unless @number.nil?
  @number.next
end

# используя try
@number.try(:next)

Другим примером является этот код из ActiveRecord::ConnectionAdapters::AbstractAdapter, где @logger может быть nil. Код использует 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 поглотит ошибки об отсутствующем методе, возвратив вместо них 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:

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

Определено в active_support/core_ext/kernel/singleton_class.rb.

2.7. acts_like?(duck)

Метод acts_like? предоставляет возможность проверить, работает ли некий класс как некоторый другой класс, основываясь на простом соглашении: класс предоставляющий тот же интерфейс, как у String определяет

def acts_like_string?
end

являющийся всего лишь маркером, его содержимое или возвращаемое значение ничего не значит. Затем, код клиента может запросить "безопасную утиную типизацию" следующим образом:

some_klass.acts_like?(:string)

В Rails имеются классы, действующие как Date или Time и следующие этому соглашению.

Определено в active_support/core_ext/object/acts_like.rb.

2.8. to_param

Все объекты в Rails отвечают на метод to_param, который предназначен для возврата чего-то, что представляет их в строке запроса или как фрагменты URL.

По умолчанию to_param просто вызывает to_s:

7.to_param # => "7"

Возвращаемое значение to_param не должно быть экранировано:

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

Некоторые классы в Rails переопределяют этот метод.

Например, nil, true и false возвращают сами себя. Array#to_param вызывает to_param на элементах и соединяет результат с помощью "/":

[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, поскольку в подобном запросе "357-john-smith" будет значением params[:id].

Определено в active_support/core_ext/object/to_param.rb.

2.9. to_query

Метод to_query создает строку запроса, который связывает заданный key с возвращаемым значением to_param. Например, для следующего определения 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 к каждому элементу с key[] в качестве ключа, и соединяет результат с помощью "&":

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

Хэши также отвечают на to_query, но c другой сигнатурой. Если аргумент не передается, вызов генерирует отсортированную серию присваиваний ключ/значение, вызвав to_query(key) на этих значениях. Затем он соединяет результат с помощью "&":

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

Метод Hash#to_query принимает опциональное пространство имен для ключей:

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

Определено в active_support/core_ext/object/to_query.rb.

2.10. with_options

Метод with_options предоставляет способ для выделения общих опций в серии вызовов метода.

Задав дефолтный хэш опций, with_options предоставляет прокси-объект в блок. Внутри блока методы, вызванные на прокси, отправляются получателю с объединением своих опций. Например, чтобы избавиться от дублирования:

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

заменяем на:

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

Эта идиома может передавать группировку в ридер (reader). Например скажем, что нужно послать newsletter, язык которого зависит от пользователя. Где-нибудь в рассыльщике можно сгруппировать кусочки, зависимые от локали, следующим образом:

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

Поскольку with_options переадресовывает вызовы получателю, они могут быть вложены. Каждый уровень вложенности будет объединять унаследованные дефолтные значения со своими собственными.

Определено в active_support/core_ext/object/with_options.rb.

2.11. Поддержка JSON

Active Support обеспечивает лучшую реализацию to_json, чем гем json, обычно предоставленный для объектов Ruby. Это так, потому что некоторые классы, такие как Hash и Process::Status, нуждаются в специальной обработке для обеспечения подходящего JSON.

Определено в active_support/core_ext/object/json.rb.

2.12. Переменные экземпляра

Active Support предоставляет несколько методов для облегчения доступа к переменным экземпляра.

2.12.1. instance_values

Метод instance_values возвращает хэш, который связывает имена переменных экземпляра без "@" с их соответствующими значениями. Ключи являются строками:

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

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

Определено в active_support/core_ext/object/instance_variables.rb.

2.12.2. instance_variable_names

Метод instance_variable_names возвращает массив. Каждое имя включает знак "@".

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

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

Определено в active_support/core_ext/object/instance_variables.rb.

2.13. Отключение предупреждений и исключения

Методы silence_warnings и enable_warnings изменяют значение $VERBOSE в течение исполнения блока, и сбрасывают в исходное значение после его окончания:

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

Отключение исключений также возможно с помощью suppress. Этот метод получает определенное количество классов исключений. Если вызывается исключение во время выполнения блока, и kind_of? соответствует любому аргументу, suppress ловит его и возвращает отключенным. В противном случае исключение не захватывается:

# Если пользователь под блокировкой, инкремент теряется, ничего страшного.
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

Определено в active_support/core_ext/kernel/reporting.rb.

2.14. in?

Предикат in? проверяет, включен ли объект в другой объект. Если переданный элемент не отвечает на include?, будет вызвано исключение ArgumentError.

Примеры применения in?:

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

Определено в active_support/core_ext/object/inclusion.rb.

3. Расширения для Module

3.1. Атрибуты

3.1.1. alias_attribute

В атрибутах модели есть ридер (reader), райтер (writer) и предикат. Можно создать псевдоним к атрибуту модели, в котором будут определены сразу три соответствующих метода, используя alias_attribute. Как и в других создающих псевдоним методах, новое имя - это первый аргумент, а старое имя - второй (мнемоническое правило такое: они идут в том же порядке, как если бы делалось присваивание):

class User < ApplicationRecord
  # Теперь можно обращаться к столбцу email как "login".
  # Это имеет больше смысла для кода аутентификации.
  alias_attribute :login, :email
end

Определено в active_support/core_ext/module/aliasing.rb.

3.1.2. Внутренние атрибуты

При определении атрибута в классе, который предназначен для подкласса, есть риск коллизии подклассовых имен. Это особенно важно для библиотек.

Active Support определяет макросы attr_internal_reader, attr_internal_writer и attr_internal_accessor. Они ведут себя подобно встроенным в Ruby коллегам attr_*, за исключением того, что они именуют лежащую в основе переменную экземпляра способом, наиболее снижающим коллизии.

Макрос attr_internal - это синоним для attr_internal_accessor:

# библиотека
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# код клиента
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

В предыдущем примере мог быть случай, при котором :log_level не принадлежит публичному интерфейсу библиотеки и используется только для разработки. Код клиента, не подозревающий о потенциальном конфликте, создает подкласс и определяет внутри него свой :log_level. Благодаря attr_internal здесь не будет коллизий.

По умолчанию внутренняя переменная экземпляра именуется с предшествующим подчеркиванием, @_log_level в примере выше. Это настраивается через Module.attr_internal_naming_format, куда можно передать любую строку в формате sprintf с предшествующими @ и %s в любом месте, которая означает место, куда вставляется имя. По умолчанию "@_%s".

Rails использует внутренние атрибуты в некоторых местах, например для вью:

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

Определено в active_support/core_ext/module/attr_internal.rb.

3.1.3. Атрибуты модуля

Макросы mattr_reader, mattr_writer и mattr_accessor - это те же самые макросы cattr_*, определенным для класса. Фактически, макросы cattr_* — это всего лишь псевдонимы для макросов mattr_*. Смотрите Атрибуты класса.

Например, API логирования Active Storage генерируется с помощью mattr_accessor:

module ActiveStorage
  mattr_accessor :logger
end

Определено в active_support/core_ext/module/attribute_accessors.rb.

3.2. Родители

3.2.1. module_parent

Метод module_parent на вложенном именованном модуле возвращает модуль, содержащий его соответствующую константу:

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

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

Если модуль анонимный или относится к верхнему уровню, module_parent возвращает Object.

Отметьте, что в этом случае module_parent_name возвращает nil.

Определено в active_support/core_ext/module/introspection.rb.

3.2.2. module_parent_name

Метод module_parent_name на вложенном именованном модуле возвращает полностью определенное имя модуля, содержащего его соответствующую константу:

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

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

Для верхнеуровневых и анонимных модулей module_parent_name возвращает nil.

Отметьте, что в этом случае module_parent возвращает Object.

Определено в active_support/core_ext/module/introspection.rb.

3.2.3. parents

Метод module_parents вызывает module_parent на получателе и вверх по иерархии, пока не будет достигнут Object. Цепочка возвращается в массиве, от низшего к высшему:

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

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

Определено в active_support/core_ext/module/introspection.rb.

3.3. Anonymous

У модуля может быть или не быть имени:

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

Отметьте, что быть недостижимым не означает быть анонимным:

module M
end

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

m.anonymous? # => false

хотя анонимный модуль недостижим по определению.

Определено в active_support/core_ext/module/anonymous.rb.

3.4. Делегирование метода

3.4.1. delegate

Макрос delegate предлагает простой способ передать методы.

Давайте представим, что у пользователей в неком приложении имеется информация о логинах в модели User, но имена и другие данные в отдельной модели Profile:

class User < ApplicationRecord
  has_one :profile
end

С такой конфигурацией можно получить имя пользователя через его профиль, user.profile.name, но было бы удобнее обеспечить доступ к такому атрибуту напрямую:

class User < ApplicationRecord
  has_one :profile

  def name
    profile.name
  end
end

Это как раз то, что делает delegate:

class User < ApplicationRecord
  has_one :profile

  delegate :name, to: :profile
end

Это короче, и намерения более очевидные.

Целевой метод должен быть публичным.

Макрос delegate принимает несколько методов:

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

При интерполяции в строку опция :to должна стать выражением, вычисляемым объектом, метод которого делегируется. Обычно строка или символ. Такое выражение вычисляется в контексте получателя:

# делегирует константе Rails
delegate :logger, to: :Rails

# делегирует классу получателя
delegate :table_name, to: :class

Если опция :prefix установлена в true - это менее характерно, смотрите ниже.

По умолчанию, если делегирование вызывает NoMethodError и цель является nil, выводится исключение. Можно попросить с помощью опции :allow_nil, чтобы вместо этого возвращался nil:

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

С :allow_nil вызов user.name возвратит nil, если у пользователя нет профиля.

Опция :prefix добавляет префикс к имени генерируемого метода. Это может быть удобно, например, для получения более благозвучного имени:

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

Предыдущий пример сгенерирует address_street, а не street.

Поскольку в этом случае имя генерируемого метода составляется из имен целевого объекта и целевого метода, опция :to должна быть именем метода.

Также может быть настроен произвольный префикс:

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

В предыдущем примере макрос генерирует avatar_size, а не size.

Опция :private изменяет область видимости методов:

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

Делегированные методы являются публичными по умолчанию. Передайте private: true, чтобы изменить это.

Определено в active_support/core_ext/module/delegation.rb

3.4.2. delegate_missing_to

Представьте, что нужно делегировать все, отсутствующее в объекте User в Profile. Макрос delegate_missing_to позволяет реализовать это быстро:

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

Целью может быть все что угодно, вызываемое внутри объекта, например, переменные экземпляра, методы, константы и т.д. Делегируются только публичные методы цели.

Определено в active_support/core_ext/module/delegation.rb.

3.5. Переопределение методов

Бывают ситуации, когда нужно определить метод с помощью define_method, но вы не знаете, существует ли уже метод с таким именем. Если так, то выдается предупреждение, если оно включено. Такое поведение хоть и не ошибочно, но не элегантно.

Метод redefine_method предотвращает такое потенциальное предупреждение, предварительно убирая существующий метод, если нужно.

Также можно использовать silence_redefinition_of_method, если необходимо определить заменяющий метод отдельно (потому что используется delegate, например).

Определено в active_support/core_ext/module/redefine_method.rb.

4. Расширения для Class

4.1. Атрибуты класса

4.1.1. class_attribute

Метод class_attribute объявляет один или более наследуемых атрибутов класса, которые могут быть переопределены на низшем уровне иерархии:

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_writer в false.

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

В модели такая опция может быть полезной как способ предотвращения массового назначения для установки атрибута.

Генерация ридер-метода экземпляра может быть отключена установлением опции :instance_reader в false.

class A
  class_attribute :x, instance_reader: false
end

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

Для удобства class_attribute определяет также предикат экземпляра, являющийся двойным отрицанием того, что возвращает ридер экземпляра. В вышеописанном примере оно может вызываться x?.

Когда instance_reader равен false, предикат экземпляра возвратит NoMethodError, как и ридер-метод.

Если не нужен предикат, передайте instance_predicate: false, и он не будет определен.

Определено в active_support/core_ext/class/attribute.rb.

4.1.2. cattr_reader, cattr_writer и cattr_accessor

Макросы cattr_reader, cattr_writer и cattr_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 и генерация райтер-метода экземпляра предотвращается установкой :instance_writer в false. Генерация обоих методов предотвращается установкой :instance_accessor в false. Во всех случаях, должно быть не любое ложное значение, а именно false:

module A
  class B
    # first_name ридер экземпляра не генерируется.
    cattr_accessor :first_name, instance_reader: false
    # last_name= райтер экземпляра не генерируется.
    cattr_accessor :last_name, instance_writer: false
    # surname ридер экземпляра или surname= райтер экземпляра не генерируется.
    cattr_accessor :surname, instance_accessor: false
  end
end

В модели может быть полезным установить :instance_accessor в false как способ предотвращения массового назначения для установки атрибута.

Определено в active_support/core_ext/class/attribute_accessors.rb.

4.2. Подклассы и потомки

4.2.1. subclasses

Метод subclasses возвращает подклассы получателя:

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

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

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

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

Порядок, в котором эти классы возвращаются, не определен.

Определено в active_support/core_ext/class/subclasses.rb.

4.2.2. descendants

Метод descendants возвращает все классы, которые являются < к его получателю:

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

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

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

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

Порядок, в котором эти классы возвращаются, не определен.

Определено в active_support/core_ext/class/subclasses.rb.

5. Расширения для String

5.1. Безопасность вывода

5.1.1. Мотивация

Вставка данных в шаблоны HTML требует дополнительной осторожности. Например, нельзя просто интерполировать @review.title на страницу HTML. С одной стороны, если заголовок рецензии "Flanagan & Matz rules!", то результат не будет правильно отображен, поскольку амперсанд должен быть экранирован как "&amp;". К тому же, в зависимости от приложения, это может быть большой дырой в безопасности, так как пользователи могут внедрить вредоносный HTML, устанавливающий вручную изготовленный заголовок рецензии. Смотрите подробную информацию о рисках в разделе о межсайтовом скриптинге в руководстве Безопасность приложений на Rails.

5.1.2. Безопасные строки

В Active Support есть концепция (html) безопасных строк. Безопасная строка - это та, которая помечена как подлежащая вставке в HTML как есть. Ей можно доверять, независимо от того, была она экранирована или нет.

Строки рассматриваются как небезопасные по умолчанию:

"".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 # => "<"

Эти методы не должны использоваться в обычных вью. Небезопасные значения автоматически экранируются:

<%= @review.title %> <%# прекрасно, экранируется, если нужно %>

Чтобы вставить что-либо дословно, используйте хелпер raw вместо вызова html_safe:

<%= raw @cms.current_template %> <%# вставляет @cms.current_template как есть %>

или используйте эквивалентную запись <%==:

<%== @cms.current_template %> <%# вставляет @cms.current_template как есть %>

Хелпер raw вызывает за вас хелпер html_safe:

def raw(stringish)
  stringish.to_s.html_safe
end

Определено в active_support/core_ext/string/output_safety.rb.

5.1.3. Преобразование

Как правило, за исключением, разве что, конкатенации, как объяснялось выше, любой метод, который может изменить строку, дает небезопасную строку. Это downcase, gsub, strip, chomp, underscore и т.д.

В случае встроенного преобразования, такого как gsub!, получатель сам становится небезопасным.

Бит безопасности всегда теряется, независимо от того, изменило ли что-то преобразование или нет.

5.1.4. Конверсия и принуждение

Вызов to_s на безопасной строке возвратит безопасную строку, но принуждение с помощью to_str возвратит небезопасную строку.

5.1.5. Копирование

Вызов dup или clone на безопасной строке создаст безопасные строки.

5.2. remove

Метод remove уберет все совпадения с шаблоном:

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

Также имеется деструктивная версия String#remove!.

Определено в active_support/core_ext/string/filters.rb.

5.3. squish

Метод squish отсекает начальные и конечные пробелы, а также заменяет внутренние пробелы на один пробел:

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

Также имеется разрушительная версия String#squish!.

Отметьте, что он обрабатывает и ASCII, и Unicode пробелы.

Определено в active_support/core_ext/string/filters.rb.

5.4. truncate

Метод truncate возвращает копию получателя, сокращенную после заданного length:

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

Многоточие может быть настроено с помощью опции :omission:

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

Отметьте, что сокращение учитывает длину строки omission.

Передайте :separator для сокращения строки по естественным разрывам:

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

Опция :separator может быть регулярным выражением:

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

В вышеуказанных примерах "dear" обрезается сначала, а затем :separator предотвращает это.

Определено в active_support/core_ext/string/filters.rb.

5.5. truncate_bytes

Метод truncate_bytes возвращает копию получателя, обрезанную к максимуму bytesize байт:

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

Многоточие может быть настроено с помощью опции :omission:

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

Определено в active_support/core_ext/string/filters.rb.

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

Определено в active_support/core_ext/string/filters.rb.

5.7. inquiry

Метод inquiry конвертирует строку в объект StringInquirer, делая проверки равенства более красивыми.

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

Определено в active_support/core_ext/string/inquiry.rb.

5.8. starts_with? и ends_with?

Active Support определяет псевдонимы String#start_with? и String#end_with? (в связи с особенностями английской морфологии, преобразует глаголы в форму 3-го лица):

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

Определено в active_support/core_ext/string/starts_ends_with.rb.

5.9. strip_heredoc

Метод strip_heredoc обрезает отступы в heredocs.

Для примера в

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

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

пользователь увидит используемое сообщение, выровненное по левому краю.

Технически это выглядит как выделение красной строки в отдельную строку и удаление всех идущих впереди пробелов.

Определено в 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

Второй аргумент, indent_string, определяет, какой отступ строки использовать. По умолчанию nil, что сообщает методу самому догадаться на основе первой строчки с отступом, а если такой нет, то использовать пробел.

"  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 это обычно один пробел или табуляция, он может быть любой строкой.

Третий аргумент, 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) эквивалентен str.to(n-1), если n > 0, и возвращает пустую строку для n == 0.

Определено в active_support/core_ext/string/access.rb.

5.11.5. last(limit = 1)

Метод last возвращает подстроку, содержащую последние limit символов строки.

Вызов str.last(n) эквивалентен str.from(-n), если n > 0, и возвращает пустую строку для 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

Определено в active_support/core_ext/string/inflections.rb.

5.12.2. singularize

Метод singularize это противоположность pluralize:

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

Связи вычисляют имя соответствующего связанного дефолтного класса, используя этот метод:

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.3. camelize

Метод camelize возвращает получателя в стиле CamelCase:

"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

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

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

camelize имеет псевдоним camelcase.

Определено в active_support/core_ext/string/inflections.rb.

5.12.4. underscore

Метод underscore делает наоборот, от CamelCase к путям:

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.5. titleize

Метод titleize озаглавит слова в получателе:

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

titleize имеет псевдоним titlecase.

Определено в active_support/core_ext/string/inflections.rb.

5.12.6. dasherize

Метод dasherize заменяет подчеркивания в получателе дефисами:

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

Сериализатор XML моделей использует этот метод для форматирования имен узлов:

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.7. demodulize

Для заданной строки с ограниченным именем константы, demodulize возвращает само имя константы, то есть правой части этого:

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

Active Record, к примеру, использует этот метод для вычисления имени столбца кэширования счетчика:

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.8. deconstantize

У заданной строки с ограниченным выражением ссылки на константу deconstantize убирает самый правый сегмент, в основном оставляя имя контейнера константы:

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

Определено в active_support/core_ext/string/inflections.rb.

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"

Определено в active_support/core_ext/string/inflections.rb.

5.12.10. tableize

Метод tableize - это underscore вместе с pluralize.

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

Как правило, tableize возвращает имя таблицы, соответствующей заданной модели для простых случаев. На самом деле фактическая реализация в Active Record не является прямым tableize, так как он также демодулизирует имя класса и проверяет несколько опций, которые могут повлиять на возвращаемую строку.

Определено в active_support/core_ext/string/inflections.rb.

5.12.11. classify

Метод classify является противоположностью tableize. Он выдает имя класса, соответствующего имени таблицы:

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

Метод понимает ограниченные имена таблиц:

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

Отметьте, что classify возвращает имя класса как строку. Можете получить фактический объект класса, вызвав constantize на ней, как объяснено далее.

Определено в active_support/core_ext/string/inflections.rb.

5.12.12. constantize

Метод constantize решает выражение, ссылающееся на константу, в его получателе:

"Integer".constantize # => Integer

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 сделал бы в том же месте, когда вычислял настоящую константу.

Тестовые случаи рассыльщика получают тестируемый рассыльщик из имени класса теста, используя constantize:

# action_mailer/test_case.rb
def determine_default_mailer(name)
  name.delete_suffix("Test").constantize
rescue NameError => e
  raise NonInferrableMailerError.new(name)
end

Определено в active_support/core_ext/string/inflections.rb.

5.12.13. humanize

Метод humanize настраивает имя атрибута для отображения конечным пользователям.

В частности, он выполняет эти преобразования:

  • Применяет правила словоизменения к аргументу.
  • Удаляет любые предшествующие знаки подчеркивания.
  • Убирает суффикс "_id".
  • Заменяет знаки подчеркивания пробелами.
  • Переводит в нижний регистр все слова, кроме акронимов.
  • Озаглавливает первое слово.

Озаглавливание первого слова может быть выключено с помощью установки опции :capitalize в false (по умолчанию true).

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

Если "SSL" был определен как акроним:

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

Метод хелпера full_messages использует humanize как резервный способ для включения имен атрибутов:

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

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.14. foreign_key

Метод foreign_key дает имя столбца внешнего ключа из имени класса. Чтобы это сделать он демодулизирует, подчеркивает и добавляет "_id":

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

Передайте аргумент false, если не хотите подчеркивание в "_id":

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

Связи используют этот метод для вывода внешних ключей, например has_one и has_many делают так:

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

Определено в active_support/core_ext/string/inflections.rb.

5.12.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_date, to_time, to_datetime

Методы to_date, to_time и to_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 для получения дополнительной информации.

Все три возвратят nil для пустых получателей.

Определено в active_support/core_ext/string/conversions.rb.

6. Расширения для Symbol

6.1. starts_with? и ends_with?

Active Support определяет сторонние псевдонимы для Symbol#start_with? и Symbol#end_with?:

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

Определено в active_support/core_ext/symbol/starts_ends_with.rb.

7. Расширения для Numeric

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

7.2. Время

Следующие методы:

включают вычисление и объявление времени, наподобие 45.minutes + 2.hours + 4.weeks. Их возвращаемое значение также может быть добавлено или вычтено из объектов Time.

Эти методы могут быть объединены с from_now, ago и так далее для уточнения вычисления даты. Например:

# эквивалент для Time.current.advance(days: 1)
1.day.from_now

# эквивалент для Time.current.advance(weeks: 2)
2.weeks.from_now

# эквивалент для Time.current.advance(days: 4, weeks: 5)
(4.days + 5.weeks).from_now

Для других длительностей, обратитесь, пожалуйста, к временному расширению для Integer.

Определено в active_support/core_ext/numeric/time.rb

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

Определено в active_support/core_ext/numeric/conversions.rb.

8. Расширения для Integer

8.1. multiple_of?

Метод multiple_of? проверяет, является ли целое число множителем аргумента:

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

Определено в active_support/core_ext/integer/multiple.rb.

8.2. ordinal

Метод ordinal возвращает суффикс порядковой строки, соответствующей полученному целому числу:

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

Определено в active_support/core_ext/integer/inflections.rb.

8.3. ordinalize

Метод ordinalize возвращает порядковую строку, соответствующую полученному целому числу. Для сравнения отметьте, что метод ordinal возвращает только строковый суффикс.

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

Определено в active_support/core_ext/integer/inflections.rb.

8.4. Время

Следующие методы:

включают объявление и вычисление времени, подобно 4.months + 5.years. Их возвращаемое значение также может быть добавлено или вычтено из объектов Time.

Эти методы могут быть объединены с from_now, ago и так далее для уточнения вычисления даты. Например:

# эквивалент для 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

Для других длительностей, обратитесь, пожалуйста, к временному расширению для Numeric.

Определено в active_support/core_ext/integer/time.rb.

9. Расширения для BigDecimal

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

10. Расширения для Enumerable

10.1. index_by

Метод [index_by][Enumerable#index_by] генерирует хэш с элементами перечисления, индексированными по некоторому ключу.

Он перебирает коллекцию и передает каждый элемент в блок. Значение, возвращенное блоком, будет ключом для элемента:

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

Ключи, как правило, должны быть уникальными. Если блок возвратит одно и то же значение для нескольких элементов, для этого ключа не будет построена коллекция. А значение получит последний элемент.

Определено в active_support/core_ext/enumerable.rb.

[index_by][Enumerable#index_by]

10.2. index_with

Метод 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 ], … }

Определено в active_support/core_ext/enumerable.rb.

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

10.4. exclude?

Предикат exclude? проверяет, является ли заданный объект не принадлежащим коллекции. Это противоположность встроенного include?:

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

Определено в active_support/core_ext/enumerable.rb.

10.5. including

Метод including возвращает новое перечисление, включающее переданные элементы:

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

10.6. excluding

Метод excluding возвращает копию перечисления без указанных элементов:

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

У excluding есть псевдоним without.

Определено в active_support/core_ext/enumerable.rb.

10.7. pluck

Метод pluck возвращает массив на основе заданного ключа:

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

Определено в active_support/core_ext/enumerable.rb.

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

11. Расширения для Array

11.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 с исключенными указанными элементами. Это оптимизация Enumerable#excluding, использующая Array#- вместо Array#reject по причинам производительности.

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

Методы second, third, fourth и fifth возвращают соответствующие элементы, так же как second_to_last и third_to_last (first и last являются встроенными). Благодаря социальной мудрости и всеобщей позитивной конструктивности, forty_two также доступен.

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

Определено в active_support/core_ext/array/access.rb.

11.2. Извлечение

Метод extract! убирает и возвращает элементы, для которых блок возвращает истинное значение. Если блок не задан, вместо этого возвратиться 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.

11.3. Извлечение опций

Когда последний аргумент в вызове метода является хэшем, за исключением, пожалуй, аргумента &block, Ruby позволяет опустить скобки:

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

Этот синтаксический сахар часто используется в Rails для избежания позиционных аргументов там, где их не слишком много, предлагая вместо них интерфейсы, эмулирующие именованные параметры. В частности, очень характерно использовать такой хэш для опций.

Если метод ожидает различное количество аргументов и использует * в своем объявлении, однако хэш опций завершает их и является последним элементом массива аргументов, тогда тип теряет свою роль.

В этих случаях можно задать хэшу опций отличительную трактовку с помощью extract_options!. Метод проверяет тип последнего элемента массива. Если это хэш, он вырезает его и возвращает, в противном случае возвращает пустой хэш.

Давайте рассмотрим пример определения макроса контроллера caches_action:

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

Этот метод получает произвольное число имен экшнов и опциональный хэш опций как последний аргумент. Вызвав extract_options!, получаем хэш опций и убираем его из actions простым и явным способом.

Определено в active_support/core_ext/array/extract_options.rb.

11.4. Преобразование

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

Этот метод принимает три опции:

  • :two_words_connector: Что используется для массивов с длиной 2. По умолчанию " and ".
  • :words_connector: Что используется для соединения элементов массивов с 3 и более элементами, кроме последних двух. По умолчанию ", ".
  • :last_word_connector: Что используется для соединения последних элементов массива из 3 и более элементов. По умолчанию ", and ".

По умолчанию эти опции могут быть локализованы, их ключи следующие:

Опция Ключ I18n
:two_words_connector support.array.two_words_connector
:words_connector support.array.words_connector
:last_word_connector support.array.last_word_connector

Определено в active_support/core_ext/array/conversions.rb.

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

Определено в active_support/core_ext/array/conversions.rb.

11.4.3. to_xml

Метод to_xml возвращает строку, содержащую представление XML его получателя:

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

Чтобы это сделать, он посылает to_xml к каждому элементу за раз и собирает результаты в корневом узле. Все элементы должны откликаться на to_xml, иначе будет вызвано исключение.

По умолчанию имя корневого элемента - это подчеркнутое и dasherize имя класса первого элемента во множественном числе, при условии что остальные элементы принадлежат этому типу (проверяется с помощью is_a?) и они не являются хэшами. В примере выше это "contributors".

Если есть какой-либо элемент, не принадлежащий типу первого, корневой узел становится "objects":

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

Если получатель является массивом хэшей, корневой элемент по умолчанию также "objects":

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

Если коллекция пустая, корневой элемент по умолчанию "nil-classes". Пример для понимания, корневой элемент вышеописанного списка вкладчиков будет не "contributors", если коллекция пустая, а "nil-classes". Можно использовать опцию :root для обеспечения согласованного корневого элемента.

Имя дочерних узлов по умолчанию является именем корневого узла в единственном числе. В вышеприведенных примерах мы видели "contributor" и "object". Опция :children позволяет установить эти имена узлов.

По умолчанию билдер XML является свежим экземпляром Builder::XmlMarkup. Можно сконфигурировать свой собственный билдер через опцию :builder. Метод также принимает опции, такие как :dasherize и ему подобные, они перенаправляются в билдер:

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

Определено в active_support/core_ext/array/conversions.rb.

11.5. Оборачивание

Метод Array.wrap оборачивает свои аргументы в массив, кроме случая когда это уже массив (или массивоподобные).

А именно:

  • Если аргумент nil, возвращается пустой массив.
  • В противном случае, если аргумент откликается на to_ary, он вызывается, и, если значение to_ary не nil, оно возвращается.
  • В противном случае, возвращается массив с аргументом в качестве его первого элемента.
Array.wrap(nil)       # => []
Array.wrap([1, 2, 3]) # => [1, 2, 3]
Array.wrap(0)         # => [0]

Этот метод похож на Kernel#Array, но с некоторыми отличиями:

  • Если аргумент откликается на to_ary, метод вызывается. Kernel#Array начинает пробовать to_a, если вернувшееся значение nil, а Arraw.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.

11.6. Дублирование

Метод Array#deep_dup дублирует себя и все объекты внутри рекурсивно с помощью метода Active Support Object#deep_dup. Он работает так же, как Array#map, посылая метод deep_dup для каждого объекта внутри.

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

Определено в active_support/core_ext/object/deep_dup.rb.

11.7. Группировка

11.7.1. in_groups_of(number, fill_with = nil)

Метод in_groups_of разделяет массив на последовательные группы определенного размера. Он возвращает массив с группами:

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

или выдает их по очереди, если передается блок:

<% 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, сколько нужно, чтобы получить требуемый размер. Можно изменить это набивочное значение используя второй опциональный аргумент:

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

Наконец, можно сказать методу не заполнять последнюю группу, передав false:

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

Как следствие false не может использоваться как набивочное значение.

Определено в active_support/core_ext/array/grouping.rb.

11.7.2. in_groups(number, fill_with = nil)

Метод in_groups разделяет массив на определенное количество групп. Метод возвращает массив с группами:

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

или выдает их по очереди, если передается блок:

%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, если необходимо. Группа может получить не более одного из этих дополнительных элементов, самый правый, если таковой имеется. И группы, получившие его, будут всегда последние.

Можно изменить это набивочное значение, используя второй опциональный аргумент:

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

Также можно сказать методу не заполнять меньшие группы, передав false:

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

Как следствие, false не может быть набивочным значением.

Определено в active_support/core_ext/array/grouping.rb.

11.7.3. split(value = nil)

Метод split разделяет массив разделителем и возвращает получившиеся куски.

Если передан блок, разделителями будут те элементы массива, для которых блок возвращает true:

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

В противном случае, значение, полученное как аргумент, которое по умолчанию является nil, будет разделителем:

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

Отметьте, в предыдущем примере, что последовательные разделители приводят к пустым массивам.

Определено в active_support/core_ext/array/grouping.rb.

12. Расширения для Hash

12.1. Конверсия

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

Чтобы это сделать, метод в цикле проходит пары и создает узлы, зависимые от value. Для заданной пары key, value:

  • Если value - хэш, происходит рекурсивный вызов с key как :root.
  • Если value - массив, происходит рекурсивный вызов с key как :root, и key в единственном числе как :children.
  • Если value - вызываемый объект, он должен ожидать один или два аргумента. В зависимости от ситуации, вызываемый объект вызывается с помощью хэша options в качестве первого аргумента с key как :root, и key в единственном числе в качестве второго аргумента. Возвращенное значение становится новым узлом.
  • Если value откликается на to_xml, метод вызывается с key как :root.
  • В иных случаях, узел с key в качестве тега создается со строковым представлением value в качестве текстового узла. Если value является nil, добавляется атрибут "nil", установленный в "true". Кроме случаев, когда существует опция :skip_types со значением true, добавляется атрибут "type", соответствующий следующему преобразованию:
XML_TYPE_NAMES = {
  "Symbol"     => "symbol",
  "Integer"    => "integer",
  "BigDecimal" => "decimal",
  "Float"      => "float",
  "TrueClass"  => "boolean",
  "FalseClass" => "boolean",
  "Date"       => "date",
  "DateTime"   => "datetime",
  "Time"       => "datetime"
}

По умолчанию корневой узел является "hash", но это настраивается с помощью опции :root.

По умолчанию билдер XML является новым экземпляром Builder::XmlMarkup. Можно настроить свой собственный билдер с помощью опции :builder. Метод также принимает опции, такие как :dasherize и ему подобные, они направляются в билдер.

Определено в active_support/core_ext/hash/conversions.rb.

12.2. Объединение

В Ruby имеется встроенный метод Hash#merge, который позволяет объединять два хэша:

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

Active Support определяет еще несколько способов объединения хэшей, которые могут быть полезными.

12.2.1. reverse_merge и reverse_merge!

В случае коллизии, в merge остается ключ в хэше аргумента. Можно компактно предоставить хэш-опцию со значениями по умолчанию с помощью такой идиомы:

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

Active Support определяет reverse_merge в случае, если нужна альтернативная запись:

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

И вариант с восклицательным знаком reverse_merge!, который выполняет объединение, модифицируя на месте:

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

Обратите внимание, что reverse_merge! может изменить хэш в вызывающем методе, что может как быть, так и не быть хорошей идеей.

Определено в active_support/core_ext/hash/reverse_merge.rb.

12.2.2. reverse_update

Метод reverse_update это псевдоним для reverse_merge!, описанного выше.

Отметьте, что у reverse_update нет варианта с восклицательным знаком.

Определено в active_support/core_ext/hash/reverse_merge.rb.

12.2.3. deep_merge и deep_merge!

Как можно было видеть в предыдущем примере, если ключ обнаруживается в обоих хэшах, выбирается значение первого из аргументов.

Active Support определяет Hash#deep_merge. В углубленном объединении, если один и тот же ключ обнаруживается в обоих хэшах, и их значения также хэши, то в результирующем хэше будет объединение их значений.

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

Метод deep_merge! выполняет углубленное объединение, модифицируя на месте:

Определено в active_support/core_ext/hash/deep_merge.rb.

12.3. Глубокое дублирование

Метод Hash#deep_dup дублирует себя, а также все ключи и значения внутри, рекурсивно с помощью метода Active Support Object#deep_dup. Он работает так же, как Enumerator#each_with_object, посылая метод deep_dup в каждую пару внутри.

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

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

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

Определено в active_support/core_ext/object/deep_dup.rb.

12.4. Работа с ключами

12.4.1. except и except!

Метод except возвращает хэш с убранными ключами, содержащимися в перечне аргументов, если они существуют:

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

Если получатель откликается на convert_key, метод вызывается на каждом из аргументов. Это позволяет except хорошо обращаться с хэшами с индифферентным доступом, например:

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

Также имеется вариант с восклицательным знаком except!, который убирает ключи в самом получателе.

Определено в active_support/core_ext/hash/except.rb.

12.4.2. stringify_keys и stringify_keys!

Метод stringify_keys возвращает хэш, в котором ключи получателя преобразованы в строку. Это выполняется с помощью применения к ним to_s:

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

В случае коллизии ключей, значением будет то, которое вставлено в хэш позже:

{ "a" => 1, a: 2 }.stringify_keys
# Результатом будет
# => {"a"=>2}

Метод может быть полезным, к примеру, для простого принятия и символов, и строк как опций. Например, ActionView::Helpers::FormHelper определяет:

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

Вторая строчка может безопасно обратиться к ключу "type" и позволить пользователю передавать или :type, или "type".

Также имеется вариант с восклицательным знаком stringify_keys!, который преобразует к строке ключи в самом получателе.

Кроме этого, можно использовать deep_stringify_keys и deep_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.

12.4.3. symbolize_keys и symbolize_keys!

Метод symbolize_keys возвращает хэш, в котором ключи получателя преобразованы к символам там, где это возможно. Это выполняется с помощью применения к ним to_sym:

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

Отметьте в предыдущем примере, что только один ключ был преобразован к символу.

В случае коллизии ключей, значением будет то, которое вставлено в хэш позже:

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

Метод может быть полезным, к примеру, для простого принятия и символов, и строк как опций. Например, ActionText::TagHelper определяет

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

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

Третья строчка может безопасно обратиться к ключу :input и позволить пользователю передавать или :input, или "input".

Также имеется вариант с восклицательным знаком symbolize_keys!, который приводит к символу ключи в самом получателе.

Кроме этого, можно использовать deep_symbolize_keys и deep_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.

12.4.4. to_options и to_options!

Методы to_options и to_options! являются псевдонимами symbolize_keys и symbolize_keys! соответственно.

Определено в active_support/core_ext/hash/keys.rb.

12.4.5. assert_valid_keys

Метод assert_valid_keys получает определенное число аргументов и проверяет, имеет ли получатель хоть один ключ вне этого белого списка. Если имеет, вызывается ArgumentError.

{ a: 1 }.assert_valid_keys(:a)  # проходит
{ a: 1 }.assert_valid_keys("a") # ArgumentError

Active Record не принимает незнакомые опции при создании связей, к примеру. Он реализует такой контроль через assert_valid_keys.

Определено в active_support/core_ext/hash/keys.rb.

12.5. Работа со значениям

12.5.1. deep_transform_values и deep_transform_values!

Метод deep_transform_values возвращает новый хэш со всеми значениями, конвертированными с помощью операции блока. Он включает значения корневого хэша и из всех вложенных хэшей и массивов.

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

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

Также имеется восклицательный вариант deep_transform_values!, деструктивно конвертирующий все значения с помощью операции блока.

Определено в active_support/core_ext/hash/deep_transform_values.rb.

12.6. Нарезка

Метод slice! заменяет хэш только заданными ключами и возвращает хэш, содержащий убранные пары ключ/значение.

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

Определено в active_support/core_ext/hash/slice.rb.

12.7. Извлечение

Метод extract! убирает и возвращает пары ключ/значение, соответствующие заданным ключам.

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

Метод extract! возвращает тот же подкласс Hash, каким является получатель.

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

Определено в active_support/core_ext/hash/slice.rb.

12.8. Индифферентный доступ

Метод with_indifferent_access возвращает ActiveSupport::HashWithIndifferentAccess из своего получателя:

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

Определено в active_support/core_ext/hash/indifferent_access.rb.

13. Расширения для Regexp

13.1. multiline?

Метод multiline? говорит, имеет ли регулярное выражение установленный флаг /m, то есть соответствует ли точка новым строкам.

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

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

Rails использует этот метод в одном месте, в коде маршрутизации. Регулярные выражения Multiline недопустимы для маршрутных требований, и этот флаг облегчает соблюдение этого ограничения.

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.

14. Расширения для Range

14.1. to_fs

Active Support определяет Range#to_s как альтернативу to_s, которая понимает опциональный аргумент формата. В настоящий момент имеется только один поддерживаемый формат, отличный от дефолтного, это :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 в поддержке значений интервала в условиях.

Определено в active_support/core_ext/range/conversions.rb.

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

Определено в active_support/core_ext/range/compare_range.rb.

14.3. overlap?

Метод Range#overlap? говорит, имеют ли два заданных интервала непустое пересечение:

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

15. Расширения для Date

15.1. Вычисления

Следующие методы вычисления имеют временную пропасть в октябре 1582 года, когда дней с 5 по 14 (включительно) просто не существовало. Это руководство не документирует свое поведение в те дни для краткости, но достаточно сказать, будет происходит то, что от них ожидается. То есть, Date.new(1582, 10, 4).tomorrow возвратит Date.new(1582, 10, 15), и так далее. Пожалуйста, проверьте test/core_ext/date_ext_test.rb в тестовом наборе Active Support, чтобы понять ожидаемое поведение.

15.1.1. Date.current

Active Support определяет Date.current как сегодняшний день в текущей временной зоне. Он похож на Date.today, за исключением того, что он учитывает временную зону пользователя, если она определена. Он также определяет Date.yesterday и Date.tomorrow, и предикаты экземпляра past?, today?, tomorrow?, next_day?, yesterday?, prev_day?, future?, on_weekday? и on_weekend?, все они зависят от Date.current.

Определено в active_support/core_ext/date/calculations.rb.

15.1.2. Именованные даты
15.1.2.1. beginning_of_week, end_of_week

Методы beginning_of_week и end_of_week возвращают даты начала и конца недели соответственно. Предполагается, что неделя начинается с понедельника, но это может быть изменено переданным аргументом, установив локально для треда Date.beginning_of_week или config.beginning_of_week.

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

У beginning_of_week есть псевдоним at_beginning_of_week, а у end_of_week есть псевдоним at_end_of_week.

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.2.2. monday, sunday

Методы monday и sunday возвращают даты предыдущего понедельника или следующего воскресенья соответственно.

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

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

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

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.2.3. prev_week, next_week

Метод next_week принимает символ с днем недели на английском (по умолчанию локально для треда Date.beginning_of_week или config.beginning_of_week, или :monday) и возвращает дату, соответствующую этому дню на следующей неделе:

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

Метод prev_week работает аналогично:

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

У prev_week есть псевдоним last_week.

И next_week, и prev_week работают так, как нужно, когда установлен Date.beginning_of_week или config.beginning_of_week.

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.2.4. beginning_of_month, end_of_month

Методы beginning_of_month и end_of_month возвращают даты начала и конца месяца:

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

У beginning_of_month есть псевдоним at_beginning_of_month, а у end_of_month есть псевдоним at_end_of_month.

Определено в active_support/core_ext/date_and_time/calculations.rb.

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

У beginning_of_quarter есть псевдоним at_beginning_of_quarter, а у end_of_quarter есть псевдоним at_end_of_quarter.

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.2.6. beginning_of_year, end_of_year

Методы beginning_of_year и end_of_year возвращают даты начала и конца года:

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

У beginning_of_year есть псевдоним at_beginning_of_year, а у end_of_year есть псевдоним at_end_of_year.

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.3. Другие вычисления дат
15.1.3.1. years_ago, years_since

Метод years_ago получает число лет и возвращает ту же дату, что и много лет назад:

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

years_since перемещает вперед по времени:

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

Если такая дата не найдена, возвращается последний день соответствующего месяца:

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

last_year это сокращение для #years_ago(1).

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.3.2. months_ago, months_since

Методы months_ago и months_since работают аналогично, но для месяцев:

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

Если такой день не существует, возвращается последний день соответствующего месяца:

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

last_month это сокращение для #months_ago(1).

Определено в active_support/core_ext/date_and_time/calculations.rb.

15.1.3.3. weeks_ago, weeks_since

Методы weeks_ago и [weeks_since][DateAndTime::Calculations#week_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

Определено в active_support/core_ext/date_and_time/calculations.rb.

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

Отметьте в предыдущем примере, что приросты могут быть отрицательными.

Определено в active_support/core_ext/date/calculations.rb.

15.1.4. Изменение компонентов

Метод change позволяет получить новую дату, которая идентична получателю, за исключением заданного года, месяца или дня:

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

Метод не принимает несуществующие даты, если изменение невалидно, вызывается ArgumentError:

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

Определено в active_support/core_ext/date/calculations.rb.

15.1.5. Длительности

Объекты Duration могут добавляться и вычитаться из дат:

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

Они переводят в вызовы since или advance. Например, здесь мы получим правильный переход ко времени календарной реформы:

Date.new(1582, 10, 4) + 1.day
# => Fri, 15 Oct 1582
15.1.6. Временные метки

Следующие методы возвращают объект Time, если возможно, в противном случае DateTime. Если установлено, учитывается временная зона пользователя.

15.1.6.1. beginning_of_day, end_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

У beginning_of_day есть псевдонимы at_beginning_of_day, midnight, at_midnight.

Определено в active_support/core_ext/date/calculations.rb.

15.1.6.2. beginning_of_hour, end_of_hour

Метод beginning_of_hour возвращает временную метку в начале часа (hh:00:00):

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

Метод end_of_hour возвращает временную метку в конце часа (hh:59:59):

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

У beginning_of_hour есть псевдоним at_beginning_of_hour.

Определено в active_support/core_ext/date_time/calculations.rb.

15.1.6.3. beginning_of_minute, end_of_minute

Метод beginning_of_minute возвращает временную метку в начале минуты (hh:mm:00):

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

Метод end_of_minute возвращает временную метку в конце минуты (hh:mm:59):

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

У beginning_of_minute есть псевдоним at_beginning_of_minute.

beginning_of_hour, end_of_hour, beginning_of_minute и end_of_minute реализованы для Time и DateTime, но не для Date, так как у экземпляра Date не имеет смысла спрашивать о начале или окончании часа или минуты.

Определено в active_support/core_ext/date_time/calculations.rb.

15.1.6.4. ago, since

Метод ago получает количество секунд как аргумент и возвращает временную метку, имеющую столько секунд до полуночи:

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

Подобным образом since двигается вперед:

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

Определено в active_support/core_ext/date/calculations.rb.

16. Расширения для DateTime

DateTime не знает о правилах DST (переходов на летнее время), и поэтому некоторые из этих методов сталкиваются с временной пропастью, когда переход на и с летнего времени имеет место. К примеру, seconds_since_midnight может не возвратить настоящее значение для таких дней.

16.1. Вычисления

Класс DateTime является подклассом Date, поэтому загрузив active_support/core_ext/date/calculations.rb будут унаследованы эти методы и их псевдонимы, за исключением того, что они будут всегда возвращать дату и время.

Следующие методы переопределены, поэтому не нужно загружать active_support/core_ext/date/calculations.rb для них:

С другой стороны, advance и change также определены, они описаны ниже.

Следующие методы реализованы только в active_support/core_ext/date_time/calculations.rb, так как они имеют смысл только при использовании с экземпляром DateTime:

16.1.1. Именованные Datetime
16.1.1.1. DateTime.current

Active Support определяет DateTime.current похожим на Time.now.to_datetime, за исключением того, что он учитывает временную зону пользователя, если она определена. Он также определяет DateTime.yesterday и DateTime.tomorrow, и предикаты экземпляра past? и future? относительно DateTime.current.

Определено в active_support/core_ext/date_time/calculations.rb.

16.1.2. Другие расширения
16.1.2.1. seconds_since_midnight

Метод seconds_since_midnight возвращает число секунд, прошедших с полуночи:

now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
now.seconds_since_midnight # => 73596

Определено в active_support/core_ext/date_time/calculations.rb.

16.1.2.2. utc

Метод utc выдает такую же дату и время получателя, но выраженную в UTC.

now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
now.utc                # => Mon, 07 Jun 2010 23:27:52 +0000

Также у этого метода есть псевдоним getutc.

Определено в active_support/core_ext/date_time/calculations.rb.

16.1.2.3. utc?

Предикат utc? сообщает, имеет ли получатель UTC в качестве своей временной зоны:

now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
now.utc?           # => false
now.utc.utc?       # => true

Определено в active_support/core_ext/date_time/calculations.rb.

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

Этот метод сначала вычисляет дату назначения, передавая :years, :months, :weeks и :days в Date#advance, описанный ранее. После этого, он корректирует время, вызвав since с количеством секунд, на которое нужно передвинуть. Этот порядок обоснован, другой порядок мог бы дать другие дату и время для некоторых временных пропастей. Используем пример в Date#advance, и расширим его, показав обоснованность порядка, применимого к единицам измерения времени.

Если сначала передвинуть единицы измерения даты (относительный порядок вычисления, показанный ранее), а затем единицы измерения времени, мы получим для примера следующее вычисление:

d = DateTime.new(2010, 2, 28, 23, 59, 59)
# => Sun, 28 Feb 2010 23:59:59 +0000
d.advance(months: 1, seconds: 1)
# => Mon, 29 Mar 2010 00:00:00 +0000

но если мы вычисляем обратным способом, результат будет иным:

d.advance(seconds: 1).advance(months: 1)
# => Thu, 01 Apr 2010 00:00:00 +0000

Поскольку DateTime не поддерживает DST (переход на летнее время), можно получить несуществующий момент времени без каких-либо предупреждений или сообщений об ошибке.

Определено в active_support/core_ext/date_time/calculations.rb.

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

Если часы обнуляются, то минуты и секунды тоже (если у них не заданы значения):

now.change(hour: 0)
# => Tue, 08 Jun 2010 00:00:00 +0000

Аналогично, если минуты обнуляются, то секунды тоже (если у них не задано значение):

now.change(min: 0)
# => Tue, 08 Jun 2010 01:00:00 +0000

Этот метод не принимает несуществующие даты, если изменение невалидно, вызывается ArgumentError:

DateTime.current.change(month: 2, day: 30)
# => ArgumentError: invalid date

Определено в active_support/core_ext/date_time/calculations.rb.

16.1.4. Длительности

Объекты Duration могут добавляться и вычитаться из даты и времени:

now = DateTime.current
# => Mon, 09 Aug 2010 23:15:17 +0000
now + 1.year
# => Tue, 09 Aug 2011 23:15:17 +0000
now - 1.week
# => Mon, 02 Aug 2010 23:15:17 +0000

Они переводят в вызовы since или advance. Например, здесь мы получим правильный переход ко времени календарной реформы:

DateTime.new(1582, 10, 4, 23) + 1.hour
# => Fri, 15 Oct 1582 00:00:00 +0000

17. Расширения для Time

17.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
  • Если since или ago переходят на время, которое не может быть выражено с помощью Time, вместо него возвращается объект DateTime.
17.1.1. Time.current

Active Support определяет Time.current как сегодняшний день в текущей временной зоне. Он похож на Time.now, за исключением того, что он учитывает временную зону пользователя, если она определена. Он также определяет предикаты экземпляра past?, today?, tomorrow?, next_day?, yesterday?, prev_day? и future?, все они относительны к Time.current.

При осуществлении сравнения Time с использованием методов, учитывающих временную зону пользователя, убедитесь, что используете Time.current вместо Time.now. Есть случаи, когда временная зона пользователя может быть в будущем по сравнению с временной зоной системы, в которой по умолчанию используется Time.now. Это означает, что Time.now.to_date может быть равным Date.yesterday.

Определено в active_support/core_ext/time/calculations.rb.

17.1.2. all_day, all_week, all_month, all_quarter и all_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

Определено в active_support/core_ext/date_and_time/calculations.rb.

17.1.3. prev_day, next_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

Определено в active_support/core_ext/time/calculations.rb.

17.1.4. prev_month, next_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

Определено в active_support/core_ext/time/calculations.rb.

17.1.5. prev_year, next_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

Если датой является 29 февраля високосного года, возвратится 28-е:

t = Time.new(2000, 2, 29) # => 2000-02-29 00:00:00 +0900
t.prev_year               # => 1999-02-28 00:00:00 +0900
t.next_year               # => 2001-02-28 00:00:00 +0900

Определено в active_support/core_ext/time/calculations.rb.

17.1.6. prev_quarter, next_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

prev_quarter имеет псевдоним last_quarter.

Определено в active_support/core_ext/date_and_time/calculations.rb.

17.2. Конструкторы Time

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 на запущенной платформе, usecs отбрасываются и вместо этого возвращается объект DateTime.

17.2.1. Длительности

Объекты Duration могут быть добавлены и вычтены из объектов времени:

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

18. Расширения для File

18.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 не может изменить владельца или права доступа на файл, эта ошибка отлавливается и пропускается, позволяя файловой системе убедиться, что файл доступен для необходимых манипуляций.

Благодаря операции chmod, выполняемой atomic_write, если у целевого файла установлен ACL, то этот ACL будет пересчитан/модифицирован.

Отметьте, что с помощью atomic_write нельзя дописывать.

Вспомогательный файл записывается в стандартной директории для временных файлов, но можно передать эту директорию как второй аргумент.

Определено в active_support/core_ext/file/atomic.rb.

19. Расширения для NameError

Active Support добавляет missing_name? к NameError, который проверяет было ли исключение вызвано в связи с тем, что имя было передано как аргумент.

Имя может быть задано как символ или строка. Символ проверяется как простое имя константы, строка - как полностью ограниченное имя константы.

Символ может представлять полностью ограниченное имя константы как :"ActiveRecord::Base", такое поведение для символов определено для удобства, а не потому, что такое возможно технически.

К примеру, когда вызывается экшн ArticlesController, Rails пытается оптимистично использовать ArticlesHelper. Это нормально, когда не существует модуля хелпера, поэтому если вызывается исключение для этого имени константы, оно должно молчать. Но в случае, если articles_helper.rb вызывает NameError благодаря неизвестной константе, оно должно быть перевызвано. Метод missing_name? предоставляет способ проведения различия в этих двух случаях:

def default_helper_module!
  module_name = name.delete_suffix("Controller")
  module_path = module_name.underscore
  helper module_path
rescue LoadError => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

Определено в active_support/core_ext/name_error.rb.

20. Расширения для LoadError

Active Support добавляет is_missing? к LoadError.

Для заданного имени пути is_missing? проверяет, будет ли вызвано исключение из-за определенного файла (за исключением файлов с расширением ".rb").

Например, когда вызывается экшн ArticlesController, Rails пытается загрузить articles_helper.rb, но этот файл может не существовать. Это нормально, модуль хелпера не обязателен, поэтому Rails умалчивает ошибку загрузки. Но может быть случай, что модуль хелпера существует, и в свою очередь требует другую библиотеку, которая отсутствует. В этом случае Rails должен вызвать исключение. Метод is_missing? предоставляет способ проведения различия в этих двух случаях:

def default_helper_module!
  module_name = name.delete_suffix("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.

21. Расширения для Pathname

21.1. existence

Метод existence возвращает получатель, если существует названный файл, а в противном случае возвращает nil. Это полезно для подобных идиом:

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

Определено в active_support/core_ext/pathname/existence.rb.