接下來Day4-6
的用法,都是由Ruby
的Enumerable
。Enumerable
是Ruby
相當強大的庫,專門處理集合資料的遞迴處理。
今天我們要介紹的是Array
,Array
最基本的用法為map
, each
,map
, each
與javascript
的map
, forEach
概念相同
each
回傳的值仍是原值map
回傳的值為處理過後的值[1, 2, 3].each {|_| _ + 1} #=> [1, 2, 3]
[1, 2, 3].map {|_| _ + 1} #=> [2, 3, 4]
那如果懂了each
, map
以後,其他的我們繼續看下去。
Ruby
可以宣告重複值的陣列
Array.new(10)
#=> [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
Array.new(10, 'a')
#=> ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a"]
此外,還可以使用下列的表示法宣告
[nil]*10 #=> [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
%w[a]*10 #=> ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a"]
除了%Q
以外,與陣列相關的方法有%w
, %i
。%w
, %i
是Ruby
相當好用的方法。用%w/%i
包覆的方法可以不用雙引號
# 可以使用中括號,也可以使用小括號
%i[a b c d e] #=> [:a, :b, :c, :d, :e]
# 可以使用中括號,也可以使用小括號
%w[a b c d e] #=> ["a", "b", "c", "d", "e"]
compact 可以濾除空值。
[1, 2, nil, ""].compact #=> [1, 2, ""]
map
搭配next
會回傳nil
,所以我們可以搭配 compact
做變化
(1..10).map do |_|
next if _.even?
_*100
end .compact
#=> [100, 300, 500, 700, 900]
今天文章的最尾端會講到reducer
,看完reducer
再回頭看compact
,就可以想像得到如果自己用reducer
來刻畫 compact
⭐️ 上面的compact
,可以用select(&:itself)
代替
[1, 2, nil, ""].compact #=> [1, 2, ""]
[1, 2, nil, ""].select(&:itself) #=> [1, 2, ""]
:itself
回傳的值為物件本身,而select
會篩選 nil
,因此select(&:itself)
會將空值去除
1.itself #=> 1
[1, 2, nil, ""].map(&:itself) #=> [1, 2, nil, ""]
⭐️ 若我們要取得第一個非空值,可以使用find(&:itself)
[nil, nil, 1, 2, nil, ""].find(&:itself) #=> 1
當我們要在陣列取得特定值,除了寫成array[2]
取單值以外,還可以使用逗點或者範圍的方式取多值。
%w[q w e r t][1, 3]
#=> ["w", "e", "r"]
%w[q w e r t][1..3]
#=> ["w", "e", "r"]
%w[q w e r t][1...3]
#=> ["w", "e"]
splat
意即展開 array
,可以作為淺拷貝用
include_models = [:shipments, :return_order, order: :customer]
current_brand.sub_orders.includes(*include_models)
# 等同於
current_brand.sub_orders.includes(:shipments, :return_order, order: :customer)
Rails 的 ActiveRecord_Relation
類別,也可以參與Array
的splat
IgImage.order(id: :asc).limit(10).class
#=> IgImage::ActiveRecord_Relation
[*IgImage.order(id: :asc).limit(10), 1,2,3]
#=> [#<IgImage:0x00007ff78b9e65a8...>, #<IgImage:0x00007ff78b9e6468...>, 1, 2, 3]
Ruby
對於Array
的解構子
x, y = [1, 2, 3]
x # => 1
y # => 2
first, *rest = [1, 2, 3]
first # => 1
rest # => [2, 3]
以下為列出關於 Array
的邏輯操作
x = [1, 1, 2, 4]
y = [1, 2, 2, 2]
# intersection
x & y # => [1, 2]
# union
x | y # => [1, 2, 4]
# difference
x - y # => [4]
# intersection
[ 1, 1, 3, 5 ] & [ 3, 2, 1 ] #=> [ 1, 3 ]
[ 'a', 'b', 'b', 'z' ] & [ 'a', 'b', 'c' ] #=> [ 'a', 'b' ]
# difference
[ 1, 1, 2, 2, 3, 3, 4, 5 ] - [ 1, 2, 4 ] #=> [ 3, 3, 5 ]
使用 Array
將陣列湊成堆,可以做轉換 Hash
用
students = ["Steve", "John", "Kim", "Gloria", "Sam"]
#=> ["Steve", "John", "Kim", "Gloria", "Sam"]
ages = [14, 12, 2, 23, 4]
#=> [14, 12, 2, 23, 4]
students.zip(ages)
#=> [["Steve", 14], ["John", 12], ["Kim", 2], ["Gloria", 23], ["Sam", 4]]
flatten
用來攤平 array
,若不給數字會全癱。
num = [1, [2, 3], [4, [5, 6]]]
num.flatten #=> [1, 2, 3, 4, 5, 6]
num.flatten(1) #=> [1, 2, 3, 4, [5, 6]]
Array
可以做搜尋用
⭐️ 找開頭為 70
的值
response = {}
response[:routes] = [
{:opcode=>"54", :remark=>"收取快件", :occurred_at=>"2021-07-12 16:32:59"},
{:opcode=>"30", :remark=>"寄到驛站了", :occurred_at=>"2021-07-12 18:57:46" },
{:opcode=>"31", :remark=>"到達台湾桃园機場】", :occurred_at=>"2021-07-12 20:42:02"},
{:opcode=>"204", :remark=>"正在派送途中", :occurred_at=>"2021-07-13 08:34:09"},
{:opcode=>"80", :remark=>"您的快件代簽收", :occurred_at=>"2021-07-13 11:21:47"},
{:opcode=>"70-55", :remark=>"您的快件代簽收", :occurred_at=>"2021-07-13 11:21:47"},
{:opcode=>"8000", :remark=>"結單", :occurred_at=>"2021-07-13 11:21:48"}
]
# 失敗時間
failed_at = response[:routes].find {|r| r[:opcode].start_with? '70'}.try(:[], :occurred_at)
#=> "2021-07-13 11:21:47"
⭐️ 找尋不是nil
的第一個元素
group_list.find { |x| !x["list"].blank? }
group_list.find(&:itself)
group_list.find{|x|!x.nil?}
group_list.compact.first
Array
可以做篩選用
⭐️ select
為正向的 filter
[1, 2, 3, 4, 5].select(&:even?) # => [2, 4]
⭐️ reject
為負向的 filter
[1, 2, 3, 4, 5].reject { |v| v.even? } #=> [1, 3, 5]
⭐️ 可以處理 Array of Strings
grep_v
fruit = ["apple", "orange", "banana"]
#===== a 開頭
fruit.grep(/^a/)
#=> ["apple"]
#===== e 結尾
fruit.grep(/ap$/)
#=> ["apple", "orange"]
#===== 含有dis字眼的值
Order.column_names.grep(/dis/)
#=> ["district",
# "discount_detail",
# "distribution_bonus",
# "vip_discount",
# "diamond_discount_price",
# "diamond_discount",
# "customer_vip_disc_pct"]
objects = ["a", "b", "c", 1, 2, 3, nil]
objects.grep(String)
# ["a", "b", "c"]
objects.grep(Integer)
# [1, 2, 3]
objects.grep(NilClass)
# [nil]
objects.grep_v(NilClass)
# ["a", "b", "c", 1, 2, 3]
[1, :a, 2, :b].grep(Symbol) # => [:a, :b]
[1, :a, 2, :b].grep(Numeric) { |v| v + 1 } # => [2, 3]
uniq
可以將重複的值濾除掉。
[1, 2, 3, 1, 1, 2].uniq
# => [1, 2, 3]
(1..10).uniq { |v| v % 5 }
# => [1, 2, 3, 4, 5]
# 除後的結果為 [1, 2, 3, 4, 5, 1, 2, 3, 4, 5],再進行 uniq
對集合做詢問的動作
all?
,any?
為Ruby
常見的兩種方法
# all?
[true, true, true].all? #=> true
[true, false, true].all? #=> false
# any?
[true, false, true].any? #=> true
[1, 2, 3].any? {|_| _.even?} #=> true
[1, 2, 3].all? {|_| _.even?} #=> false
⭐️ 該筆子訂單的母訂單底下的所有子訂單,是否狀態為 完成
或 退貨
# sub_order 為某張子訂單,為instance (object)
sub_order.order.sub_orders.pluck(:status).all?{ |_| _.in? [Status::DONE.to_s, Status::RETURNED.to_s] }
詢問動作還有 #none?
, #include?
等用法,#include?
在Day3已經介紹過了,而#none?
就為字面上的意思,即為全部都沒有回傳true
排序也是Enum很實用的方法
Order.all.sort_by(&:price) #=> 訂單價格由小排大
以上這些方法在算最佳解、最便宜單價等地方可以用,為Ruby
相當實用的方法之一
#===== #max
[1, 2, 3].max
# => 3
[1, 2, 3].max { |a, b| b <=> a }
# => 1
[1, 2, 3].max(2)
# => [3, 2]
#===== #min
[1, 2, 3].min
# => 1
#===== #minmax
[1, 2, 3, 4, 5].minmax
# => [1, 5]
[1, 2, 3].count #=> 6
使用tally
,可以省卻非常多的程式碼。
%w(a b b c c c d).group_by(&:itself).map { |k, vs| [k, vs.size] }.to_h
#=> {"a"=>1, "b"=>2, "c"=>3, "d"=>1}
%w(a b b c c c d).each_with_object(Hash.new(0)) { |key, hash| hash[key] += 1 }
#=> {"a"=>1, "b"=>2, "c"=>3, "d"=>1}
%w(a b b c c c d).each_with_object(Hash.new(100)) { |key, hash| hash[key] += 1 }
#=> {"a"=>101, "b"=>102, "c"=>103, "d"=>101}
#======= 用tally可以簡單做到
%w(a b b c c c d).tally
#=> {"a"=>1, "b"=>2, "c"=>3, "d"=>1}
至於each_with_object
的用法,接下來就會開始介紹
將判斷結果為true
放一邊,false
放在另外一邊
[1, 2, 3, 4].partition(&:even?)
# => [[2, 4], [1, 3]]
⭐️ 可以應用在分主/附圖、主信用卡/其他信用卡,是蠻好用的功能
將同一群的放在一邊。以下又使用:itself
做為例子,是為了讓讀者更明白:itself
不只可以當作回傳本身物件使用這麼沒用,還可以與Enumable
, ActiveRecord
做為搭配。
%w(a b b c c c d).group_by(&:itself)
#=> {"a"=>["a"], "b"=>["b", "b"], "c"=>["c", "c", "c"], "d"=>["d"]}
#===== group_by 第一個字元
%w(apple banana bear cat car cap dude).group_by{ |_| _.first }
#=> {"a"=>["apple"], "b"=>["banana", "bear"], "c"=>["cat", "car", "cap"], "d"=>["dude"]}
Javascript 的reduce
是對陣列處理中最難理解的,同樣的,ruby 的inject
, reduce
也不好理解。
# 數字使用 reduce
(5..10).reduce(:+) #=> 45
# 數字使用 inject
(5..10).inject { |sum, n| sum + n } #=> 45
# 從1開始乘
(1..5).reduce(1, :*) #=> 120
# 從2開始乘
(1..5).reduce(2, :*) #=> 240
# 從1開始乘
[2, 3, 4].inject(1) {|product, i| product*i } # => 24
# 從10開始乘
[2, 3, 4].inject(10) {|product, i| product*i } # => 240
# inject 可以使用 block
(1..5).inject(1) { |product, n| product * n } #=> 120
# 找最長的文字 #=> "sheep"
longest = %w{ cat sheep bear }.inject do |memo, word|
memo.length > word.length ? memo : word
end
漢漢老師列了很多reduce
, inject
的用法,有沒有發現用法很像嗎? 其實reduce,
inject`是一樣的東西,並沒有任何差別。
請問讀者看到這裡有被耍的感覺嗎? 漢漢老師只是想要讓讀者印象比較深刻。
reduce
, inject
兩個詞本來就比較不直覺,尤其是reduce
這個詞,根本跟「集合」單詞打不上關係。
reduce
的詞是源自於 functional programming
的 reducer
,維基百科定義的 reducer
是:
A reducer is the component in a pipeline that reduces the pipe size from a larger to a smaller bore (inner diameter).
我們搭配下列的例子來更了解reduce/inject
的用法
# 遞減處理
(1..5).inject { |sum, n| sum + n } #=> 45
(1..5).inject(0) { |sum, n| sum + n } #=> 45
假設我的身體沒有任何藥物0
,第一次打1劑、第二次打2劑、...、第五次打5劑,共打了45劑,這就是inject
的意思,而reducer
的意思為逐步的縮小陣列的處理範圍,動詞稱為reduce
(1..5).inject(0) { |sum, n| sum + n } #=> 45
ruby
有很多方法是用reduce
實作的,下列為常見使用reduce`實作的用法
[1, 2, 3].sum # => 6
[1, 2, 3].min # => 1
[1, 2, 3].max # => 2
[1, 2, 3].count # => 3
each_with_object
的用法與前面提到的reduce/inject
用法很像,接著我們來比較 each_with_object
, inject
的用法
#======= inject =======#
%w{foo bar blah}.inject({}) do |hash, string|
hash[string] = "something"
hash # 需要回傳運算結果
end
#=> {"foo"=>"something" "bar"=>"something" "blah"=>"something"}
#======= each_with_object =======#
%w{foo bar blah}.each_with_object({}){|string, hash| hash[string] = "something"}
#=> {"foo"=>"something", "bar"=>"something", "blah"=>"something"}
#======= inject =======#
MainOrder.payment_types
.inject({}) {|h, d| h[d.last.to_s] = I18n.t("main_orders.payment_type.#{d.first}"); h }
#=> {"1"=>"信用卡", "2"=>"小品點", "3"=>"ATM", "4"=>"PayEasy", "5"=>"超商付款", "6"=>"7-11付款", "7"=>"銀聯卡", "8"=>"Eslite Pay"}
#======= each_with_object =======#
MainOrder.payment_types
.each_with_object({}) {|h, d| h[d.last.to_s] = I18n.t("main_orders.payment_type.#{d.first}")}
#=> {"1"=>"信用卡", "2"=>"小品點", "3"=>"ATM", "4"=>"PayEasy", "5"=>"超商付款", "6"=>"7-11付款", "7"=>"銀聯卡", "8"=>"Eslite Pay"}
each_with_object
會順道回傳hash
值,不用像hash
特地回傳值
注意:inject
, each_with_object
的block
帶入值
h
, hash
)雖然inject
, each_with_object
萬用,但兩者還是有用法上的適應性
Hash
, Array
⭐️ zip
與 each_with_object
的組合技
students = ["Steve", "John", "Kim", "Gloria", "Sam"]
ages = [14, 12, 2, 23, 4]
students.zip(ages).each_with_object({}) { |pair, hsh| hsh[pair[0]] = pair[1] }
#=> {"Steve"=>14, "John"=>12, "Kim"=>2, "Gloria"=>23, "Sam"=>4}
集合引入索引值
也很簡單,只要用each.with_index
, map.with_index
即可。
[11, 22, 31].each_with_index { |val,index| puts "index: #{index} for #{val}" if val < 30}
# index: 0 for 11
# index: 1 for 22
#=> [11, 22, 31]
[11,22,31].each.with_index { |val,index| puts "index: #{index} for #{val}" if val < 30}
# index: 0 for 11
# index: 1 for 22
#=> [11, 22, 31]
下列為 map
搭配 index
的用法
[11,22,31].map.with_index { |val, index| [val, index]}
#=> [[11, 0], [22, 1], [31, 2]]
我們可以使用下列方法來range
和#map
。
(1..rand(10)).map(&:itself)
#
#=> [1]
#=> [1, 2]
#=> ...
#=> ...
#=> [1, 2, 3, 4, 5, 6, 7, 8]
取出隨機值,並將該值從原本的陣列拿出來。
variant_ids = Variant.limit(10).pluck(:id)
#=> [1, 2, 131, 132, 133, 134, 135, 136, 137, 138]
variant_id = variant_ids.sample #=> 134
variant_ids = variant_ids.reject {|id| id == variant_id }
#=> [1, 2, 131, 132, 133, 135, 136, 137, 138]
variant_id = variant_ids.sample #=> 2
variant_ids = variant_ids.reject {|id| id == variant_id }
#=> [1, 131, 132, 133, 135, 136, 137, 138]
今天介紹了除了map
, each
以外,很多不一樣的用法。這些方法都很好用,但其中幾個概念更為重要。
:itself
不只可以用來回傳本身Enumable
方法的原型each_with_object({})
, each_with_object([])
的用法明天會開始講Hash