更多内容请访问 rubyonrails.org:

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,该注释为

这意味着您可以像这样引用它

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

Active Support 经过仔细修订,因此挑选文件只加载严格需要的依赖项(如果有的话)。

1.1.2. 加载分组的核心扩展

下一个级别是简单地加载 Hash 的所有扩展。根据经验,加载 active_support/core_ext/some_class 即可一次性获取 SomeClass 的所有扩展。

因此,要加载 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. Ruby on Rails 应用程序中的 Active Support

除非 config.active_support.bare 为 true,否则 Ruby on Rails 应用程序会加载所有 Active Support。在这种情况下,应用程序只会加载框架本身为其自身需求挑选的内容,并且仍然可以按照上一节所述的任何粒度级别进行挑选。

2. 所有对象的扩展

2.1. blank?present?

在 Rails 应用程序中,以下值被认为是空白的

  • nilfalse

  • 仅由空白字符组成的字符串(请参阅下面的注释),

  • 空数组和哈希,以及

  • 任何其他响应 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

2.2. presence

如果 present? 为真,presence 方法返回其接收者,否则返回 nil。它对于以下习语很有用

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

2.3. duplicable?

从 Ruby 2.5 开始,大多数对象都可以通过 dupclone 进行复制

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

Active Support 提供了 duplicable? 来查询对象是否可复制

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

任何类都可以通过删除 dupclone 或从中引发异常来禁止复制。因此,只有 rescue 才能判断给定任意对象是否可复制。duplicable? 取决于上面硬编码的列表,但它比 rescue 快得多。仅当您知道硬编码列表足以满足您的用例时才使用它。

2.4. deep_dup

deep_dup 方法返回给定对象的深层副本。通常,当您 dup 包含其他对象的对象时,Ruby 不会 dup 它们,因此它会创建对象的浅层副本。例如,如果您有一个包含字符串的数组,它将如下所示

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

duplicate.push "another-string"

# the object was duplicated, so the element was added only to the duplicate
array     # => ["string"]
duplicate # => ["string", "another-string"]

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

# first element was not duplicated, it will be changed in both arrays
array     # => ["foo"]
duplicate # => ["foo, "another-string"]

如您所见,复制 Array 实例后,我们得到了另一个对象,因此我们可以修改它而原始对象将保持不变。但是,对于数组的元素来说,情况并非如此。由于 dup 不会进行深层复制,因此数组中的字符串仍然是同一个对象。

如果您需要对象的深层副本,则应使用 deep_dup。这是一个示例

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

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

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

如果对象不可复制,deep_dup 将直接返回它

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

2.5. try

当您只想在对象不为 nil 时才调用其方法时,最简单的方法是使用条件语句,但会增加不必要的混乱。替代方法是使用 trytry 类似于 Object#public_send,但如果发送给 nil,则返回 nil

这是一个示例

# without try
unless @number.nil?
  @number.next
end

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

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

2.7. acts_like?(duck)

方法 acts_like? 提供了一种根据简单约定检查某个类是否像另一个类的方法:提供与 String 相同接口的类定义

def acts_like_string?
end

这只是一个标记,其主体或返回值无关紧要。然后,客户端代码可以这样查询鸭子类型安全性

some_klass.acts_like?(:string)

Rails 有些类行为类似于 DateTime,并且遵循此约定。

2.8. to_param

Rails 中的所有对象都响应 to_param 方法,该方法旨在返回表示它们在查询字符串中作为值或作为 URL 片段的值。

默认情况下,to_param 只调用 to_s

7.to_param # => "7"

to_param 的返回值不应该被转义

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

Rails 中的几个类覆盖了此方法。

例如,niltruefalse 返回它们自身。Array#to_param 对元素调用 to_param 并用 "/" 连接结果

[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] 的值。

2.9. to_query

to_query 方法构造一个查询字符串,将给定的 keyto_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,但签名不同。如果没有传递参数,则调用会生成一系列有序的键/值赋值,对值调用 to_query(key)。然后它用 "&" 连接结果

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

方法 Hash#to_query 接受可选的键命名空间

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

2.10. with_options

with_options 方法提供了一种在一系列方法调用中提取共同选项的方法。

给定一个默认选项哈希,with_options 将一个代理对象交给块。在块内,对代理对象调用的方法将转发给接收者,并合并其选项。例如,您可以消除以下重复

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

这样

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

这种表达方式也可以向读者传达“分组”的含义。例如,假设您要发送一份时事通讯,其语言取决于用户。在邮件程序中的某个地方,您可以像这样对与区域设置相关的部分进行分组

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

由于 with_options 将调用转发给其接收者,因此它们可以嵌套。每个嵌套级别除了自身之外,还将合并继承的默认值。

2.11. JSON 支持

Active Support 提供了比 json gem 通常为 Ruby 对象提供的 to_json 更好的实现。这是因为某些类(如 HashProcess::Status)需要特殊处理才能提供正确的 JSON 表示。

2.12. 实例变量

Active Support 提供了几种方法来简化对实例变量的访问。

2.12.1. instance_values

instance_values 方法返回一个哈希,该哈希将没有“@”的实例变量名称映射到其相应的值。键是字符串

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

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

2.12.2. instance_variable_names

instance_variable_names 方法返回一个数组。每个名称都包含“@”符号。

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

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

2.13. 抑制警告和异常

方法 silence_warningsenable_warnings 在其块的持续时间内相应地更改 $VERBOSE 的值,并在之后重置它

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

也可以使用 suppress 抑制异常。此方法接收任意数量的异常类。如果在块执行期间引发异常并且其 kind_of? 任何参数,suppress 将捕获它并静默返回。否则异常不会被捕获

# If the user is locked, the increment is lost, no big deal.
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

2.14. in?

谓词 in? 测试一个对象是否包含在另一个对象中。如果传递的参数不响应 include?,则会引发 ArgumentError 异常。

in? 的示例

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

3. Module 的扩展

3.1. 属性

3.1.1. alias_attribute

模型属性具有读取器、写入器和谓词。您可以使用 alias_attribute 为模型属性创建别名,并为您定义所有相应的三个方法。与其他别名方法一样,新名称是第一个参数,旧名称是第二个(一个助记符是它们的顺序与赋值相同)

class User < ApplicationRecord
  # You can refer to the email column as "login".
  # This can be meaningful for authentication code.
  alias_attribute :login, :email
end

3.1.2. 内部属性

当您在一个旨在被子类化的类中定义属性时,命名冲突是一个风险。这对于库来说尤为重要。

Active Support 定义了宏 attr_internal_readerattr_internal_writerattr_internal_accessor。它们的行为类似于 Ruby 内置的 attr_* 对应项,只是它们以一种不太可能发生冲突的方式命名底层实例变量。

attr_internalattr_internal_accessor 的同义词

# library
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# client code
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

在前面的示例中,:log_level 可能不属于库的公共接口,仅用于开发。客户端代码,不知道潜在的冲突,子类化并定义了自己的 :log_level。由于 attr_internal,没有冲突。

默认情况下,内部实例变量以一个下划线开头命名,在上面的示例中为 @_log_level。这可以通过 Module.attr_internal_naming_format 进行配置,您可以传递任何类似 sprintf 的格式字符串,其中包含一个前导 @ 和一个 %s,后者是放置名称的位置。默认值为 "@_%s"

Rails 在一些地方使用内部属性,例如视图

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

3.1.3. 模块属性

mattr_readermattr_writermattr_accessor 与为类定义的 cattr_* 宏相同。实际上,cattr_* 宏只是 mattr_* 宏的别名。请查看 类属性

例如,Active Storage 日志记录器的 API 是使用 mattr_accessor 生成的

module ActiveStorage
  mattr_accessor :logger
end

3.2. 父级

3.2.1. module_parent

嵌套命名模块上的 module_parent 方法返回包含其相应常量的模块

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

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

如果模块是匿名的或属于顶层,module_parent 返回 Object

请注意,在这种情况下,module_parent_name 返回 nil

3.2.2. module_parent_name

嵌套命名模块上的 module_parent_name 方法返回包含其相应常量的模块的完全限定名称

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

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

对于顶级或匿名模块,module_parent_name 返回 nil

请注意,在这种情况下,module_parent 返回 Object

3.2.3. module_parents

module_parents 方法在接收者上调用 module_parent,并向上调用直到达到 Object。链以数组形式返回,从下到上

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

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

3.3. 匿名

模块可能有名称,也可能没有名称

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

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

Module.new.name # => nil

您可以使用谓词 anonymous? 检查模块是否有名称

module M
end
M.anonymous? # => false

Module.new.anonymous? # => true

请注意,不可达并不意味着匿名

module M
end

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

m.anonymous? # => false

尽管匿名模块根据定义是不可达的。

3.4. 方法委托

3.4.1. delegate

delegate 提供了一种转发方法的简单方法。

假设某个应用程序中的用户在 User 模型中拥有登录信息,但在单独的 Profile 模型中拥有姓名和其他数据

class User < ApplicationRecord
  has_one :profile
end

通过这种配置,您可以通过用户的个人资料 user.profile.name 获取用户的姓名,但仍然可以直接访问此类属性可能会很方便

class User < ApplicationRecord
  has_one :profile

  def name
    profile.name
  end
end

这就是 delegate 为您所做的事情

class User < ApplicationRecord
  has_one :profile

  delegate :name, to: :profile
end

它更短,意图更明显。

该方法必须在目标中是公共的。

delegate 宏接受多个方法

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

当插入到字符串中时,:to 选项应成为一个表达式,该表达式评估为方法委托到的对象。通常是字符串或符号。此类表达式在接收者的上下文中进行评估

# delegates to the Rails constant
delegate :logger, to: :Rails

# delegates to the receiver's class
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 以更改此设置。

3.4.2. delegate_missing_to

想象一下,您希望将 User 对象中缺失的所有内容委托给 Profile 对象。delegate_missing_to 宏让您轻松实现这一点

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

目标可以是对象内部任何可调用的东西,例如实例变量、方法、常量等。只有目标的公共方法被委托。

3.5. 重定义方法

在某些情况下,您需要使用 define_method 定义一个方法,但不知道是否已经存在同名方法。如果存在,并且启用了警告,则会发出警告。没什么大不了的,但也不干净。

方法 redefine_method 会阻止此类潜在警告,如果需要,它会先删除现有方法。

如果您需要自己定义替换方法(例如,因为您正在使用 delegate),也可以使用 silence_redefinition_of_method

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, comes from A
a2.x # => 2, overridden in 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_readerfalse 时,实例谓词返回 NoMethodError,就像读取器方法一样。

如果您不想要实例谓词,请传递 instance_predicate: false,它将不会被定义。

4.1.2. cattr_readercattr_writercattr_accessor

cattr_readercattr_writercattr_accessor 类似于它们的 attr_* 对应项,但适用于类。它们将类变量初始化为 nil,除非它已经存在,并生成相应的类方法来访问它

class MysqlAdapter < AbstractAdapter
  # Generates class methods to access @@emulate_booleans.
  cattr_accessor :emulate_booleans
end

此外,您可以向 cattr_* 传递一个块,以使用默认值设置属性

class MysqlAdapter < AbstractAdapter
  # Generates class methods to access @@emulate_booleans with default value of true.
  cattr_accessor :emulate_booleans, default: true
end

为了方便起见,也创建了实例方法,但它们只是内部值的代理,该值在类之间共享。因此,当实例修改值时,这会影响整个类层次结构。此行为与 class_attribute(见上文)不同。

例如

class Foo
  cattr_accessor :bar
end

instance = Foo.new

Foo.bar = 1
instance.bar # => 1

instance.bar = 2
Foo.bar # => 2

可以通过将 :instance_reader 设置为 false 来阻止读取器实例方法的生成,通过将 :instance_writer 设置为 false 来阻止写入器实例方法的生成。通过将 :instance_accessor 设置为 false 来阻止这两种方法的生成。在所有情况下,该值必须严格为 false,而不是任何假值。

module A
  class B
    # No first_name instance reader is generated.
    cattr_accessor :first_name, instance_reader: false
    # No last_name= instance writer is generated.
    cattr_accessor :last_name, instance_writer: false
    # No surname instance reader or surname= writer is generated.
    cattr_accessor :surname, instance_accessor: false
  end
end

模型可能会发现将 :instance_accessor 设置为 false 作为防止批量赋值设置属性的方法很有用。

4.2. 子类和后代

4.2.1. subclasses

subclasses 方法返回接收者的子类

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

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

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

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

这些类的返回顺序是不确定的。

4.2.2. descendants

descendants 方法返回所有小于其接收者的类

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

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

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

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

这些类的返回顺序是不确定的。

5. String 的扩展

5.1. 输出安全

5.1.1. 动机

将数据插入 HTML 模板需要额外小心。例如,您不能将 @review.title 原样插入 HTML 页面。一方面,如果评论标题是 "Flanagan & Matz rules!",输出将无法格式良好,因为&符号必须转义为 "&amp;"。更重要的是,根据应用程序的不同,这可能是一个巨大的安全漏洞,因为用户可以通过设置精心制作的评论标题来注入恶意 HTML。有关风险的更多信息,请查看 安全指南 中关于跨站脚本攻击的部分。

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 %> <%# fine, escaped if needed %>

要原样插入内容,请使用 raw 助手而不是调用 html_safe

<%= raw @cms.current_template %> <%# inserts @cms.current_template as is %>

或者,等效地,使用 <%==

<%== @cms.current_template %> <%# inserts @cms.current_template as is %>

raw 助手会为您调用 html_safe

def raw(stringish)
  stringish.to_s.html_safe
end

5.1.3. 转换

通常,除了上面解释的字符串连接之外,任何可能更改字符串的方法都会给您一个不安全的字符串。这些方法包括 downcasegsubstripchompunderscore 等。

对于 gsub! 等原地转换,接收器本身变得不安全。

无论转换是否实际更改了内容,安全位都会丢失。

5.1.4. 转换和强制类型转换

对安全字符串调用 to_s 返回安全字符串,但使用 to_str 进行强制类型转换返回不安全字符串。

5.1.5. 复制

在安全字符串上调用 dupclone 会生成安全字符串。

5.2. remove

remove 方法将删除模式的所有出现

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

还有一个破坏性版本 String#remove!

5.3. squish

squish 方法去除开头和结尾的空白字符,并将连续的空白字符替换为单个空格

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

还有破坏性版本 String#squish!

请注意,它同时处理 ASCII 和 Unicode 空白。

5.4. truncate

truncate 方法返回其接收者的副本,该副本在给定 length 后截断

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

省略号可以使用 :omission 选项自定义

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

特别注意,截断会考虑省略字符串的长度。

传递一个 :separator 以在自然断点处截断字符串

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

选项 :separator 可以是正则表达式

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

在上面的例子中,“dear”先被剪掉,但随后 :separator 阻止了它。

5.5. truncate_bytes

truncate_bytes 方法返回其接收者的副本,该副本最多截断为 bytesize 字节

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

省略号可以使用 :omission 选项自定义

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

5.6. truncate_words

truncate_words 方法返回其接收者的副本,该副本在给定字数后截断

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

省略号可以使用 :omission 选项自定义

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

传递一个 :separator 以在自然断点处截断字符串

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

选项 :separator 可以是正则表达式

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

5.7. inquiry

inquiry 方法将字符串转换为 StringInquirer 对象,使相等性检查更美观。

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

5.8. starts_with?ends_with?

Active Support 定义了 String#start_with?String#end_with? 的第三人称别名

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

5.9. strip_heredoc

strip_heredoc 方法去除 heredoc 中的缩进。

例如

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

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

用户将看到与左边距对齐的使用消息。

技术上,它会在整个字符串中查找缩进最少的行,并删除相应数量的前导空白。

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! 方法就地执行缩进。

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

5.11.2. from(position)

from 方法返回从 position 位置开始的字符串的子字符串

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

5.11.3. to(position)

to 方法返回字符串中直到 position 位置的子字符串

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

5.11.4. first(limit = 1)

first 方法返回包含字符串前 limit 个字符的子字符串。

如果 n > 0,调用 str.first(n) 等同于 str.to(n-1),如果 n == 0,则返回空字符串。

5.11.5. last(limit = 1)

last 方法返回包含字符串最后 limit 个字符的子字符串。

如果 n > 0,调用 str.last(n) 等同于 str.from(-n),如果 n == 0,则返回空字符串。

5.12. 变体

5.12.1. pluralize

pluralize 方法返回其接收者的复数形式

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

如前面的例子所示,Active Support 知道一些不规则复数和不可数名词。内置规则可以在 config/initializers/inflections.rb 中扩展。此文件默认由 rails new 命令生成,并包含注释中的说明。

pluralize 也可以接受可选的 count 参数。如果 count == 1,则返回单数形式。对于 count 的任何其他值,将返回复数形式

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

Active Record 使用此方法计算与模型对应的默认表名

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

5.12.2. singularize

singularize 方法是 pluralize 的反向操作

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

关联使用此方法计算相应默认关联类的名称

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

5.12.3. camelize

camelize 方法将其接收者转换为驼峰式

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

通常,您可以将此方法视为将路径转换为 Ruby 类或模块名称的方法,其中斜杠分隔命名空间

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

例如,Action Pack 使用此方法加载提供特定会话存储的类

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

camelize 接受一个可选参数,可以是 :upper(默认)或 :lower。后者会将第一个字母转换为小写

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

这对于在遵循该约定的语言(例如 JavaScript)中计算方法名称可能很有用。

通常,您可以将 camelize 视为 underscore 的逆操作,尽管在某些情况下它不成立:"SSLError".underscore.camelize 返回 "SslError"。为了支持这种情况,Active Support 允许您在 config/initializers/inflections.rb 中指定缩写词

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

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

camelize 的别名为 camelcase

5.12.4. underscore

underscore 方法反向操作,从驼峰式转换为路径

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

还将 "::" 转换回 "/"

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

并理解以小写字母开头的字符串

"visualEffect".underscore # => "visual_effect"

underscore 不接受任何参数。

Rails 使用 underscore 获取控制器类的小写名称

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

例如,该值是您在 params[:controller] 中获得的值。

一般来说,您可以将 underscore 视为 camelize 的反义词,尽管在某些情况下并非如此。例如,"SSLError".underscore.camelize 返回 "SslError"

5.12.5. titleize

titleize 方法将接收者中的单词大写

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

titleize 的别名为 titlecase

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

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

5.12.8. deconstantize

给定一个带有限定常量引用表达式的字符串,deconstantize 删除最右边的部分,通常留下常量容器的名称

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

5.12.9. parameterize

parameterize 方法以可在漂亮的 URL 中使用的方式标准化其接收者。

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

为了保留字符串的大小写,将 preserve_case 参数设置为 true。默认情况下,preserve_case 设置为 false。

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

要使用自定义分隔符,请覆盖 separator 参数。

"John Smith".parameterize(separator: "_") # => "john_smith"
"Kurt Gödel".parameterize(separator: "_") # => "kurt_godel"

5.12.10. tableize

tableize 方法是 underscore 后面跟着 pluralize

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

通常,tableize 返回与给定模型对应的表名,适用于简单情况。Active Record 中的实际实现并非直接的 tableize,因为它还会对类名进行解模块化并检查一些可能影响返回字符串的选项。

5.12.11. classify

classify 方法是 tableize 的逆操作。它为您提供了与表名对应的类名

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

该方法理解限定表名

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

请注意,classify 返回一个字符串形式的类名。您可以对其调用 constantize(接下来解释)来获取实际的类对象。

5.12.12. constantize

constantize 方法解析其接收者中的常量引用表达式

"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

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

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"

如果您不想在 "_id" 中使用下划线,请传递一个 false 参数

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

关联使用此方法推断外键,例如 has_onehas_many 执行此操作

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

5.12.15. upcase_first

upcase_first 方法将接收者的第一个字母大写

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

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

5.13. 转换

5.13.1. to_dateto_timeto_datetime

方法 to_dateto_timeto_datetime 基本上是 Date._parse 的便捷包装

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

to_time 接收一个可选参数 :utc:local,以指示您希望时间所在的本地时区

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

默认为 :local

有关更多详细信息,请参阅 Date._parse 的文档。

对于空白接收者,它们三个都返回 nil

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

7. Numeric 的扩展

7.1. 字节

所有数字都响应这些方法

它们返回相应的字节量,使用 1024 的转换因子

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

单数形式被别名化,所以您可以这样说

1.megabyte # => 1048576

7.2. 时间

以下方法

启用时间声明和计算,例如 45.minutes + 2.hours + 4.weeks。它们的返回值也可以添加到或从 Time 对象中减去。

这些方法可以与 from_nowago 等结合使用,用于精确的日期计算。例如

# equivalent to Time.current.advance(days: 1)
1.day.from_now

# equivalent to Time.current.advance(weeks: 2)
2.weeks.from_now

# equivalent to Time.current.advance(days: 4, weeks: 5)
(4.days + 5.weeks).from_now

对于其他持续时间,请参阅 Integer 的时间扩展。

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"

8. Integer 的扩展

8.1. multiple_of?

multiple_of? 方法测试一个整数是否是参数的倍数

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

8.2. ordinal

ordinal 方法返回与接收整数对应的序数后缀字符串

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

8.3. ordinalize

ordinalize 方法返回与接收整数对应的序数字符串。相比之下,请注意 ordinal 方法返回后缀字符串。

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

8.4. 时间

以下方法

启用时间声明和计算,例如 4.months + 5.years。它们的返回值也可以添加到或从 Time 对象中减去。

这些方法可以与 from_nowago 等结合使用,用于精确的日期计算。例如

# equivalent to Time.current.advance(months: 1)
1.month.from_now

# equivalent to Time.current.advance(years: 2)
2.years.from_now

# equivalent to Time.current.advance(months: 4, years: 5)
(4.months + 5.years).from_now

有关其他持续时间,请参阅 Numeric 的时间扩展。

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 方法生成一个哈希,其中枚举元素的键是某个键。

它遍历集合,并将每个元素传递给一个块。该元素将以块返回的值作为键

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

键通常应该是唯一的。如果块对不同的元素返回相同的值,则不会为该键构建集合。最后一个元素将胜出。

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 ], … }

10.3. many?

many? 方法是 collection.size > 1 的简写

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

如果给定可选块,many? 只考虑那些返回 true 的元素

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

10.4. exclude?

谓词 exclude? 测试给定对象是否属于集合。它是内置 include? 的否定

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

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

10.6. excluding

excluding 方法返回一个可枚举对象的副本,其中移除了指定的元素

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

excluding 的别名为 without

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

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

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 方法返回一个排除指定元素的数组副本。这是 Enumerable#excluding 的优化,它出于性能原因使用 Array#- 而不是 Array#reject

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

方法 secondthirdfourthfifth 返回相应的元素,second_to_lastthird_to_last 也是如此(firstlast 是内置的)。由于普遍的社会智慧和积极的建设性,forty_two 也可用。

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

11.2. 提取

extract! 方法删除并返回块返回 true 的元素。如果没有给出块,则返回一个 Enumerator。

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

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 中删除。

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

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 的相应调用。

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,否则会引发异常。

默认情况下,根元素的名称是第一个项目的类的下划线和短划线复数形式,前提是其余元素属于该类型(通过 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>

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,则调用该方法。如果返回值为 nilKernel#Array 会尝试 to_a,但 Array.wrap 会立即返回一个以参数作为其唯一元素的数组。
  • 如果 to_ary 的返回值既不是 nil 也不是 Array 对象,Kernel#Array 会引发异常,而 Array.wrap 不会,它只会返回该值。
  • 它不会对参数调用 to_a;如果参数不响应 to_ary,它会返回一个以参数作为其唯一元素的数组。

最后一点对于某些可枚举对象尤其值得比较

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

还有一种使用 splat 运算符的相关惯用法

[*object]

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

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 不能用作填充值。

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 不能用作填充值。

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

在前面的例子中观察到,连续的分隔符会产生空数组。

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>

为此,该方法遍历键值对并构建依赖于 的节点。给定一个键值对 key, value

  • 如果 value 是一个哈希,则进行递归调用,其中 key 作为 :root

  • 如果 value 是一个数组,则进行递归调用,其中 key 作为 :rootkey 的单数形式作为 :children

  • 如果 value 是一个可调用对象,它必须接受一个或两个参数。根据参数数量,可调用对象会以 options 哈希作为第一个参数(其中 key 作为 :root),并以 key 的单数形式作为第二个参数被调用。其返回值成为一个新的节点。

  • 如果 value 响应 to_xml,则调用该方法,其中 key 作为 :root

  • 否则,将创建一个以 key 作为标签的节点,并以 value 的字符串表示形式作为文本节点。如果 valuenil,则添加一个设置为 "true" 的 "nil" 属性。除非 :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 等选项,它们会被转发给构建器。

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

merge 中,如果发生冲突,参数哈希中的键将获胜。您可以使用这种惯用法以紧凑的方式支持带有默认值的选项哈希

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

如果您喜欢这种替代表示法,Active Support 定义了 reverse_merge

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

以及一个就地执行合并的 bang 版本 reverse_merge!

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

请注意,reverse_merge! 可能会更改调用者中的哈希,这可能是一个好主意,也可能不是。

12.2.2. reverse_update

方法 reverse_updatereverse_merge! 的别名,如上所述。

注意 reverse_update 没有 bang。

12.2.3. deep_mergedeep_merge!

如您在前面的示例中看到的,如果两个哈希中都存在某个键,则参数中的哈希的值将获胜。

Active Support 定义了 Hash#deep_merge。在深度合并中,如果两个哈希中都存在某个键,并且它们的值本身也是哈希,那么它们的 合并 将成为结果哈希中的值

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

方法 deep_merge! 执行就地深度合并。

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

12.4. 处理键

12.4.1. except!

方法 except! 与内置的 except 方法相同,但会就地移除键,并返回 self

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

如果接收者响应 convert_key,则对每个参数调用该方法。这使得 except!(和 except)可以很好地处理具有不区分访问的哈希,例如

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

12.4.2. stringify_keysstringify_keys!

方法 stringify_keys 返回一个哈希,其中接收者中的键已字符串化。它通过向它们发送 to_s 来实现

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

如果发生键冲突,值将是最晚插入哈希的那个

{ "a" => 1, a: 2 }.stringify_keys
# The result will be
# => {"a"=>2}

此方法可能很有用,例如,可以轻松地同时接受符号和字符串作为选项。例如 ActionView::Helpers::FormHelper 定义了

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

第二行可以安全地访问 "type" 键,并允许用户传递 :type 或 "type"。

还有一个 bang 变体 stringify_keys!,它会就地将键字符串化。

除此之外,可以使用 deep_stringify_keysdeep_stringify_keys! 来字符串化给定哈希及其内部所有嵌套哈希中的所有键。结果示例如下

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

12.4.3. symbolize_keyssymbolize_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_textarea_tag(name, value = nil, options = {})
  options = options.symbolize_keys

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

第三行可以安全地访问 :input 键,并允许用户传递 :input 或 "input"。

还有一个 bang 变体 symbolize_keys!,它会就地将键符号化。

除此之外,可以使用 deep_symbolize_keysdeep_symbolize_keys! 来符号化给定哈希及其内部所有嵌套哈希中的所有键。结果示例如下

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

12.4.4. to_optionsto_options!

方法 to_optionsto_options! 分别是 symbolize_keyssymbolize_keys! 的别名。

12.4.5. assert_valid_keys

方法 assert_valid_keys 接收任意数量的参数,并检查接收者是否包含此列表之外的任何键。如果存在,则引发 ArgumentError

{ a: 1 }.assert_valid_keys(:a)  # passes
{ a: 1 }.assert_valid_keys("a") # ArgumentError

例如,Active Record 在构建关联时不允许使用未知选项。它通过 assert_valid_keys 实现此控制。

12.5. 处理值

12.5.1. deep_transform_valuesdeep_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"}}

还有一个 bang 变体 deep_transform_values!,它通过块操作破坏性地转换所有值。

12.6. 切片

方法 slice! 将哈希替换为仅包含给定键的哈希,并返回一个包含已移除键值对的哈希。

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

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

12.8. 不区分访问

方法 with_indifferent_access 从其接收者返回一个 ActiveSupport::HashWithIndifferentAccess

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

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 在一个地方使用了此方法,也在路由代码中。多行正则表达式不允许用于路由要求,此标志有助于强制执行该约束。

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

14.Range 的扩展

14.1. to_fs

Active Support 将 Range#to_fs 定义为 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 格式生成一个 BETWEEN SQL 子句。Active Record 在其对条件中范围值的支持中使用了该子句。

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

14.3. overlap?

方法 Range#overlap? 表示任意两个给定范围是否有非空交集

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

15.Date 的扩展

15.1. 计算

以下计算方法在1582年10月存在特殊情况,因为5-14日根本不存在。为简洁起见,本指南不记录它们在那些日期的行为,但可以说它们会做您期望的事情。也就是说,Date.new(1582, 10, 4).tomorrow 返回 Date.new(1582, 10, 15) 等。请查阅 Active Support 测试套件中的 test/core_ext/date_ext_test.rb 以了解预期行为。

15.1.1. Date.current

Active Support 定义 Date.current 为当前时区的今天。这类似于 Date.today,但如果定义了用户时区,它会遵循用户时区。它还定义了 Date.yesterdayDate.tomorrow,以及实例谓词 past?today?tomorrow?next_day?yesterday?prev_day?future?on_weekday?on_weekend?,它们都相对于 Date.current

在使用遵循用户时区的方法进行日期比较时,请务必使用 Date.current 而不是 Date.today。在某些情况下,用户时区可能比系统时区(Date.today 默认使用)未来,这意味着 Date.today 可能等于 Date.yesterday

15.1.2. 命名日期

15.1.2.1. beginning_of_week, end_of_week

方法 beginning_of_weekend_of_week 分别返回一周的开始日期和结束日期。默认情况下,一周从星期一开始,但可以通过传递参数、设置线程局部变量 Date.beginning_of_weekconfig.beginning_of_week 来更改。

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

beginning_of_weekat_beginning_of_week 的别名,end_of_weekat_end_of_week 的别名。

15.1.2.2. monday, sunday

方法 mondaysunday 分别返回上一个星期一和下一个星期日的日期。

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
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_weeklast_week 的别名。

当设置了 Date.beginning_of_weekconfig.beginning_of_week 时,next_weekprev_week 都能按预期工作。

15.1.2.4. beginning_of_month, end_of_month

方法 beginning_of_monthend_of_month 返回当月的开始日期和结束日期

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

beginning_of_monthat_beginning_of_month 的别名,end_of_monthat_end_of_month 的别名。

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_quarterend_of_quarter 返回接收者日历年季度的开始日期和结束日期

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

beginning_of_quarterat_beginning_of_quarter 的别名,end_of_quarterat_end_of_quarter 的别名。

15.1.2.6. beginning_of_year, end_of_year

方法 beginning_of_yearend_of_year 返回一年的开始日期和结束日期

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

beginning_of_yearat_beginning_of_year 的别名,end_of_yearat_end_of_year 的别名。

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) 的简写。

15.1.3.2. months_ago, months_since

方法 months_agomonths_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) 的简写。

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

请注意,在前面的示例中,增量可以是负数。

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

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

它们转换为对 sinceadvance 的调用。例如,在这里我们获得了日历改革中的正确跳转

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_dayat_beginning_of_daymidnightat_midnight 的别名。

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_hourat_beginning_of_hour 的别名。

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_minuteat_beginning_of_minute 的别名。

beginning_of_hourend_of_hourbeginning_of_minuteend_of_minute 是为 TimeDateTime 实现的,但 适用于 Date,因为在 Date 实例上请求小时或分钟的开始或结束没有意义。

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

16.DateTime 的扩展

DateTime 不知晓 DST 规则,因此其中一些方法在 DST 变更时存在边缘情况。例如,seconds_since_midnight 在这样的日子可能不会返回实际的数量。

16.1. 计算

DateTimeDate 的子类,因此通过加载 active_support/core_ext/date/calculations.rb,您将继承这些方法及其别名,只是它们总是返回 datetime。

以下方法被重新实现,因此您 不需要 为这些方法加载 active_support/core_ext/date/calculations.rb

另一方面,advancechange 也被定义并支持更多选项,它们将在下面记录。

以下方法仅在 active_support/core_ext/date_time/calculations.rb 中实现,因为它们仅在与 DateTime 实例一起使用时才有意义

16.1.1. 命名日期时间

16.1.1.1. DateTime.current

Active Support 定义 DateTime.current 类似于 Time.now.to_datetime,但如果定义了用户时区,它会遵循用户时区。实例谓词 past?future? 相对于 DateTime.current 定义。

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

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
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,您可能会在没有任何警告或错误的情况下最终到达一个不存在的时间点。

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

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

它们转换为对 sinceadvance 的调用。例如,在这里我们获得了日历改革中的正确跳转

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

17.Time 的扩展

17.1. 计算

它们是类似的。请参阅上面的文档,并注意以下差异

  • change 接受一个额外的 :usec 选项。
  • Time 了解 DST,因此您会得到正确的 DST 计算,如下所示
Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>

# In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST.
t = Time.local(2010, 3, 28, 1, 59, 59)
# => Sun Mar 28 01:59:59 +0100 2010
t.advance(seconds: 1)
# => Sun Mar 28 03:00:00 +0200 2010
  • 如果 sinceago 跳到一个无法用 Time 表示的时间,则会返回一个 DateTime 对象。

17.1.1. Time.current

Active Support 定义 Time.current 为当前时区的今天。这类似于 Time.now,但如果定义了用户时区,它会遵循用户时区。它还定义了实例谓词 past?today?tomorrow?next_day?yesterday?prev_day?future?,它们都相对于 Time.current

在使用遵循用户时区的方法进行时间比较时,请务必使用 Time.current 而不是 Time.now。在某些情况下,用户时区可能比系统时区(Time.now 默认使用)未来,这意味着 Time.now.to_date 可能等于 Date.yesterday

17.1.2. all_day, all_week, all_month, all_quarterall_year

方法 all_day 返回表示当前时间一整天的范围。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_day
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00

类似地,all_weekall_monthall_quarterall_year 都用于生成时间范围。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_week
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
now.all_week(:sunday)
# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
now.all_month
# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
now.all_quarter
# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
now.all_year
# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00

17.1.3. prev_day, next_day

prev_daynext_day 返回前一天或后一天的时间

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_day               # => 2010-05-07 00:00:00 +0900
t.next_day               # => 2010-05-09 00:00:00 +0900

17.1.4. prev_month, next_month

prev_monthnext_month 返回上个月或下个月中相同日期的时刻

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_month             # => 2010-04-08 00:00:00 +0900
t.next_month             # => 2010-06-08 00:00:00 +0900

如果该日期不存在,则返回对应月份的最后一天

Time.new(2000, 5, 31).prev_month # => 2000-04-30 00:00:00 +0900
Time.new(2000, 3, 31).prev_month # => 2000-02-29 00:00:00 +0900
Time.new(2000, 5, 31).next_month # => 2000-06-30 00:00:00 +0900
Time.new(2000, 1, 31).next_month # => 2000-02-29 00:00:00 +0900

17.1.5. prev_year, next_year

prev_yearnext_year 返回上一年或下一年中相同日期/月份的时刻

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_year              # => 2009-05-08 00:00:00 +0900
t.next_year              # => 2011-05-08 00:00:00 +0900

如果日期是闰年的2月29日,您将得到28日

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

17.1.6. prev_quarter, next_quarter

prev_quarternext_quarter 返回上一季度或下一季度中相同日期的日期

t = Time.local(2010, 5, 8) # => 2010-05-08 00:00:00 +0300
t.prev_quarter             # => 2010-02-08 00:00:00 +0200
t.next_quarter             # => 2010-08-08 00:00:00 +0300

如果该日期不存在,则返回对应月份的最后一天

Time.local(2000, 7, 31).prev_quarter  # => 2000-04-30 00:00:00 +0300
Time.local(2000, 5, 31).prev_quarter  # => 2000-02-29 00:00:00 +0200
Time.local(2000, 10, 31).prev_quarter # => 2000-07-31 00:00:00 +0300
Time.local(2000, 11, 31).next_quarter # => 2001-03-01 00:00:00 +0200

prev_quarterlast_quarter 的别名。

17.2. 时间构造函数

Active Support 将 Time.current 定义为 Time.zone.now(如果定义了用户时区),否则回退到 Time.now

Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
Time.current
# => Fri, 06 Aug 2010 17:11:58 CEST +02:00

DateTime 类似,谓词 past?future? 相对于 Time.current

如果待构造的时间超出了运行时平台 Time 支持的范围,则会丢弃微秒,并返回 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

它们转换为对 sinceadvance 的调用。例如,在这里我们获得了日历改革中的正确跳转

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 无法更改文件所有权或权限,此错误会被捕获并跳过,相信用户/文件系统会确保文件对需要它的进程可访问。

由于 atomic_write 执行了 chmod 操作,如果目标文件设置了 ACL,则该 ACL 将被重新计算/修改。

请注意,您不能使用 atomic_write 进行追加操作。

辅助文件写入标准临时文件目录中,但您可以将您选择的目录作为第二个参数传递。

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

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

21. 对 Pathname 的扩展

21.1. existence

existence 方法返回接收者(如果指定的文件存在),否则返回 nil。它对于以下惯用法很有用

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


回到顶部