converting a hash into array in ruby - ruby-on-rails

I need the next hash:
x = {
params: {
user_params1: { name: "stephen", dir: "2001", dir2: nil },
user_params2: { name: "josh", dir: "jhon", dir2: nil }
}
to return a new hash of arrays like this:
x = {
params: {
user_params1: ["stephen","201", ""],
user_params2: ["josh","jhon",""]
}

Given:
x = {
params: {
user_params1: { name: "stephen", dir: "2001", dir2: nil },
user_params2: { name: "josh", dir: "jhon", dir2: nil }
}
}
Try:
x[:params] = x[:params].each_with_object({}) do |(k,v), returning|
returning[k] = v.map{|k,v| v}
end
Which will yield:
{:params=>{:user_params1=>["stephen", "2001", nil], :user_params2=>["josh", "jhon", nil]}}
If you want empty strings instead of nils (as in your example), do:
x[:params] = x[:params].each_with_object({}) do |(k,v), returning|
returning[k] = v.map{|k,v| v.to_s}
end
If you don't want to modify x, then just create a new hash and do the same:
y ={}
y[:params] = x[:params].each_with_object({}) do |(k,v), returning|
returning[k] = v.map{|k,v| v.to_s}
end
Since you're not doing anything with that k in v.map, you could just do v.values.map(&:to_s) (stolen shamelessly from Gerry's answer) - which is cleaner, IMO, but costs you one extra character(!) - and end up with:
y ={}
y[:params] = x[:params].each_with_object({}) do |(k,v), returning|
returning[k] = v.values.map(&:to_s)
end
As Sebastian points out, there is syntactic sugar for this:
y[:params] = x[:params].transform_values do |value|
# Then use one of:
# hash.values.map { |value| value.nil? ? '' : value }
# hash.values.map { |value| value ? value : '' }
# hash.values.map { |value| value || '' }
# hash.values.map(&:to_s)
end
Interestingly, if you look at the source code,
you'll see that the each_with_object and tranform_values mechanics are quite similar:
def transform_values
return enum_for(:transform_values) unless block_given?
result = self.class.new
each do |key, value|
result[key] = yield(value)
end
result
end
You could imagine this re-written as:
def transform_values
return enum_for(:transform_values) unless block_given?
each_with_object(self.class.new) do |(key, value), result|
result[key] = yield(value)
end
end
Which, at its root (IMO), is pretty much what Gerry and I came up with.
Seems to me this cat is well-skinned.

You use each_with_object (twice in case you have more thane one key on the top level); for example:
x.each_with_object({}) do |(k, v), result|
result[k] = v.each_with_object({}) do |(k1, v1), result1|
result1[k1] = v1.values.map(&:to_s)
end
end
#=> {:params=>{:user_params1=>["stephen", "2001", ""], :user_params2=>["josh", "jhon", ""]}}

Related

Ruby hash path to each leaf

First of all I beg your pardon if this question already exists, I deeply searched for a solution here but I've been able to find it, nevertheless I feel it's a problem so common that is seems so strange to not find anything here...
My struggle is the following: given an hash, I need to return all the PATHS to each leaf as an array of strings; so, for example:
{:a=> 1} gives ['a']
{:a=>{:b=>3, :c=>4} returns an array with two results: ["a.b", "a.c"]
{:a=>[1, {:b=>2}]} will result in ["a.0", "a.1.b"]
and so on...
I have found only partial solutions to this and with dozens of codelines. like this
def pathify
self.keys.inject([]) do |acc, element|
return acc if element.blank?
if !(element.is_a?(Hash) || element.is_a?(Array))
if acc.last.is_a?(Array)
acc[acc.size-1] = acc.last.join('.')
else
acc << element.to_s
end
end
if element.is_a?(Hash)
element.keys.each do |key|
if acc.last.is_a?(Array)
acc.last << key.to_s
else
acc << [key.to_s]
end
element[key].pathify
end
end
if element.is_a?(Array)
acc << element.map(&:pathify)
end
acc
end
end
But it does not work in all cases and is extremely inefficient. Summarizing: is there any way to "pathify" an hash to return all the paths to each leaf in form of array of strings?
Thank you for the help!
Edited
Adding some specs
for {} it returns []
for {:a=>1} it returns ["a"]
for {:a=>1, :b=>1} it returns ["a", "b"]
for {:a=>{:b=>1}} it returns ["a.b"] (FAILED - 1) got: ["a"]
for {:a=>{:b=>1, :c=>2}} it returns ["a.b", "a.c"] (FAILED - 2) got: ["a"]
for {:a=>[1]} it returns ["a.0"] (FAILED - 3) got: ["a"]
for {:a=>[1, "b"]} it returns ["a.0", "a.1"] (FAILED - 4) got: ["a"]
def show(key, path)
if path.is_a? Array
path.map {|p| "#{key}.#{p}"}
else
path == "" ? key.to_s : "#{key}.#{path}"
end
end
def pathify(input)
if input.is_a? Hash
input.map do |k,v|
sub_path = pathify(v)
show(k, sub_path)
end.flatten
elsif input.is_a? Array
input.map.with_index do |v, i|
sub_path = pathify(v)
show(i, sub_path)
end.flatten
else
""
end
end
def leaf_paths(enum)
return unless [Hash, Array].include? enum.class
[].tap do |result|
if enum.is_a?(Hash)
enum.each { |k, v| result = attach_leaf_paths(k, v, result) }
elsif enum.is_a?(Array)
enum.each_with_index { |elem, index| result = attach_leaf_paths(index, elem, result) }
end
end
end
def attach_leaf_paths(key, value, result)
if (children = leaf_paths(value))
children.each { |child| result << "#{key}.#{child}" }
else
result << key.to_s
end
result
end
This is very similar to https://github.com/wteuber/yaml_normalizer/blob/b85dca7357df00757c471acb5dadb79a53dd27c1/lib/yaml_normalizer/ext/namespaced.rb
So I tweaked the code a bit to fit your needs:
module Leafs
def leafs(namespace = [], tree = {})
each do |key, value|
child_ns = namespace.dup << key
if value.instance_of?(Hash)
value.extend(Leafs).leafs child_ns, tree
elsif value.instance_of?(Array)
value.each.with_index.inject({}) {|h, (v,k)| h[k]=v; h}.extend(Leafs).leafs child_ns, tree
else
tree[child_ns.join('.')] = value
end
end
tree.keys.to_a
end
end
Here is how to use it:
h = {a: [1, "b"], c: {d:1}}
h.extend(Leafs)
h.leafs
# => ["a.0", "a.1", "c.d"]
I hope you find this helpful.
def pathify(what)
paths = []
if what.is_a?(Array)
what.each_with_index do | element, index |
paths+= pathify(element).map{|e| index.to_s + '.' + e.to_s}
end
elsif what.is_a?(Hash)
what.each do |k,v|
paths+= pathify(v).map{|e| k.to_s + '.' + e.to_s}
end
else
paths.append('')
end
paths.map{|e| e.delete_suffix('.')}
end

Callback when parsing with oj gem

I'd like to understand how to understand the way to apply transformation (underscore in my case) on each key when parsing a JSON file with Oj.
For example in ruby/rails :
require 'oj'
Oj.optimize_rails()
def transform_keys(hash)
hash.deep_transform_keys { |k| k.to_s.underscore }
end
data = '{ "JsonKey": "JsonValue", "JsonKey2": { "JsonSubKey2": "JsonSubValue" }, "Array": ["ValueArray1","ValueArray2"] }'
data2 = '[{ "JsonKey": "JsonValue", "JsonKey2": { "JsonSubKey2": "JsonSubValue" }, "Array": ["ValueArray1","ValueArray2"] }]'
json_data = JSON.parse(data)
case json_data
when Hash
transform_keys(json_data)
when Array
json_data.map { |hash| transform_keys(hash) }
end
This do the job but it iterate twice : one during parsing and for apply the transformation.
Checking oj, I found :
::Oj::ScHandler http://www.ohler.com/oj/doc/Oj/ScHandler.html
::Oj::Saj http://www.ohler.com/oj/doc/Oj/Saj.html
Let's try :
class UnderscoreKeyHandler < ::Oj::ScHandler
def hash_start
{}
end
def hash_set(h,k,v)
h[k.to_s.underscore] = v
end
def array_start
[]
end
def array_append(a,v)
a << v
end
def add_value(v)
end
end
handler = UnderscoreKeyHandler.new
Oj.sc_parse(handler, data)
It work! Ok, let's benchmark this :
require 'oj'
Oj.optimize_rails()
hash = Hash.new
key = 'abcd'
1000.times { hash[key.succ!] = hash.keys }
json = hash.to_json; nil
def transform_keys(hash)
hash.deep_transform_keys { |k| k.to_s.underscore }
end
10000 * Benchmark.realtime {
json_data = JSON.parse(json)
case json_data
when Hash
transform_keys(json_data)
when Array
json_data.map { |hash| transform_keys(hash) }
end
} # Between 2400 and 3500
10000 * Benchmark.realtime { Oj.sc_parse(handler, json) } # Between 700 and 1600
Perfect? I seem to be but i feel insecure...
In fact i just want to override each key for a hash and not rewrite a full handler. Is there a way?
Moreover :
What is the difference between ::Oj::ScHandler and ::Oj::Saj ?
What are the risk of using a custom handler
What is the purpose of the add_value method.
How can i raise another error than the Oj::ParseError default error?

transform_keys for an array of hashes

I have the following array of hashes and I want to use transform_keys to strip the beginning of each key using a regex:
array_of_hashes = [{"a_0_abc"=>"1",
"a_0_def"=>"1",
"a_0_hij"=>"1",},
{"a_1_abc”=>"2",
"a_1_def"=>"2",
"a_1_hij"=>"2"}]
and I want the following:
transformed_hash_keys = [{"abc"=>"1",
"def"=>"1",
"hij"=>"1",},
{"abc"=>"2",
"def"=>"2",
"hij"=>"2"}]
I have the following method but it results in array_of_hashes instead of transformed_hash_keys:
def strip
s = array_of_hashes.each { |hash| hash.transform_keys { |key| key.sub(/^a_(\d+)_/, '') } }
end
Can anyone tell me what I'm doing wrong in this method?
transform_keys doesn't operate in place and each returns the original iterator, not the result of the block.
You could do what you want with map instead of each.
def strip
s = array_of_hashes.map { |hash| hash.transform_keys { |key| key.sub(/^a_(\d+)_/, '') } }
end
Or, you could use transform_keys! to modify the contents of array_of_hashes
def strip
s = array_of_hashes.each { |hash| hash.transform_keys! { |key| key.sub(/^a_(\d+)_/, '') } }
end
Here's a pure Ruby solution.
arr = [{ "a_0_abc"=>"1", "a_0_def"=>"1", "a_0_hij"=>"1" },
{ "a_1_abc"=>"2", "a_1_def"=>"2", "a_1_hij"=>"2" }]
arr.map { |h| h.map { |k,v| [k[/[[:alpha:]]+\z/], v] }.to_h }
#=> [{"abc"=>"1", "def"=>"1", "hij"=>"1"}, {"abc"=>"2", "def"=>"2", "hij"=>"2"}]
or
arr.map { |h| h.each_with_object({}) { |(k,v),g| g[k[/[[:alpha:]]+\z/]] = v } }
# => [{"abc"=>"1", "def"=>"1", "hij"=>"1"}, {"abc"=>"2", "def"=>"2", "hij"=>"2"}]
This lets you do any kind of transformation on the keys just passing a block to it:
def strip_keys(object)
deep_transform_keys_in_object!(object) { |key| key.sub(/^a_(\d+)_/, '') }
end
def deep_transform_keys_in_object!(object, &block)
case object
when Hash
object.keys.each do |key|
value = object.delete(key)
object[yield(key)] = deep_transform_keys_in_object!(value, &block)
end
object
when Array
object.map! { |e| deep_transform_keys_in_object!(e, &block) }
else
object
end
end

How can I replace a hash key with another key?

I have a condition that gets a hash.
hash = {"_id"=>"4de7140772f8be03da000018", .....}
Yet, I want to rename the key of that hash as follows.
hash = {"id"=>"4de7140772f8be03da000018", ......}
P.S. I don't know what keys are in the hash; they are random. Some keys are prefixed with an underscore that I would like to remove.
hash[:new_key] = hash.delete :old_key
rails Hash has standard method for it:
hash.transform_keys{ |key| key.to_s.upcase }
http://api.rubyonrails.org/classes/Hash.html#method-i-transform_keys
UPD: ruby 2.5 method
If all the keys are strings and all of them have the underscore prefix, then you can patch up the hash in place with this:
h.keys.each { |k| h[k[1, k.length - 1]] = h[k]; h.delete(k) }
The k[1, k.length - 1] bit grabs all of k except the first character. If you want a copy, then:
new_h = Hash[h.map { |k, v| [k[1, k.length - 1], v] }]
Or
new_h = h.inject({ }) { |x, (k,v)| x[k[1, k.length - 1]] = v; x }
You could also use sub if you don't like the k[] notation for extracting a substring:
h.keys.each { |k| h[k.sub(/\A_/, '')] = h[k]; h.delete(k) }
Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }
And, if only some of the keys have the underscore prefix:
h.keys.each do |k|
if(k[0,1] == '_')
h[k[1, k.length - 1]] = h[k]
h.delete(k)
end
end
Similar modifications can be done to all the other variants above but these two:
Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }
should be okay with keys that don't have underscore prefixes without extra modifications.
you can do
hash.inject({}){|option, (k,v) | option["id"] = v if k == "_id"; option}
This should work for your case!
If we want to rename a specific key in hash then we can do it as follows:
Suppose my hash is my_hash = {'test' => 'ruby hash demo'}
Now I want to replace 'test' by 'message', then:
my_hash['message'] = my_hash.delete('test')
For Ruby 2.5 or newer with transform_keys and delete_prefix / delete_suffix methods:
hash1 = { '_id' => 'random1' }
hash2 = { 'old_first' => '123456', 'old_second' => '234567' }
hash3 = { 'first_com' => 'google.com', 'second_com' => 'amazon.com' }
hash1.transform_keys { |key| key.delete_prefix('_') }
# => {"id"=>"random1"}
hash2.transform_keys { |key| key.delete_prefix('old_') }
# => {"first"=>"123456", "second"=>"234567"}
hash3.transform_keys { |key| key.delete_suffix('_com') }
# => {"first"=>"google.com", "second"=>"amazon.com"}
h.inject({}) { |m, (k,v)| m[k.sub(/^_/,'')] = v; m }
hash.each {|k,v| hash.delete(k) && hash[k[1..-1]]=v if k[0,1] == '_'}
I went overkill and came up with the following. My motivation behind this was to append to hash keys to avoid scope conflicts when merging together/flattening hashes.
Examples
Extend Hash Class
Adds rekey method to Hash instances.
# Adds additional methods to Hash
class ::Hash
# Changes the keys on a hash
# Takes a block that passes the current key
# Whatever the block returns becomes the new key
# If a hash is returned for the key it will merge the current hash
# with the returned hash from the block. This allows for nested rekeying.
def rekey
self.each_with_object({}) do |(key, value), previous|
new_key = yield(key, value)
if new_key.is_a?(Hash)
previous.merge!(new_key)
else
previous[new_key] = value
end
end
end
end
Prepend Example
my_feelings_about_icecreams = {
vanilla: 'Delicious',
chocolate: 'Too Chocolatey',
strawberry: 'It Is Alright...'
}
my_feelings_about_icecreams.rekey { |key| "#{key}_icecream".to_sym }
# => {:vanilla_icecream=>"Delicious", :chocolate_icecream=>"Too Chocolatey", :strawberry_icecream=>"It Is Alright..."}
Trim Example
{ _id: 1, ___something_: 'what?!' }.rekey do |key|
trimmed = key.to_s.tr('_', '')
trimmed.to_sym
end
# => {:id=>1, :something=>"what?!"}
Flattening and Appending a "Scope"
If you pass a hash back to rekey it will merge the hash which allows you to flatten collections. This allows us to add scope to our keys when flattening a hash to avoid overwriting a key upon merging.
people = {
bob: {
name: 'Bob',
toys: [
{ what: 'car', color: 'red' },
{ what: 'ball', color: 'blue' }
]
},
tom: {
name: 'Tom',
toys: [
{ what: 'house', color: 'blue; da ba dee da ba die' },
{ what: 'nerf gun', color: 'metallic' }
]
}
}
people.rekey do |person, person_info|
person_info.rekey do |key|
"#{person}_#{key}".to_sym
end
end
# =>
# {
# :bob_name=>"Bob",
# :bob_toys=>[
# {:what=>"car", :color=>"red"},
# {:what=>"ball", :color=>"blue"}
# ],
# :tom_name=>"Tom",
# :tom_toys=>[
# {:what=>"house", :color=>"blue; da ba dee da ba die"},
# {:what=>"nerf gun", :color=>"metallic"}
# ]
# }
Previous answers are good enough, but they might update original data.
In case if you don't want the original data to be affected, you can try my code.
newhash=hash.reject{|k| k=='_id'}.merge({id:hash['_id']})
First it will ignore the key '_id' then merge with the updated one.
Answering exactly what was asked:
hash = {"_id"=>"4de7140772f8be03da000018"}
hash.transform_keys { |key| key[1..] }
# => {"id"=>"4de7140772f8be03da000018"}
The method transform_keys exists in the Hash class since Ruby version 2.5.
https://blog.bigbinary.com/2018/01/09/ruby-2-5-adds-hash-transform_keys-method.html
If you had a hash inside a hash, something like
hash = {
"object" => {
"_id"=>"4de7140772f8be03da000018"
}
}
and if you wanted to change "_id" to something like"token"
you can use deep_transform_keys here and do it like so
hash.deep_transform_keys do |key|
key = "token" if key == "_id"
key
end
which results in
{
"object" => {
"token"=>"4de7140772f8be03da000018"
}
}
Even if you had a symbol key hash instead to start with, something like
hash = {
object: {
id: "4de7140772f8be03da000018"
}
}
you can combine all of these concepts to convert them into a string key hash
hash.deep_transform_keys do |key|
key = "token" if key == :id
key.to_s
end
If you only want to change only one key, there is a straightforward way to do it in Ruby 2.8+ using the transform_keys method. In this example, if you want to change _id to id, then you can:
hash.transform_keys({_id: :id})
Reference: https://bugs.ruby-lang.org/issues/16274

How to recursively remove all keys with empty values from (YAML) hash?

I have been trying to get rid of all hash keys in my YAML file that have empty (blank) values or empty hashes as values.
This earlier post helped me to get it almost right, but the recursive one-liner leaves my YAML dump with empty hashes whenever there is sufficiently deep nesting.
I would really appreciate any help on this. Thanks!
proc = Proc.new { |k, v| (v.kind_of?(Hash) && !v.empty? ) ? (v.delete_if(&proc); nil) : v.blank? }
hash = {"x"=>{"m"=>{"n"=>{}}}, 'y' => 'content'}
hash.delete_if(&proc)
Actual output
{"x"=>{"m"=>{}}, "y"=>"content"}
Desired output
{"y"=>"content"}
class Hash
def delete_blank
delete_if{|k, v| v.empty? or v.instance_of?(Hash) && v.delete_blank.empty?}
end
end
p hash.delete_blank
# => {"y"=>"content"}
Here's a more generic method:
class Hash
def deep_reject(&blk)
self.dup.deep_reject!(&blk)
end
def deep_reject!(&blk)
self.each do |k, v|
v.deep_reject!(&blk) if v.is_a?(Hash)
self.delete(k) if blk.call(k, v)
end
end
end
{ a: 1, b: nil, c: { d: nil, e: '' } }.deep_reject! { |k, v| v.blank? }
==> { a: 1 }
I think this the most correct version:
h = {a: {b: {c: "",}, d:1}, e:2, f: {g: {h:''}}}
p = proc do |_, v|
v.delete_if(&p) if v.respond_to? :delete_if
v.nil? || v.respond_to?(:"empty?") && v.empty?
end
h.delete_if(&p)
#=> {:a=>{:d=>1}, :e=>2}
I know this thread is a bit old but I came up with a better solution which supports Multidimensional hashes. It uses delete_if? except its multidimensional and cleans out anything with a an empty value by default and if a block is passed it is passed down through it's children.
# Hash cleaner
class Hash
def clean!
self.delete_if do |key, val|
if block_given?
yield(key,val)
else
# Prepeare the tests
test1 = val.nil?
test2 = val === 0
test3 = val === false
test4 = val.empty? if val.respond_to?('empty?')
test5 = val.strip.empty? if val.is_a?(String) && val.respond_to?('empty?')
# Were any of the tests true
test1 || test2 || test3 || test4 || test5
end
end
self.each do |key, val|
if self[key].is_a?(Hash) && self[key].respond_to?('clean!')
if block_given?
self[key] = self[key].clean!(&Proc.new)
else
self[key] = self[key].clean!
end
end
end
return self
end
end
Just a bit related thing. If you want to delete specified keys from nested hash:
def find_and_destroy(*keys)
delete_if{ |k, v| (keys.include?(k.to_s) ? true : ( (v.each { |vv| vv = vv.find_and_destroy(*keys) }) if v.instance_of?(Array) ; (v.each { |vv| vv = vv.find_and_destroy(*keys) }) if v.instance_of?(Hash); false) )}
end
.You can also customize it further
hash = {"x"=>{"m"=>{"n"=>{}}}, 'y' => 'content'}
clean = proc{ |k,v| !v.empty? ? Hash === v ? v.delete_if(&clean) : false : true }
hash.delete_if(&clean)
#=> {"y"=>"content"}
or like #sawa suggested, you can use this proc
clean = proc{ |k,v| v.empty? or Hash === v && v.delete_if(&clean) }

Resources