iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

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

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之間的關係。

參考資料


上一篇
Day4. 一起精通 Rails Array,處理更複雜的問題
下一篇
Day6. Array & Hash 之間的組合應用
系列文
初階 Rails 工程師的養成34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言