Leon's Blogging

Coding blogging for hackers.

Ruby on Rails - 好用的 Enumerable

| Comments

這次主要來介紹一些好用的 Enumerable 可以很方便的將需要的資料整合在一起

Map/Collect

對 block 每個值進行運算,並回傳成一個新的 array 處理 hash 時,也可以分開處理 key 和 value

mapcollect 其實是一樣的東西,主要是因為其他語言很多都是用 collect

1
2
3
4
5
6
7
array = [1,2,3]
array.map {|v| v * 2}
# => [2, 4, 6]

hash = {:name => "abc", :age => 18}
hash.map {|k, v| v }
# => ["abc", 18]

Enumerable
map/collect

Select

對物件,挑出指定欄位的值,並回傳 ActiveRecord::Relation
另一個則是,回傳 block 為 true 的值,成一個新的 array

1
2
User.all.select(:id)
=> #<ActiveRecord::Relation [#<User id: 1>, #<User id: 2>, #<User id: 3>,...]>

可以再搭配 map 變成一個 array

1
2
User.all.select(:id).map(&:id)
# => [1,2,3]

或是直接針對 array 去篩選

1
2
3
my_array = [1,2,3,4,5,6,7,8,100]
my_array.select{|item| item%2==0 }
# => [2,4,6,8,100]

hash

1
2
3
4
5
6
7
8
9
10
my_hash = {"Joe" => "male", "Jim" => "male", "Patty" => "female"}
my_hash.select{|name, gender| gender == "male" }
# => {"Joe" => "male", "Jim" => "male"}

也可以
my_hash.select{|k,v| ["Joe", "Jim"].include?(k)}

#改成 map 會變成,回傳 boolean值,並且回傳 array
my_hash.map{|name, gender| gender == "male" }
# => [true, true, false]

只回傳 ture 的選項

進階用法

1
2
3
4
5
6
7
8
9
#在Hash裡定義新的方法 沒有特定對象的通常可以放在lib資料夾
class Hash
  def keep(*args)
    select {|k,v| args.include?(k)}
  end
end

my_hash.keep("Joe", "Jim")
# => {"Joe" => "male", "Jim" => "male"}

Enumerable
select

Pluck

對物件,挑出指定欄位的值,並回傳一個新的 array 像是 mapselect 合在一起的指令

pluck 每次都會發 query 跟 map 不太一樣,因此如果是已經 preload 的 data 最好用 map 會比較好

Approach - map

1
2
3
4
5
User.pluck(:id)
#=> [1, 2, 3]

puts Benchmark.measure {User.pluck(:id)}
#0.000000   0.000000   0.000000 (  0.000857)
1
2
3
4
5
User.all.map{|a| a.id}
#=> [1, 2, 3]

puts Benchmark.measure {User.all.map{|a| a.id}}
# 0.000000   0.020000   0.020000 (  0.026401)
1
2
3
4
5
User.select(:id)
=> #<ActiveRecord::Relation [#<User id: 1>, #<User id: 2>, #<User id: 3>]>

puts Benchmark.measure {User.select(:id).to_a}
#0.000000   0.000000   0.000000 (  0.001549)

顯然效能上還是 pluck 最快

map 會將所有欄位找出來,再根據 block 的值,回傳新的 array

pluckselect 則是只將需要的欄位選出來,但 select 回傳的是 ActiveRecord::Relation 必須再透過 map 轉成 array

Enumerable
pluck

參考文件:
Rails Pluck vs Select and Map/Collect
Getting to Know Pluck and Select
Pluck vs. map and select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Person.pluck(:id)
# SELECT people.id FROM people
# => [1, 2, 3]

Person.pluck(:id, :name)
# SELECT people.id, people.name FROM people
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]

Person.pluck('DISTINCT role')
# SELECT DISTINCT role FROM people
# => ['admin', 'member', 'guest']

Person.where(age: 21).limit(5).pluck(:id)
# SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
# => [2, 3]

Person.pluck('DATEDIFF(updated_at, created_at)')
# SELECT DATEDIFF(updated_at, created_at) FROM people
# => ['0', '27761', '173']

reject

回傳 block 為 false 的值,成一個新的 array

1
2
3
4
5
6
7
8
9
10
11
12
# Remove even numbers
(1..30).reject { |n| n % 2 == 0 }
# => [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]

# Remove years dividable with 4 (this is *not* the full leap years rule)
(1950..2000).reject { |y| y % 4 != 0 }
# => [1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000]

# Remove users with karma below arithmetic mean
total = users.inject(0) { |total, user| total += user.karma }
mean = total / users.size
good_users = users.reject { |u| u.karma < mean }

Enumerable
reject

inject

inject 方法可以先給予初始值(數字,hash,array 都可以),之後給予指定的元素,不斷的迭代。

1
2
3
4
(5..10).inject(1) {|init, n| init * n }
# => 151200
(5..10).inject(1, :*)
#=> 151200
1
2
3
4
(5..10).inject {|sum, n| sum * n }
# => 45
(5..10).inject(:+)
#=> 45

也可以拿來做比較。

1
2
3
4
%w{ cat sheep bear }.inject do |memo,word|
   memo.length > word.length ? memo : word
end
# => "sheep"

如果給予 inject 的參數為一個空區塊,那麼 inject 會將結果整理成 Hash。

1
2
3
4
5
User.all.inject({}) do |hash, user|
  hash[user.name] = user.id
  hash # 需要回傳運算結果
end
# => {"A"=>1, "B"=>2, "C"=>3}

但要注意的是,由於每跑一次,都會取用最後的回傳值,當做這次的初始值,因此最後必須再加個 hash ,否則會出錯。

也可以直接給予有值的 hash 再繼續加上後面的值,取代掉 inject({}) 中的空 hash

也可改用 reduce 跟 inject 一模一樣 Is inject the same thing as reduce in ruby?

額外說明

也可以用 map 方式,湊成上面的值。

1
2
3
4
5
Hash[User.all.map {|user| [user.name, user.id ]}]
# => {"A"=>1, "B"=>2, "C"=>3}

User.all.map {|user| [user.name, user.id ]}.to_h
# => {"A"=>1, "B"=>2, "C"=>3}

Enumerable
inject

each_with_object

跟 inject 非常類似,,主要差別在於你不用回傳運算結果,還有參數是顛倒過來的。

1
2
3
User.all.each_with_object({}) do | user, hash |
  hash[user.name] = user.id
end

複雜用法

1
2
3
4
5
6
7
8
9
10
11
12
people = [{ math:10, english:20, chinese:30},
          { math:90, english:80, chinese:90}
          ]
#=> [{:math=>10, :english=>20, :chinese=>30}, {:math=>90, :english=>80, :chinese=>90}]

all = Hash.new(0).merge(label: "總分", student: people)
#=> {:label=>"總分", :student=>[{:math=>10, :english=>20, :chinese=>30}, {:math=>90, :english=>80, :chinese=>90}]}

people.each_with_object(all) do |number, hash|
  number.each {|key, value| hash[key] += value }
end
#=> {:label=>"總分", :student=>[{:math=>10, :english=>20, :chinese=>30}, {:math=>90, :english=>80, :chinese=>90}], :math=>100, :english=>100, :chinese=>120}

Note that you can’t use immutable objects like numbers, true or false as the memo. You would think the following returns 120, but since the memo is never changed, it does not.

1
(1..5).each_with_object(1) { |value, memo| memo *= value } # => 1

Enumerable
each_with_object

merge

可以將另一個 hash 合併在一起

1
2
3
4
5
6
7
8
9
10
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge(h2)
#=> {"a"=>100, "b"=>254, "c"=>300}

h1.merge(h2){|key, oldval, newval| newval - oldval}
#=> {"a"=>100, "b"=>54,  "c"=>300}

h1
#=> {"a"=>100, "b"=>200}
1
Hash.new(0).merge(name: "abc", phone: 09xxxxx)

Enumerable
merge

each_with_index

用來加上索引。

1
2
3
4
5
6
7
8
hash = Hash.new
%w(cat dog wombat).each_with_index {|item, index|
  hash[item] = index
}
#=> ["cat", "dog", "wombat"]

hash
#=> {"cat"=>0, "dog"=>1, "wombat"=>2}

Ruby’s inject/reduce and each_with_object

也可以用來將複數的的 position 印出來。

1
2
3
4
["Cool", "chicken!", "beans!", "beef!"].each_with_index do |item, index|
  print "#{item} " if index%2==0
end
Cool beans!  # => ["Cool", "chicken!", "beans!", "beef!"]

Enumerable
each_with_index

sum

可以算出集合的加總

1
2
payments.sum { |p| p.price * p.tax_rate }
payments.sum(&:price)

數字,字串,陣列都可以,其實就是用 + 的方法

1
2
3
[5, 15, 10].sum # => 30
['foo', 'bar'].sum # => "foobar"
[[1, 2], [3, 1, 5]].sum #=> [1, 2, 3, 1, 5]

Enumerable
sum

group_by

可以依照指定的欄位分組出來。

1
2
3
4
5
6
7
8
9
10
11
latest_transcripts.group_by(&:day).each do |day, transcripts|
  p "#{day} -> #{transcripts.map(&:class).join(', ')}"
end

# "2006-03-01 -> Transcript"
# "2006-02-28 -> Transcript"
# "2006-02-27 -> Transcript, Transcript"
# "2006-02-26 -> Transcript, Transcript"
# "2006-02-25 -> Transcript"
# "2006-02-24 -> Transcript, Transcript"
# "2006-02-23 -> Transcript"
1
2
3
names = ["James", "Bob", "Joe", "Mark", "Jim"]
names.group_by{|name| name.length}
# => {5=>["James"], 3=>["Bob", "Joe", "Jim"], 4=>["Mark"]}

Enumerable
group_by

grep

根據指定的條件塞選

1
2
3
names = ["James", "Bob", "Joe", "Mark", "Jim"]
names.grep(/J/)
#=> ["James", "Joe", "Jim"]

index_by

index_by可以指定欄位做為鍵值整理成Hash。

1
2
User.index_by(&:phone)
# => {'0912xxxxxx' => <User ...>, '0919xxxxxx' => <User ...>, ...}

鍵值通常必須是唯一的,若不是唯一的話,會以最後出現的元素做為判斷值。

Enumerable
index_by

all?

所有 條件符合,就回傳true

1
2
3
4
5
6
7
%w{ant bear cat}.all? {|word| word.length >= 3}
#=> true
%w{ant bear cat}.all? {|word| word.length >= 4}
#=> false
[ nil, true, 99 ].all?
#=> false
#=> 只要有一個是 nil 或 false 就是 false

Enumerable
all?

any?

只要有 任何 條件符合,就回傳true

1
2
3
4
5
6
7
%w{ant bear cat}.any? {|word| word.length >= 3}
#=> true
%w{ant bear cat}.any? {|word| word.length >= 4}
#=> true
[ nil, true, 99 ].any?
#=> true
#=> 只要有一個不是 nil 和 false 就是 true

Enumerable
any?

可參考之前的 .nil? .empty? .blank? .present? 傻傻分不清楚?

chunk

可以將數列分區塊,去判斷。

1
2
3
4
5
6
7
8
9
10
11
12
[3,1,4,1,5,9,2,6,5,3,5].chunk{|n| n.even?}.each {|even, ary|
  p [even, ary]
}

#=> [false, [3, 1]]
#   [true, [4]]
#   [false, [1, 5, 9]]
#   [true, [2, 6]]
#   [false, [5, 3, 5]]

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n.even? }.to_a
#=> [[false, [3, 1]], [true, [4]], [false, [1, 5, 9]], [true, [2, 6]], [false, [5, 3, 5]]]

Enumerable
chunk

partition

將元素劃分為兩個

1
2
(1..6).partition { |v| v.even? }
#=> [[2, 4, 6], [1, 3, 5]]

Enumerable
partition

slice

取出相對應的值

Array

1
2
3
4
5
6
7
8
a = ['a','b','c']

a.slice(2)
#=> 'c'
a.slice!(2)
#=> 'c'
a
#=> ['a', 'b']

Hash

1
2
3
4
5
6
7
8
a = {a:1, b:2, c:3}

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

Array 和 Hash 在 slice! 有些不一樣的地方

Array: 是將指定的值拿掉。
Hash : 是將指定的值留下來。

zip

每個元素和對應的元素合在一起

1
2
3
4
5
6
7
8
9
10
11
a = [ 4, 5, 6 ]
b = [ 7, 8, 9 ]

[1, 2, 3].zip(a, b)
#=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

[1, 2].zip(a, b)
#=> [[1, 4, 7], [2, 5, 8]]

a.zip([1, 2], [8])
#=> [[4, 1, 8], [5, 2, nil], [6, nil, nil]]

Enumerable
partition

each_slice

1
2
3
4
5
(1..10).each_slice(3) { |a| p a }
#[1, 2, 3]
#[4, 5, 6]
#[7, 8, 9]
#[10]

flat_map

  • 傾向使用 flat_map 而不是 map + flatten 的組合。
  • 這並不適用於深度大於2 的數組,舉個例子,如果 users.first.songs == ['a', ['b', 'c']] ,則使用 map + flatten 的組合,而不是使用 flat_map
  • flat_map 將數組變平坦一個層級,而 flatten 會將整個數組變平坦。
1
[[1,2],[3,4]].flat_map {|i| i }   #=> [1, 2, 3, 4]

reverse_each

使用 reverse_each 代替 reverse.eachreverse_each 不會分配一個新數組並且這是好事。

1
(1..3).reverse_each {|v| p v }

&:

1
User.all.map(&:name)

&: 代表代入一個Proc (&:name) = {|name| user.name} 的概念XD。

partition

回傳兩個 array,第一個符合條件,第二個是條件以外

1
2
3
4
5
6
7
8
(1..6).partition { |v| v.even? }  #=> [[2, 4, 6], [1, 3, 5]]

a, b = (1..6).partition { |v| v.even? }
#=> [[2, 4, 6], [1, 3, 5]]
a
#=> [2, 4, 6]
b
#=> [1, 3, 5]

partition

detect

找出符合條件的第一個

1
2
(1..10).detect   { |i| i % 5 == 0 and i % 7 == 0 }   #=> nil
(1..100).find    { |i| i % 5 == 0 and i % 7 == 0 }   #=> 35

detect

Benchmark

上面其實很多都很類似,主要差異的話就是速度吧 所以可以用以下的方式來測試每種執行出來的速度。

Benchmark benchmark-ips

建議

1
2
3
4
5
map    勝於 collect
find   勝於 detect
select 勝於 find_all
reduce 勝於 inject
size   勝於 length

自建 methods

mash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Enumerable
  def mash(&block)
    self.inject({}) do |output, item|
      binding.pry
      key, value = block_given? ? yield(item) : item
      output.merge(key => value)
    end
  end
end

["functional", "programming", "rules"].map { |s| [s, s.length] }.mash
# {"functional"=>10, "programming"=>11, "rules"=>5}

["functional", "programming", "rules"].mash { |s| [s, s.length] }
# {"functional"=>10, "programming"=>11, "rules"=>5}

官方文件:
Enumerable
map/collect
reject
inject
select
pluck
reduce
each_with_object
each_with_index
merge
sum
group_by
index_by
many?
all?
any?
chunk
each_slice

參考文件:
Rails Pluck vs Select and Map/Collect
Getting to Know Pluck and Select
Pluck vs. map and select
Ruby Explained: Map, Select, and Other Enumerable Methods
each_with_object vs inject
ActiveSupport - 工具函式庫
Ruby 用 inject 和 each_with_object 來組 hash
What does map(&:name) mean in Ruby?

Comments