How to deep transform values on Ruby hash
I have a hash which looks like this:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
I need to convert each value which is a single string inside an array so that it ends up like this:
hash = {
'key1' => 'value' ,
'key2' => {
'sub1' => 'string' ,
'sub2' => 'string' ,
},
'shippingInfo' => {
'shippingType' => 'Calculated' ,
'shipToLocations' => 'Worldwide' ,
'expeditedShipping' => 'false' ,
'oneDayShippingAvailable' => 'false' ,
'handlingTime' => '3' ,
}
}
I found this but couldn't get it work https://gist.github.com/chris/b4138603a8fe17e073c6bc073eb17785
Solution 1:
What about something like:
def deep_transform_values(hash)
return hash unless hash.is_a?(Hash)
hash.transform_values do |val|
if val.is_a?(Array) && val.length == 1
val.first
else
deep_transform_values(val)
end
end
end
Tested with something like:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
'an_integer' => 1,
'an_empty_array' => [],
'an_array_with_more_than_one_elements' => [1,2],
'a_symbol' => :symbol,
'a_string' => 'string'
}
}
Gives:
{
"key1"=>"value",
"key2"=>{
"sub1"=>"string",
"sub2"=>"string"
},
"shippingInfo"=> {
"shippingType"=>"Calculated",
"shipToLocations"=>"Worldwide",
"expeditedShipping"=>"false",
"oneDayShippingAvailable"=>"false",
"handlingTime"=>"3",
"an_integer"=>1,
"an_empty_array"=>[],
"an_array_with_more_than_one_elements"=>[1, 2],
"a_symbol"=>:symbol,
"a_string"=>"string"
}
}
Following your question in the comments, I guess the logic would change a bit:
class Hash
def deep_transform_values
self.transform_values do |val|
next(val.first) if val.is_a?(Array) && val.length == 1
next(val) unless val.respond_to?(:deep_transform_values)
val.deep_transform_values
end
end
end
Solution 2:
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide', 'Web'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
def recurse(hash)
hash.transform_values do |v|
case v
when Array
v.size == 1 ? v.first : v
when Hash
recurse v
else
# raise exception
end
end
end
recurse hash
#=> {"key1"=>"value",
# "key2"=>{
# "sub1"=>"string",
# "sub2"=>"string"
# },
# "shippingInfo"=>{
# "shippingType"=>"Calculated",
# "shipToLocations"=>["Worldwide", "Web"],
# "expeditedShipping"=>"false",
# "oneDayShippingAvailable"=>"false",
# "handlingTime"=>"3"
# }
# }
Solution 3:
As an alternative, consider using an object and allowing the initializer to deconstruct some of the keys for you.
One of the reasons a lot of people like myself started using Ruby in favour of Perl was because of the better expression of objects in place of primitives like arrays and hashes. Use it to your advantage!
class ShippingStuff # You've kept the data vague
def initialize key1:, key2:, shippingInfo:
@blk = -> val {
val.respond_to?(:push) && val.size == 1 ?
val.first :
cleankeys(val)
}
@key1 = cleankeys key1
@key2 = cleankeys key2
@shippingInfo = shippingInfo
end
attr_reader :key1, :key2, :shippingInfo
# basically a cut down version of what
# Sebastian Palma answered with
def cleankeys data
if data.respond_to? :transform_values
data.transform_values &@blk
else
@blk.call(data)
end
end
end
hash = {
'key1' => ['value'],
'key2' => {
'sub1' => ['string'],
'sub2' => ['string'],
},
'shippingInfo' => {
'shippingType' => ['Calculated'],
'shipToLocations' => ['Worldwide'],
'expeditedShipping' => ['false'],
'oneDayShippingAvailable' => ['false'],
'handlingTime' => ['3'],
}
}
shipper = ShippingStuff.new hash.transform_keys!(&:to_sym)
shipper.key1
# "value"
shipper.key2
# {"sub1"=>"string", "sub2"=>"string"}
shipper.shippingInfo
# {"shippingType"=>["Calculated"], "shipToLocations"=>["Worldwide"], "expeditedShipping"=>["false"], "oneDayShippingAvailable"=>["false"], "handlingTime"=>["3"]}
In the same vein, I'd even make an Info
class for the shippingInfo
data.
You may run into a different problem if key1
and key2
are dynamic, but there's ways around that too (double splat for one).