這次主要來介紹一些好用的 Enumerable
可以很方便的將需要的資料整合在一起
Map/Collect
對 block 每個值進行運算,並回傳成一個新的 array
處理 hash
時,也可以分開處理 key 和 value
map
和 collect
其實是一樣的東西,主要是因為其他語言很多都是用 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
像是 map
和 select
合在一起的指令
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
pluck
和 select
則是只將需要的欄位選出來,但 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 : 09 xxxxx )
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.each
。 reverse_each
不會分配一個新數組並且這是好事。
1
( 1 . . 3 ) . reverse_each { | v | p v }
&:
&:
代表代入一個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?