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 它
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 Ruby on Rails 应用程序中的 Active Support
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
2.2 presence
presence
方法如果 present?
则返回其接收者,否则返回 nil
。它对于像这样的习惯用法很有用
host = config[:host].presence || "localhost"
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
快得多。仅当你知道硬编码列表足以满足你的用例时才使用它。
2.4 deep_dup
deep_dup
方法返回给定对象的深拷贝。通常,当你复制包含其他对象的某个对象时,Ruby 不会复制它们,因此它会创建一个对象的浅拷贝。例如,如果你有一个包含字符串的数组,它看起来像这样
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
时才调用它上的某个方法时,实现它的最简单方法是使用条件语句,这会导致不必要的混乱。另一种选择是使用 try
。try
类似于 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 有些类像 Date
或 Time
一样,遵循此契约。
2.8 to_param
Rails 中的所有对象都响应 to_param
方法,该方法旨在返回一个表示它们在查询字符串中或作为 URL 片段的值。
默认情况下,to_param
只是调用 to_s
7.to_param # => "7"
to_param
的返回值 **不应** 编码。
"Tom & Jerry".to_param # => "Tom & Jerry"
Rails 中的几个类重写了此方法。
例如,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]
的值。
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
,但签名不同。如果没有传递参数,则调用会生成一系列排序的键值对,对它的值调用 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
实现。这是因为一些类,比如 Hash
和 Process::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_warnings
和 enable_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_reader
、attr_internal_writer
和 attr_internal_accessor
。它们的行为类似于它们的 Ruby 内置 attr_*
对应项,不同的是它们以不太可能发生冲突的方式命名底层实例变量。
宏 attr_internal
是 attr_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_reader
、mattr_writer
和 mattr_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
可以防止此类潜在的警告,如果需要,会在定义之前删除现有的方法。
您还可以使用 silence_redefinition_of_method
,如果您需要自己定义替换方法(例如,因为您正在使用 delegate
)。
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_reader
为 false
时,实例谓词会返回 NoMethodError
,就像读取器方法一样。
如果您不希望实例谓词,请传递 instance_predicate: false
,它将不会被定义。
4.1.2 cattr_reader
、cattr_writer
和 cattr_accessor
宏 cattr_reader
、cattr_writer
和 cattr_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
那样覆盖它(见上文)。例如,给定
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
# 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!”,则输出将不是格式良好的,因为必须将“&”转义为“&”。更重要的是,根据应用程序的不同,这可能是一个很大的安全漏洞,因为用户可以通过设置手工制作的评论标题来注入恶意 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 + "<" # => "<"
安全参数会直接追加。
"".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 转换
根据经验,除了上面解释的串联之外,任何可能更改字符串的方法都会为您提供不安全的字符串。这些是 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!
。
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: "…")
# => "Oh dear! Oh …"
特别要注意,截断会考虑省略字符串的长度。
传递 :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: "…")
# => "Oh dear! Oh dear!…"
传递 :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
获取控制器类的 lowercase 名称。
# 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_one
和 has_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_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
。
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_now
、ago
等一起使用,以进行精确的日期计算。例如:
# 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_now
、ago
等一起使用,以进行精确的日期计算。例如:
# 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
返回一个不包含指定元素的 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
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
,则调用此方法。Kernel#Array
继续尝试to_a
,如果返回值是nil
,但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
作为: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
等选项,它们将转发给生成器。
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!
可能会更改调用者中的哈希表,这可能是一个好主意,也可能不是。
12.2.2 reverse_update
方法reverse_update
是reverse_merge!
的别名,如上所述。
注意,reverse_update
没有感叹号。
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!
在原地执行深度合并。
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_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
# 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"。
还有一个感叹号变体 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}}
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_textarea_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}}
12.4.4 to_options
和 to_options!
方法 to_options
和 to_options!
分别是 symbolize_keys
和 symbolize_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_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!
,它使用块操作破坏性地转换所有值。
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 = { 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.yesterday
和 Date.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_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
。
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
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
。
当 Date.beginning_of_week
或 config.beginning_of_week
设置时,next_week
和 prev_week
都能按预期工作。
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
。
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
。
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
。
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_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)
的简写。
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
它们转换为对 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
。
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
。
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
实例上请求小时或分钟的开始或结束没有意义。
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 变化发生时,某些方法会出现边缘情况。例如,当 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 命名的日期时间
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
此方法首先使用上面文档中提到的 `Date#advance` 方法,通过传递 `:years`、`:months`、`:weeks` 和 `:days` 参数来计算目标日期。之后,使用 `since` 方法和要前进的秒数来调整时间。此顺序很重要,不同的顺序在某些边缘情况下会导致不同的日期时间。`Date#advance` 中的示例适用,我们可以扩展它来展示与时间位相关的顺序相关性。
如果我们首先移动日期位(它们也有一个相对的处理顺序,如前所述),然后移动时间位,例如,我们会得到以下计算结果
d = DateTime.new(2010, 2, 28, 23, 59, 59)
# => Sun, 28 Feb 2010 23:59:59 +0000
d.advance(months: 1, seconds: 1)
# => Mon, 29 Mar 2010 00:00:00 +0000
但如果我们以相反的顺序计算它们,结果将不同
d.advance(seconds: 1).advance(months: 1)
# => Thu, 01 Apr 2010 00:00:00 +0000
由于 `DateTime` 不支持夏令时,因此您可能会遇到时间点不存在的情况,而不会有任何警告或错误提示。
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
它们转换为对 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
支持夏令时,因此您会得到与以下示例中一样的正确的夏令时计算结果
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
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_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
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
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
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
如果日期是闰年中的 2 月 29 日,则您会获得 2 月 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_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` 的别名。
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` 支持的范围,则会丢弃 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` 无法更改文件所有权或权限,此错误会被捕获并跳过,信任用户/文件系统以确保该文件对需要它的进程是可访问的。
由于 `atomic_write` 执行了 chmod 操作,因此如果目标文件设置了 ACL,该 ACL 将被重新计算/修改。
请注意,您无法使用 `atomic_write` 追加。
辅助文件写入标准的临时文件目录,但您可以将您选择的目录作为第二个参数传递。
19 `NameError` 的扩展
Active Support 向 `NameError` 添加了 `missing_name?`,它测试异常是否因为作为参数传递的名称而引发。
名称可以作为符号或字符串给出。符号将与裸常量名进行比较,字符串将与完全限定的常量名进行比较。
符号可以表示完全限定的常量名,例如 `:"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 向 `LoadError` 添加了 `is_missing?`。
给定一个路径名,`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