iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
Modern Web

初階 Rails 工程師的養成系列 第 4

Day4. 一起精通 Rails Array,處理更複雜的問題

  • 分享至 

  • xImage
  •  

接下來Day4-6的用法,都是由RubyEnumerableEnumerableRuby相當強大的庫,專門處理集合資料的遞迴處理。

今天我們要介紹的是ArrayArray最基本的用法為map, eachmap, eachjavascriptmap, 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"]

%w %i

除了%Q以外,與陣列相關的方法有%w, %i%w, %iRuby相當好用的方法。用%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

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

itself

⭐️ 上面的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

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類別,也可以參與Arraysplat

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]

Destructuring

Ruby 對於Array 的解構子

x, y = [1, 2, 3]
x # => 1
y # => 2
first, *rest = [1, 2, 3]
first # => 1
rest # => [2, 3]

Array operation

以下為列出關於 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 ]

zip

使用 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

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 可以做搜尋用

#find

⭐️ 找開頭為 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

Filter 集合

Array 可以做篩選用

#select?

⭐️ select 為正向的 filter

[1, 2, 3, 4, 5].select(&:even?)  # => [2, 4]

#reject?

⭐️ reject 為負向的 filter

[1, 2, 3, 4, 5].reject { |v| v.even? } #=> [1, 3, 5]

#grep

⭐️ 可以處理 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

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?

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很實用的方法

#sort_by

Order.all.sort_by(&:price)   #=> 訂單價格由小排大

比較集合

#max #min #minmax

以上這些方法在算最佳解、最便宜單價等地方可以用,為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]

Counting 集合

#count

[1, 2, 3].count #=> 6

#tally

使用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的用法,接下來就會開始介紹

Grouping 集合

#partition

將判斷結果為true放一邊,false放在另外一邊

[1, 2, 3, 4].partition(&:even?)
# => [[2, 4], [1, 3]]

⭐️ 可以應用在分主/附圖、主信用卡/其他信用卡,是蠻好用的功能

#group_by

將同一群的放在一邊。以下又使用: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"]}

Reducer

#inject #reduce

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 programmingreducer,維基百科定義的 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

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_objectblock帶入值

  • reduce/inject: 累積結果在前面(以上方為例為h, hash
  • each_with_object: 累積結果在後面

雖然inject, each_with_object萬用,但兩者還是有用法上的適應性

  • reduce/inject: 適合回傳單一物件,如數字和字串
  • each_with_object: 適合回傳複雜物件,如Hash, Array

⭐️ zipeach_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

我們可以使用下列方法來range#map

(1..rand(10)).map(&:itself)
#
#=> [1]
#=> [1, 2]
#=> ...
#=> ...
#=> [1, 2, 3, 4, 5, 6, 7, 8] 

sample

取出隨機值,並將該值從原本的陣列拿出來。

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 不只可以用來回傳本身
  • reducer ➡️ 許多Enumable方法的原型
  • each_with_object({}), each_with_object([]) 的用法

明天會開始講Hash

參考資料


上一篇
Day3. Ruby的數字、字串,以及 ===
下一篇
Day5. 活用Hash,掌握資料處理的訣竅
系列文
初階 Rails 工程師的養成34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言