今天我們會介紹Hash
,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) }
除了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
會揭開謎底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
Javascript
有對object
的解構,當然Ruby
對 Hashes
也會有!
hash = {:a => 1, :b => 2, :c => 3}
a, b = hash.values_at(:a, :b)
a # => 1
b # => 2
Javascript
有對object
的解構,當然Ruby
對 Hashes
也會有!
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
Day4 的篇章結尾已講過,記得要複習。很重要!
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
相比於Javascript
,Ruby
的hash
並沒有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
中間的型別,叫做Struct
。Struct
可以模擬一個 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')
可以深挖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 可以用在回應不到目標 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
跟 slice!
是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}
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
同樣有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"=>""}
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 }
檢查 hash 是否有 nil
{a: 1, b: 2}.all? {|k,v| !v.nil?} #=> "true"
{a: 1, b: nil}.all? {|k,v| !v.nil?} #=> "false"
將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
Hash 也可以拿來做遞迴運算,只要用 each
或map
就行!
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]]
下列為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便已經講完,因此今天的篇幅比較少,但這裡還是整理一些重點
each
, map
明天會介紹Array
, Hash
之間的關係。