ActiveSupport - 工具函式库

I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages — Alan Kay, creator of Smalltalk

Active Support 是 Rails 里的工具函式库,它也扩充了一些 Ruby 标准函式库。除了被用在 Rails 核心程式中,你也可以在你的程式中使用。本章介绍其中的一小部分较为常用的功能。

blank? 和 present?

Rails中下面几种情况被定义是blank:

  • nil或是false
  • 只由空白组成的字串
  • 空阵列或是空Hash
  • 任何物件当使用empty?方法呼叫时回应为true

Ruby1.9中的字串支援辨识Unicode字符,因此某些字符像是U2029(分隔线也会被视为是空白。

以数字来说,0或是0.0并不是blank

举例来说在ActionDispatch::Session::AbstractStore中就使用了blank?方法来确定session key是否存在:

  1. def ensure_session_key!
  2. if @key.blank?
  3. raise ArgumentError, 'A key is required...'
  4. end
  5. end

present?方法就是blank?方法的相反,判断是否存在,因此present?方法与!blank?方法两者表达的意思是一样的。

try

try是一个相当实用的功能,当我们去呼叫一个物件的方法,而该物件当时却是nil的时候,Rails会抛出method_missing的例外,最常见的例子像是我们想判断某些动作只有管理员可以进行操作,因此我们通常会这样写:

  1. if current_user.is_admin?
  2. # do something
  3. end

但这样的写法当使用者其实是未登入时我们的current_user便会回传nil,而再去呼叫is_admin?方法时便会发生错误抛出例外,try方法便是运用在这样的情况,刚刚的例子我们可以改写成

  1. if current_user.try(:is_admin?)
  2. # do something
  3. end

这样子当使用者并未登入的时候会直接回传nil而不会再去呼叫后面的is_admin?方法

to_param

Rails中所有的物件都支援to_param方法,这个方法会帮我们将物件转为可用的数值并以字串表示:

  1. 7.to_param # => "7" to_param 方法默认会去呼叫物件的 to_s 方法

Rails中某些类别去复写了to_param方法,像是niltruefalse等在呼叫to_param时会回传自己本身,而阵列会将所有的元素印出来并加上”/”:

  1. [0, true, String].to_param # => "0/true/String"

值得注意的是,在RailsRouting系统中,我们常使用/:id来表示该物件的id,事实上是Rails改写了ActiveRecord::Base中的to_param方法,让它回传数据库的 id,当然我们也可以自己去改写他:

  1. class User
  2. def to_param
  3. "#{id}-#{name.parameterize}"
  4. end
  5. end

那么当我们呼叫userpath(@user)的时候,_Rails就会转换成 “#{id}-#{name.parameterize}”,这技巧常使用在改写URL的表现方式

to_query

toquery会帮我们去呼叫物件的to_param方法,并且帮我们整理成查询的格式并输出,例如我们去改写_User Modelto_param方法:

  1. class User
  2. def to_param
  3. "#{id}-#{name.parameterize}"
  4. end
  5. end
  6. current_user.to_query('user') # => user=357-john-smith

toquery会将输出的符号都以逸出程式码(_escape)取代,无论是键或是值,因此更方便去处理:

  1. account.to_query('company[name]')
  2. # => "company%5Bname%5D=Johnson+%26+Johnson"

当呼叫阵列的to_query方法时会呼叫阵列中所有元素的to_query方法,并且使用"[]"做为键值,并在每个元素与元素间插入"&"做为区隔:

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

呼叫Hashtoquery方法时,当没有给予_query的字串时默认会以Hash本身的键值做为query字串输出(to_query(key)):

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

换句话说你也可以自己指定做为query的字串,这个字串会变为Hash本身键值的namespace:

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

扩充 Class

Class Attributes

class_attribute

class_attribute这个方法可以宣告一个或多个类别变量,且此类别变量是可以被继承的类别所复写的:

  1. class A
  2. class_attribute :x
  3. end
  4. class B < A; end
  5. class C < B; end
  6. A.x = :a
  7. B.x # => :a
  8. C.x # => :a
  9. B.x = :b
  10. A.x # => :a
  11. C.x # => :b
  12. C.x = :c
  13. A.x # => :a
  14. B.x # => :b

也可以在实例变量的层级被读取或复写:

  1. A.x = 1
  2. a1 = A.new
  3. a2 = A.new
  4. a2.x = 2
  5. a1.x # => 1, comes from A
  6. a2.x # => 2, overridden in a2

class_attribute同时也帮你定义了查询的方法,你可以在变量名称后面加上问号来看此变量是否已经被定义,以上面的例子来说就是x?,结果会回传truefalse

cattr_readercattr_writercattr_accessor

cattrreadercattr_writercattr_accessor这三个方法就像是attr*的类别变量版本,透过这三个方法可以建立相对应的类别变量及存取方法:

  1. class MysqlAdapter < AbstractAdapter
  2. # Generates class methods to access @@emulate_booleans.
  3. cattr_accessor :emulate_booleans
  4. self.emulate_booleans = true
  5. end

同时也会帮我们建立实例变量的方法,让我们可以在实例变量层级来存取:

  1. module ActionView
  2. class Base
  3. cattr_accessor :field_error_proc
  4. @@field_error_proc = Proc.new{ ... }
  5. end
  6. end

如此我们便可以在ActionView中存取field_error_proc

更多关于class_attribute的部份可以参考深入Rails3: ActiveSupport 的 class_attribute

扩充 String

安全输出

当输出HTML格式的资料时需要格外注意,例如当你文章的标题存成Flanagan & Matz rules!时,在没有格式化的情况下&会被逸出码所取代成&amp;,另一方面是安全性上的问题,因为使用者可能就会在字段中写入攻击性的script造成安全性问题,因此在处理字串输出时我们都会对输出进行处理:

我们可以使用htmlsafe?方法来判断字串是否是_html安全格式,一般字串默认是false:

  1. "".html_safe? # => false

你可以透过html_safe方法来指定字串:

  1. s = "".html_safe
  2. s.html_safe? # => true

你必须注意htmlsafe这个方法并不会帮你处理_html中的tag,这方法只是单纯的指定该字串是否为htmlsafe,你必须自己去处理_tag的部份:

  1. s = "<script>...</script>".html_safe
  2. s.html_safe? # => true
  3. s # => "<script>...</script>"

当你使用像是concat<<或是+的方式将一个不是html_safe的字串与一个html_safe的字串作结合时,会输出成一个html_safe的字串,但将原先不是html_safe的字串内容作逸出码的处理:

  1. "".html_safe + "<" # => "&lt;"
  2. "".html_safe + "<".html_safe # => "<" 如果是 html_safe 的内容则不会作逸出码的处理

但在Rails3之后版本的View会自动帮你把不安全的部份作逸出处理,因此你大可直接在View中使用像是<%= @post.title %>来输出,但由于这样会直接把HTML的部份都去除,如果你希望保持HTML的格式那么你可以使用raw这个helper来帮你输出:

  1. <%= raw @post.content %>

基于上述安全性的前提,任何可能改变原有字串的方法都会将原先的字串变为unsafe的状态,像是downcasegsubstripchompunderscore等,但是复制的方法像是dup或是clone并不会影响。

truncated

truncate方法会将字串截断为指定的长度:

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

你可以使用omission参数将撷取后的字串的后面取代为指定的文字:

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

你必须注意truncate后的字串不是html_safe的,因此在你没有使用raw来作处理的时候会将html格式直接输出:

  1. "<p>Oh dear! Oh dear! I shall be late!</p>".truncate(20, :omission => "(blah)")
  2. => "<p>Oh dear! Oh(blah)"

为了避免撷取的部分会将单字直接从中撷取,你可以用:separator参数来取代被撷取的单字部分:

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

:separator无法使用正规表示法

inquiry

inquiry方法会将字串转型为StringInquirer物件,可以让我们像用一般方法的方式来比对字串是否符合,最常见的例子就是判断Rails正在使用的版本:

  1. Rails.env.production? # 等同于 Rails.env == "production"

因此你可以用inquiry将一般字串转型后来达到一样的效果:

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

Key-based Interpolation

Ruby1.9以后的版本支援使用%符号做为字串中的变量键值:

  1. "I say %{foo}" % {:foo => "wadus"} # => "I say wadus"
  2. "I say %{woo}" % {:foo => "wadus"} # => KeyError

字串转换相关

to_dateto_timeto_datetime三个方法是与转换时间相关的方法,可以帮我们将字串转型为时间物件:

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

to_time另外还接受:utc或是:local的参数用来指定时区,默认为:utc:

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

其他实用的方法

pluralize方法可以帮我们将名词字串转为复数的名词:

  1. "table".pluralize # => "tables"
  2. "ruby".pluralize # => "rubies"
  3. "equipment".pluralize # => "equipment"

singularize方法则是可以帮我们转为单数:

  1. "tables".singularize # => "table"
  2. "rubies".singularize # => "ruby"
  3. "equipment".singularize # => "equipment"

camelize可以帮我们将字串转为驼峰式的字串:

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

Rails中也会将路径中”/”符号转为ClassModule中的命名空间符号::

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

underscore则是将原先驼峰式的字串转为路径式的字串:

  1. "Product".underscore # => "product"
  2. "AdminUser".underscore # => "admin_user"
  3. "Backoffice::Session".underscore # => "backoffice/session"

titleize方法可以将字串标题化,将单字的开头皆转为大写:

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

dasherize可以将字串中的底线转为横线:

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

demodulize可以将整串的namespace去除仅留下最后的Class name或是Module name:

  1. "Backoffice::UsersController".demodulize # => "UsersController"
  2. "Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"

deconstantize则是相反的作用,将上层的部分全部找出来:

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

必须注意的是这是处理字串,因此若直接仅给予Class name或是Module name是无法找出上层参照的

  1. "Product".deconstantize # => ""

parameterize可以将字串转为适合url的方式:

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

tableize除了会将单数名词转为复数之外,还会将驼峰式的名词改为底线:

  1. "InvoiceLine".tableize # => "invoice_lines"

tableize的作用其实在于帮助你找出Model的资料表名称

classify则是tableize的相反,能够帮你从资料表的名称转为Model:

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

humanize可以帮你将Model的属性转为较容易阅读的形式:

  1. "name".humanize # => "Name"
  2. "author_id".humanize # => "Author"
  3. "comments_count".humanize # => "Comments count"

扩充 Enumerable

group_by

group_by可以将列举依照指定的字段分组出来,例如将记录依照日期排序出来:

  1. latest_transcripts.group_by(&:day).each do |day, transcripts|
  2. p "#{day} -> #{transcripts.map(&:class).join(', ')}"
  3. end
  4. "2006-03-01 -> Transcript"
  5. "2006-02-28 -> Transcript"
  6. "2006-02-27 -> Transcript, Transcript"
  7. "2006-02-26 -> Transcript, Transcript"
  8. "2006-02-25 -> Transcript"
  9. "2006-02-24 -> Transcript, Transcript"
  10. "2006-02-23 -> Transcript"

sum

sum可以算出集合的加总:

  1. [1, 2, 3].sum # => 6
  2. (1..100).sum # => 5050

sum的作用其实就是帮你将元素彼此用+方法连结起来:

  1. [[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4]
  2. %w(foo bar baz).sum # => "foobarbaz"
  3. {:a => 1, :b => 2, :c => 3}.sum # => [:b, 2, :c, 3, :a, 1]

对空集合呼叫sum默认回传0,但你也可以改写:

  1. [].sum # => 0
  2. [].sum(1) # => 1

如果给予一个block,那么会迭代执行集合中的元素运算后再将结果加总起来:

  1. (1..5).sum {|n| n * 2 } # => 30
  2. [2, 4, 6, 8, 10].sum # => 30

空集合的元素也可以这样被改写:

  1. [].sum(1) {|n| n**3} # => 1

each_with_object

inject方法可以为集合中的元素迭代的给予指定的元素并运算:

  1. [2, 3, 4].inject(1) {|product, i| product*i } # => 24

如果给予inject的参数为一个空区块,那么inject会将结果整理成Hash,但需注意在运算的结尾必须回传运算结果:

  1. %w{foo bar blah}.inject({}) do |hash, string|
  2. hash[string] = "something"
  3. hash # 需要回传运算结果
  4. end
  5. => {"foo"=>"something" "bar"=>"something" "blah"=>"something"}

each_with_object这个方法也可以达到一样的效果,差别在于你不用回传运算结果:

  1. %w{foo bar blah}.each_with_object({}){|string, hash| hash[string] = "something"}
  2. => {"foo"=>"something", "bar"=>"something", "blah"=>"something"}

index_by

indexby可以帮我们将集合元素以指定的字段做为键值整理成_Hash:

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

键值通常必须是唯一的,若不是唯一的话将会以最后出现的元素做为判断值。

many?

many?是可个好用的方法可以帮助我们快速的判断集合的数量是否大于1:

  1. <% if pages.many? %>
  2. <%= pagination_links %>
  3. <% end %>

如果对many?传入区块运算时,many?仅会回传运算结果是true的结果:

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

扩充 Array

随机挑选

  1. shape_type = ["Circle", "Square", "Triangle"].sample
  2. # => Square, for example
  3. shape_types = ["Circle", "Square", "Triangle"].sample(2)
  4. # => ["Triangle", "Circle"], for example

增加元素

prepend会将新元素插入在整个阵列的最前方(index为0的位置)

  1. %w(a b c d).prepend('e') # => %w(e a b c d)
  2. [].prepend(10) # => [10]

append会将元素插入在阵列的最后方:

  1. %w(a b c d).append('e') # => %w(a b c d e)
  2. [].append([1,2]) # => [[1,2]]

options_extractions!

Rails中我们常常会看到一个方法可以传入不定数量的参数,例如:

  1. my_method :arg1
  2. my_method :arg1, :arg2, :argN
  3. my_method :arg1, :foo => 1, :bar => 2

一个方法能够接收不定数量的多个参数主要仰赖的是extractoptions!这个方法会帮我们将传入的集合参数展开,若没有传入参数时这个方法便会回传空_Hash

  1. def my_method(*args)
  2. options = args.extract_options!
  3. puts "参数: #{args.inspect}"
  4. puts "选项: #{options.inspect}"
  5. end
  6. my_method(1, 2)
  7. # 参数: [1, 2]
  8. # 选项: {}
  9. my_method(1, 2, :a => :b)
  10. # 参数: [1, 2]
  11. # 选项: {:a=>:b}

因此extract_options!这个方法可以很方便的帮你展开一个阵列中选项元素,最主要的作用就是展开传入方法的参数。

Grouping

in_groups_of方法可以将阵列依照我们指定的数量做分组:

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

如果给予一个block的话可以将分组的元素做yield:

  1. <% sample.in_groups_of(3) do |a, b, c| %>
  2. <tr>
  3. <td><%=h a %></td>
  4. <td><%=h b %></td>
  5. <td><%=h c %></td>
  6. </tr>
  7. <% end %>

在元素数量不够分组的时候默认在不足的元素部分补nil,像第一个例子中最后一个元素是nil,你也可以在呼叫in_groups_of方法的同时传入第二个参数做为不足元素的填充值:

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

你也可以传入false指定当元素不足的时候就不要以nil做为填充值,也由于这层关系你无法指定false来做为一个填充值:

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

in_groups_of这个方法最常拿来使用在当你页面每一列想要有n个元素来呈现的时候,例如假设我们有一个待办清单的网站,我们希望页面上每一列可以有四笔清单,我们可以这样写:

  1. <% @tasks.in_groups_of(4) do |tasks| %>
  2. <ul>
  3. <% tasks.each do |task| %>
  4. <li><%= task.name %></li>
  5. <% end %>
  6. </ul>
  7. <% end %>

split这个方法会依照你给的条件来判断阵列内的元素做分割:

  1. [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] 如果阵列内元素是3的话做分割
  2. (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] 如果阵内元素是3的倍数就做分割

扩充 Hash

Merging 合并

Ruby本身有Hash#merge方法来合并两个Hash

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

reverse_merge与reverse_merge!

在合并Hash时可能会遇到有一样的key造成需要判断以哪个key值做为依据的情况:

  1. a = {:a => 1, :b => 2}
  2. b = {:a => 3, :c => 4}
  3. a.merge(b) # Ruby 本身的 merge 不会改变原先呼叫的 hash,并且以后面的 hash 为优先产生一个新的 hash
  4. => {:a=>3, :b=>2, :c=>4}
  5. a # => {:a=>1, :b=>2}
  6. b # => {:a=>3, :c=>4}
  7. a.reverse_merge(b) # reverse_merge 不会改变原先呼叫的 hash,以前面呼叫的 hash 为优先产生一个新的 hash
  8. => {:a=>1, :c=>4, :b=>2}
  9. a # => {:a=>1, :b=>2}
  10. b # => {:a=>3, :c=>4}
  11. a.reverse_merge!(b) # reverse_merge! 会以前面呼叫的 hash 优先并直接改变原先呼叫的 hash,不会产生新的 hash
  12. => {:a=>1, :b=>2, :c=>4}
  13. a # => {:a=>1, :b=>2, :c=>4}
  14. b # {:a=>3, :c=>4}

因此reversemerge这个方法常用在指定_hash的默认值:

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

deep_merge与deep_merge!

在两个hash的键值相同,而值也是个hash的情况下,我们可以使用deepmerge将两个_hash组合:

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

deep_merge!的版本则是会直接更改呼叫的hash

Key 键值

except与except!

except方法可以将指定的键值从hash中移除:

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

except通常用在我们更新资料时对一些不想被更改的资料字段做保护的动作:

  1. params[:account] = params[:account].except(:plan_id) unless admin?
  2. @account.update(params[:account])

except!会直接更改原本呼叫的hash而不是产生一个新的hash

stringify_keys 与 stringify_keys!

stringifykeys可以将_hash中的键值改为字串:

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

如果hash中有冲突发生,则以后者优先:

  1. {"a" => 1, :a => 2}.stringify_keys
  2. => {"a"=>2}

这方法方便我们将传入的hash做一致性的处理,而不用去考虑使用者传入的hash是用symbol或是字串

stringifykeys!的版本会直接更改呼叫的_hash

symbolize_keys与symbolize_keys!

symbolizekeys则是会把_hash中的键值都呼叫tosym方法将之改为_symbol:

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

如果hash中有冲突发生,以后面的优先:

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

symbolizekeys!版本会直接更改呼叫的_hash

to_options与to_options!

to_optionsto_options!方法作用与symbolize_keys方法是一样的

assert_valid_keys

assertvalid_keys是用来指定_hash键值的白名单,没有在白名单里的键值出现在hash中都会抛出例外:

  1. {:a => 1}.assert_valid_keys(:a) # => {:a=>1}
  2. {:a => 1}.assert_valid_keys("a") # ArgumentError: Unknown key: a

分割 Hash

slice方法可以帮我们从hash中切出指定的值:

  1. {:a => 1, :b => 2, :c => 3}.slice(:a, :c)
  2. # => {:c => 3, :a => 1}
  3. {:a => 1, :b => 2, :c => 3}.slice(:b, :X)
  4. # => {:b => 2} # 不存在的值会被忽略

这方法也常用来做为检验hash的白名单使用,将核可的值从hash中抽出

slice!的版本会直接更改呼叫的hash

抽取

extract!方法会将hash中指定的值取出变为一个新的hash,并将原先的hash中减去我们抽取出来的部分:

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

扩充 DateTime

DateTime本身已经写好很多实用的方法可以方便我们计算时间:

  1. yesterday
  2. tomorrow
  3. beginning_of_week (at_beginning_of_week)
  4. end_of_week (at_end_of_week)
  5. monday
  6. sunday
  7. weeks_ago
  8. prev_week
  9. next_week
  10. months_ago
  11. months_since
  12. beginning_of_month (at_beginning_of_month)
  13. end_of_month (at_end_of_month)
  14. prev_month
  15. next_month
  16. beginning_of_quarter (at_beginning_of_quarter)
  17. end_of_quarter (at_end_of_quarter)
  18. beginning_of_year (at_beginning_of_year)
  19. end_of_year (at_end_of_year)
  20. years_ago
  21. years_since
  22. prev_year
  23. next_year

DateTime并不支援日光节约时间

DateTime.current类似于 Time.now.to_datetime,但他的结果会依使用者本身的时区而定,如果在时区有设定的情况下,还会有些其他好用的方法像是DateTime.yesterdayDateTime.tomorrow,也可以使用像是past?future?来与DateTime.current做判断

seconds_since_midnight会回传从午夜00:00:00到指定时间所经过的秒数:

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

utc可以把时间转为UTC格式

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

utc?可以判断是否为UTC格式

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

advance是个非常好用的方法,当我们想要找出相对于一个时间加加减减后的另一个时间非常好用:

  1. d = DateTime.current
  2. # => Thu, 05 Aug 2010 11:33:31 +0000
  3. d.advance(:years => 1, :months => 1, :days => 1, :hours => 1, :minutes => 1, :seconds => 1)
  4. # => Tue, 06 Sep 2011 12:34:32 +0000

要注意的是你如果呼叫多次advance去做计算,其结果可能与呼叫一次是有差异的,你可以参考下面的例子:

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

change可以传入参数给指定的时间将它改为我们想要的时间:

  1. now = DateTime.current
  2. # => Tue, 08 Jun 2010 01:56:22 +0000
  3. now.change(:year => 2011, :offset => Rational(-6, 24))
  4. # => Wed, 08 Jun 2011 01:56:22 -0600 将年份跟时区指定为我们传入的参数

如果你传入的参数只有hour的时候并且为0的时候,分钟及秒数都会被设为0:

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

同样的,如果传入的参数只有min并且值为0的时候,秒数就会被设为0:

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

DateTime也可以方便得用时间间隔来做加减:

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

扩充 Time

Time继承从DateTime来很多好用的方法:

  1. past?
  2. today?
  3. future?
  4. yesterday
  5. tomorrow
  6. seconds_since_midnight
  7. change
  8. advance
  9. ago
  10. since (in)
  11. beginning_of_day (midnight, at_midnight, at_beginning_of_day)
  12. end_of_day
  13. beginning_of_week (at_beginning_of_week)
  14. end_of_week (at_end_of_week)
  15. monday
  16. sunday
  17. weeks_ago
  18. prev_week
  19. next_week
  20. months_ago
  21. months_since
  22. beginning_of_month (at_beginning_of_month)
  23. end_of_month (at_end_of_month)
  24. prev_month
  25. next_month
  26. beginning_of_quarter (at_beginning_of_quarter)
  27. end_of_quarter (at_end_of_quarter)
  28. beginning_of_year (at_beginning_of_year)
  29. end_of_year (at_end_of_year)
  30. years_ago
  31. years_since
  32. prev_year
  33. next_year

Timechange方法接受一个额外的参数:usec

Time不同于DateTime,是能正确计算出时区间的差异,DateTime是不支援时光节约时间的

  1. Time.zone_default
  2. # => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
  3. # In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST.
  4. t = Time.local_time(2010, 3, 28, 1, 59, 59)
  5. # => Sun Mar 28 01:59:59 +0100 2010
  6. t.advance(:seconds => 1)
  7. # => Sun Mar 28 03:00:00 +0200 2010

使用since或是ago时,如果得到的时间无法用Time来呈现时,会自动转型为DateTime

Time.current

Time.current类似于Time.now会回传现在时间,唯一的差别在于Time.current会依照使用者的时区来回传,在有定义时区的情况下你也可以使用像是Time.yesterdayTime.tomorrow的方法,以及像是past?today?future?等用来与Time.current比较的方法

也因为如此,当我们在做时间的处理时尽量使用像是Time.current而少用Time.now,不然很有可能会出现时区问题所造成的错误计算

all_day、all_week、all_month、all_quarter 与 all_year

上面所列的all_*方法会回传与指定时间相较的一个区间:

  1. now = Time.current
  2. # => Mon, 09 Aug 2010 23:20:05 UTC +00:00
  3. now.all_day
  4. # => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00
  5. now.all_week
  6. # => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
  7. now.all_month
  8. # => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
  9. now.all_quarter
  10. # => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
  11. now.all_year
  12. # => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00

Time Constructors

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

localtime这个_class method可以帮助我们建立基于使用者时区设定的时间物件:

Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
Time.local_time(2010, 8, 15)
# => Sun Aug 15 00:00:00 +0200 2010

utctime可以回传_UTC格式的时间物件:

Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
Time.utc_time(2010, 8, 15)
# => Sun Aug 15 00:00:00 UTC 2010

local_timeutc_time这两个方法都接受七个时间参数:yearmonthdayhourminsec以及usecyear是必填参数,monthday默认为1,而其他参数默认为0

时间也可以使用简单的加减:

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

Concerns

假设我们现在有一个Module AModule B有相依关系:

Module A
  self.included(base)
    include B
    # 当 Module A 被 include 后便 include Module B
  end
end

今天当我们想要include Module A时,由于Module AModule B的相依关系,我们必须同时将两个Moduleinclude进来:

class Something
  include A, B
end

但我们其实没有必要我想要includeModule之间的相依关系,如此便有了ActiveSupport::Concern的意义,就是让我们只需要include我们想要使用的Module,其他的相依关系我们不需要去考虑他,你所需要作的只是在Module Aextend ActiveSupport::Concern

Module A
  extend ActiveSupport::Concern
  included do
    include B
    # 当 Module A 被 include 后便 include Module B
  end
end

如此一来我们只需要include A就可以搞定了!

更多内容请请参考:深入Rails3: ActiveSupport::Concern

Benchmarks

benchmark方法可以用来测试template的执行时间并记录起来:

<% benchmark "Process data files" do %>
  <%= expensive_files_operation %>
<% end %>

这样将会在你的log记录中增加一笔像是“Process data files (345.2ms)”的纪录,你便可用来测量并改善你的程式码。

你也可以设定log的层级,默认是info

<% benchmark "Low-level files", :level => :debug do %>
  <%= lowlevel_files_operation %>
<% end %>

Configurable

Configurable这个模组是Rails本身用来作为AbstractController::Base的设定使用,我们可以借用这个功能来为我们的类别增加设定选项:

class Employee
  include ActiveSupport::Configurable
end

employee = Employee.new
employee.config.sex = male
employee.config.permission = :normal
employee.config.salary = 22000

config_accessor方法可以帮助我们将这些设定转为方法:

class Employee
  include ActiveSupport::Configurable
  config_accessor  :sex, :permission, :salary

  # 现在你可以使用 employee.sex, employee.permission, employee.salary 来取用这些设定
end

上面的范例让每个Employee的实例变量都能有自己的设定,但其实我们也可以有类别层级的设定让每个实例变量都能共享设定:

# 设定类别层级的设定
Employee.config.duty_hour = 8

# 新增一个employee
employee = Employee.new
employee.config.duty_hour # => 8

# 由实例变量更改设定
employee.config.duty_hour = 5

# 会更改类别层级设定
Employee.config.duty_hour # => 5

更多线上资源