Day4. 一起精通 Rails Array,处理更复杂的问题

接下来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

参考资料


<<:  日常要讲干话,但我不会

>>:  开启Python IDLE

[Day 25]从零开始学习 JS 的连续-30 Days---addEventListener 事件监听

addEventListener 事件监听 JavaScript 是一个事件驱动 (Event-dr...

用e-paper做普普风格影像显示

这次使用的元件是1.54inch_e-paper_b (黑白红显示) Pin Layout VCC ...

如何拥有一个好的网页设计

随着科技进步与智能设备的普及,越来越多人使用智能手机或者手提电脑搜寻网络上的资料。2019年Goog...

Day 27. B2E-密码加密

还记得第2天在做专案规划时,有提到一个目标「加密敏感资料实现资安管理」吗? 目前我们的密码还是一样...

D-14 渲染 ? view ? razor

view 的渲染 在这几天小光认识了dotnetcore的网页开发相关知识,从请求流水线、路由到过滤...