Day5. 活用Hash,掌握资料处理的诀窍

Day5. Hash in Ruby

今天我们会介绍HashHash中文为杂凑,不过汉汉老师还是习惯念英文。

看完这篇文章,读者即将会学到

  • Hash 的基本用法
  • Struct and OpenStruct

宣告

hash的赋值很简单。如下所示,只要给key, value即可。

data = {}
data[:a] = 1

data          
#=> { :a=>1 }

阵列也可以用Hash的写法来写,但不实用

data = []
data[2] = 'qwer'

data
#=> [nil, nil, "qwer"]

若宣告深层的hash的话,就会报错

data = {}
data[:a][:b] = 1

# Traceback (most recent call last):
# NoMethodError (undefined method `[]=' for nil:NilClass)

可以用比较更难的notation 来达成宣告深层Hash的目的

data = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
data[:a][:b] = 1
#=> {:a=>{:b=>1}}

接着我们用实际的例子,说明宣告深层Hash的原理

# 可以宣告2阶
hash = Hash.new { |h,k| h[k] = {} }
# 可以宣告3阶
hash = Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = {} } }
# 可以宣告n阶
hash = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }

splat

除了Array以外,Hash可以做到展开。Hash的展开以两个星号**表示。

a = {a: 1}
b = {b: 2}
c = {c: { c: 1 }}
  
{**a, **b} #=> {a: 1, b: 2}
a.merge(b) #=> {a: 1, b: 2}

{**a, **c} #=> {:a=>1, :c=>{:c=>1}}
a.merge(c) #=> {:a=>1, :c=>{:c=>1}}

** 在某种意义上跟 merge很像,就像在Array中*之余concat

一边写Javascript的读者们,不知道会不会不小心写成这样 ?

{...a, ...b}

我们举实际的例子,我们对data_attr做Hash层面的展开,而这里可以会有两个不懂的地方

  • .()到底是什麽 ➡️ Day8-10会揭开谜底
  • 为什麽html要绑那麽多data属性 ➡️ Stimulus 依靠资料绑定控制,会在Day26-27揭开谜底

def tab_list(list)
  # Day9 会讲到 block 与 Procedure
  data_attr = -> (content) { content.try(:[], :data).presence || {} }

  content_tag :ul, class: 'nav nav-tabs', role: 'tablist' do
    list.each_with_index.map do |content, index|
      if index.zero?
        content_tag(:li,
          content_tag(:a, content[:wording], href: "##{content[:id]}-tab",
            class: 'nav-link active', data: { toggle: 'tab', **data_attr.(content) },
            aria: { controls: "#{content[:id]}-tab", selected: 'true' }),
          class: 'nav-item', role: 'presentation')
      else
        content_tag(:li,
          content_tag(:a, content[:wording], href: "##{content[:id]}-tab",
            class: 'nav-link', data: { toggle: 'tab', **data_attr.(content) },
            aria: { controls: "#{content[:id]}-tab", selected: 'false' }),
          class: 'nav-item', role: 'presentation')
      end
    end .join.html_safe
  end
end

# helper
module Admin::UnshippedOrdersHelper
  def unshipped_tab
    [
      { id: 'unshipped', wording: '待出货', data: {a: 1, b: 2, c: 3} },
      { id: 'not_arrived', wording: '未取/未送达,需重新出货' },
    ]
  end
end

Destructuring

Javascript 有对object的解构,当然RubyHashes也会有!

hash = {:a => 1, :b => 2, :c => 3}
a, b = hash.values_at(:a, :b)

a # => 1
b # => 2

Destructuring

Javascript 有对object的解构,当然RubyHashes也会有!

hash = {:a => 1, :b => 2, :c => 3}
a, b = hash.values_at(:a, :b)

a # => 1
b # => 2

如果不能确定hash是否有可能为空值的话,可以写成下列形式

a, b = (hash || {}).values_at(:a, :b) #=> [nil, nil] 

a # => nil
b # => nil

#each_with_object

Day4 的篇章结尾已讲过,记得要复习。很重要!

Rescue

Day2 提到 save navigator➡️ &hash 的话不能使用&,但可以使用try 的方式救回

{a: 1, key: 3}.try(:[], :key) #=> 3
{a: 1}.try(:[], :key)         #=> nil
[{a:1}, {b:2}][2].try(:[], :qwer) #=> nil 
[{a:1}, {b:2}][2].try(:[], :qwer) #=> nil 
[{a:1}, {b:2}][1].try(:[], :b)    #=> 2 

⭐️ 取运货单好时使用的方法

if sub_order.sf_taken_at.nil?  
  # 用白话文比对: response[:routes][0][:occured_at]  
  response[:routes][0].try(:[], :occurred_at)
end

Struct

相比於JavascriptRubyhash并没有dot notation,不觉得这种事情很让人在意吗?尤其是在两个语言中间切换的时候,在Ruby写却会常常报错。

const a = {animal: 'cat'}
a['animal']  // cat
a.animal     // cat

Ruby程序语言中,Hash没有办法使用dot notation的形式。

a = {animal: 'cat'}
#=> {:animal=>"cat"} 

a.animal
# Traceback (most recent call last):
# NoMethodError (undefined method `animal' for {:animal=>"cat"}:Hash)

a[:animal]
#=> "cat" 

其实Ruby 有个介於Hash和自定义class中间的型别,叫做StructStruct可以模拟一个 class 物件,至於class的话会在Day11介绍。

Animal = Struct.new(:species)
animal = Animal.new('cat')

animal.species  # cat

另一个用法为OpenStruct

require 'ostruct'

cat = OpenStruct.new(species: "cat")

# 读取
cat.species # => "cat"
cat[:species] # => "cat"
cat["species"] # => "cat"

# 存入
cat.species = "Dog" # => "Dog"
cat[:species] = "Dog" # => "Dog"
cat["species"] = "Dog" # => "Dog"

# 像hash一样,可以新增属性
cat.foot = 4
cat.foot

# hash 转 OpenStruct 的应用
cat = {species: "cat"}
cat = OpenStruct.new(cat)

OpenStruct的使用方式,几乎就和使用 Javascript 一样,不过OpenStruct 一直有会拖慢速度的诟病。如果是大型专案,能避免就避免,但若为快速接案,要用真的可以。

⭐️ 若OpenStruct 作为Api使用会踩到一些雷。目前在回传值上,遇到会被多包一层:table 的状况

table: {
  return_amount: 5
  return_cash_amount: 5
  return_rebate_amount: 0
}  

我将原本的结果加上

original_openstruct_instance&.as_json.try(:[], 'table')

#dig

可以深挖hash,若不存在回传nil。虽然用法有点丑,不过这方法很好用

a = {a: {a: {a: {a: {a: {a: 1}}}}}}

a.dig(*%i(a a a a a a))
#=> 1

a.dig(*%i(a a))
#=> {:a=>{:a=>{:a=>{:a=>1}}}}

a.dig(*%i(a a b))
#=> nil

#fetch

fetch 可以用在回应不到目标 key 时回传预设 key

h = {
  'a' => :a_value,
  'b' => nil,
  'c' => false
}

h.fetch('a', :default_value) #=> :a_value
h.fetch('b', :default_value) #=> nil
h.fetch('c', :default_value) #=> false
h.fetch('d', :default_value) #=> :default_value

#slice

sliceslice!rails提供的方法。顺带一提,惊叹号在Ruby的程序语言中称为bang!,代表会破坏原本的结构。

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :b=>2}
option = [:a, :b]
{ a: 1, b: 2, c: 3, d: 4 }.slice(*option)

注意slice, slice!回传的结果不一样。

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

#except #without

without 就是except 的别称,不过 except 比较常拿来被使用。

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

h.without(:a, :c)  #=> { :b => 2 }

h.without!(:a, :c) # { :b => 2 }
h                  #=> { :b => 2 }

#merge

merge同样有bang跟没有的版本!

# merge!
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge!(h2)   #=> {"a"=>100, "b"=>254, "c"=>300}
h1              #=> {"a"=>100, "b"=>254, "c"=>300}

# merge!
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge!(h2) { |key, v1, v2| v1 }
                #=> {"a"=>100, "b"=>200, "c"=>300}
h1              #=> {"a"=>100, "b"=>200, "c"=>300}

# merge!
h = {}
h.merge!(key: "bar")  # => {:key=>"bar"}

题外话,Rails Controller 里面的ActionController::Parameters物件,也可以被视为Hash操作,所以也可以使用merge

_params
#=> <ActionController::Parameters {"status_eq"=>"", "payment_status_eq"=>"", "shipping_type_eq"=>"", "created_at_gteq"=>"2020-10-05", "created_at_lt"=>"2021-10-02", "stores_id_eq"=>"", "number_or_receiver_phone_or_receiver_name_or_customer_phone_or_customer_name_cont"=>"", "sync_pos_at_not_null"=>"", "invoice_status_eq"=>""}

_params.merge(status_eq: "unpaid")
#=> <ActionController::Parameters {"status_eq"=>"unpaid", "payment_status_eq"=>"", "shipping_type_eq"=>"", "created_at_gteq"=>"2020-10-05", "created_at_lt"=>"2021-10-02", "stores_id_eq"=>"", "number_or_receiver_phone_or_receiver_name_or_customer_phone_or_customer_name_cont"=>"", "sync_pos_at_not_null"=>"", "invoice_status_eq"=>""}

#reverse_merge

merge! 会改变Hash键的值,我们可以用reverse_merge 防止改变已经存在的key

hash_one = { a: 1, b:2 }
hash_one.merge({ a:2, b:3 }) # => { a:2, b:3 } 
hash_one = { a: 1, b:2 }
hash_one.reverse_merge({ a:2, b:3, c:3 }) # => { a:1, b:2, c:3 } 

#all?

检查 hash 是否有 nil

{a: 1, b: 2}.all? {|k,v| !v.nil?}     #=> "true"
{a: 1, b: nil}.all? {|k,v| !v.nil?}   #=> "false"

key to symbol

hash所有的key转为符号

# key 转为符号 (Ruby 2.5 以上可以用)
my_hash.transform_keys(&:to_sym)

# key 转为字串 (Ruby 2.5 以上可以用)
my_hash.transform_keys(&:to_s)

# 旧写法
my_hash = my_hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}

# Rails可以使用
my_hash.symbolize_keys
my_hash.deep_symbolize_keys 

iterate

Hash 也可以拿来做递回运算,只要用 eachmap 就行!

Invoice.statuses
#=> {"unissued"=>0, "issued"=>1, "allowance"=>2, "allowance_failed"=>3}

Invoice.statuses.map {1}
#=> [1, 1, 1, 1]

Invoice.statuses.map { |k, v| [k,v] }
#=> [["unissued", 0], ["issued", 1], ["allowance", 2], ["allowance_failed", 3]]

deep_transform_values

下列为Rails 6提供的方法,可以将hash包含深层的value做转换。下列的情境是要将以下hash的资料做省略符号跟大写开头

hash = {a: {b: "rewyeryewry", c: "weryewrewry"}, d: {e: "saelouertewryteryewrttwerytrewyn"}, f: ["reaergergdieweqrtqwteng", "ergehrerheerhehherdheherherhewrhewhrehrhehehrerehherhrehreng"]}

hash.deep_transform_values(&:capitalize).deep_transform_values{ |attribute| attribute.truncate(6) }
#=> {:a=>{:b=>"Rew...", :c=>"Wer..."}, :d=>{:e=>"Sae..."}, :f=>["Rea...", "Erg..."]}

实际上,当我收到了资料内容比较多的Array of Hash,这时候就可以使用对每一笔的value加省略符号的动作

layout_params.map { |data| data.deep_transform_values {|attribute| attribute.is_a?(String) ? attribute.truncate(3) : attribute} }

#=> [
#   {"layout_type"=>"...", "store_landing_elements"=>[{"element_type"=>"...", "panel_type"=>"...", "photo"=>{"url"=>"..."}}]},
#   {"layout_type"=>"...", "store_landing_elements"=>[{"element_type"=>"...", "panel_type"=>"...", "photo"=>{"url"=>"..."}}]},
#   {"layout_type"=>"...", "store_landing_elements"=>[{"element_type"=>"...", "panel_type"=>"...", "id"=>"2", "content"=>"..."}]},
#   {"...}]

deep_transform_values 可以广泛的应用在专案当中,因此一并介绍给读者

结论

大部分的重点,在Day4便已经讲完,因此今天的篇幅比较少,但这里还是整理一些重点

  • Hash 跟阵列一样可以使用each, map
  • 使用 Bang 表示会破坏原本资料的结构
  • 宣告多阶 Hash

明天会介绍Array, Hash之间的关系。

参考资料


<<:  流程控制

>>:  D-25. 枚举(enumerate) && Intersection of Two Arrays II

[Day18] 箭头函式

Arrow Function 这个从 ES6 开始新增的一种写法,叫做 Arrow Function...

Real Microsoft DA-100 Dumps - Pass DA-100 Exam With Ease

Actual Microsoft DA-100 Dumps – Quickest Way to Ge...

Day30 - this&Object Prototypes Ch3 Objects - Review

Iteration forEach()、every()、some() 三者的差异在於:他们会对我们...

Vue Router介绍

在昨天建置vue-cli插件时我们有新增vuex和vue-router,所以今天要先来介绍vue-r...

Day_01: 让 Vite 来开启你的Vue 前言

Hi Da Gei Ho~ 初次见面,我是Winnie~ 我是一位刚转职六个月的菜鸟前端(前身是网页...