Ruby hash path to each leaf - ruby-on-rails

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

Related

converting a hash into array in ruby

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", ""]}}

Extract the error values of a hash

I have this hash
obj= {"User"=>["user_error", "Jack", "Jill1"], "Project"=>[ "project_error", "xxx"], "Task"=>[39], "Date"=>"date_error", "Time (Hours)"=>["time_error", "-2"], "Comment"=>"comment_error"}
I have to extract the error values of the keys and store them else where
.The end result should be
error = ["user_error", "project_error","date_error","time_error","comment_error"]
obj = {"User"=>["Jack", "Jill1"], "Project"=>[ "xxx"], "Task"=>[39], "Date"=>nil, "Time (Hours)"=>["-2"], "Comment"=>nil}
could some one help how to do this?
Not too pretty, but you can do something like this:
errors = obj.each_with_object([]) do |(k, v), err|
if v.is_a?(Array) && v.first =~ /_error$/
err << v.shift
elsif v =~ /_error$/
err << v
obj[k] = nil
end
end
Results:
errors
#=> ["user_error", "project_error", "date_error", "time_error", "comment_error"]
obj
#=> {"User"=>["Jack", "Jill1"], "Project"=>["xxx"], "Task"=>[39], "Date"=>nil, "Time (Hours)"=>["-2"], "Comment"=>nil}
You could DRY the code a bit by transforming all values to arrays first, but you will get empty arrays instead of nil for Date and Comment keys:
errors = obj.each_with_object([]) do |(k, v), err|
obj[k] = v = [v].flatten
err << v.shift if v.first =~ /_error$/
end
errors
#=> ["user_error", "project_error", "date_error", "time_error", "comment_error"]
obj
#=> {"User"=>["Jack", "Jill1"], "Project"=>["xxx"], "Task"=>[39], "Date"=>[], "Time (Hours)"=>["-2"], "Comment"=>[]}
You can do the following:
errors = []
obj.map do |class_name, strings|
errors.push(strings.shift) # shift remove the first element of the array
obj[class_name] = strings
end

Deeply compact nested hash in ruby?

Given the following hash structure...
{
a: nil,
b: [],
c: {c1: {c2: nil}},
d: [{d1: "Value!"}],
e: "Value!",
f: {f1: {f2: nil, f3: "Value!"}}
}
I'd like to be able to return...
{
d: [{d1: "Value!"}],
e: "Value!",
f: {f1: {f3: "Value!"}}
}
So the rules would be
1) Remove any key that points to a nil, {}, or [] value
2) Remove any key that leads to value which points to an empty value (example c: from the original hash)
3) Preserve the outer key if one or more inner keys point to a non empty value, but remove inner keys that point to an empty value. (see f: and notice that f2: is removed)
Any help would be appreciated!
You could have some fun with monkey-patching the core classes involved:
class Object
def crush
self
end
end
class Array
def crush
r = map(&:crush).compact
r.empty? ? nil : r
end
end
class Hash
def crush
r = each_with_object({ }) do |(k, v), h|
if (_v = v.crush)
h[k] = _v
end
end
r.empty? ? nil : r
end
end
It's an unusual thing to want to do, but if you do need it done writing a method like crush might help.
This should be a one pass operation that works with nested arrays and hashes:
def crush(thing)
if thing.is_a?(Array)
thing.each_with_object([]) do |v, a|
v = crush(v)
a << v unless [nil, [], {}].include?(v)
end
elsif thing.is_a?(Hash)
thing.each_with_object({}) do |(k,v), h|
v = crush(v)
h[k] = v unless [nil, [], {}].include?(v)
end
else
thing
end
end
def deep_compact(hash)
res_hash = hash.map do |key, value|
value = deep_compact(value) if value.is_a?(Hash)
value = nil if [{}, []].include?(value)
[key, value]
end
res_hash.to_h.compact
end

Iteration on rails hash

I have hash like this , I get value as params in my controller
Parameters:
{
"utf8"=>"✓",
"authenticity_token"=>"WwNhv6pbXMvQWamzcKTm6gixDEUvvbrsZ7OrMR8RSAA=",
"form_holiday"=>{
"user_id"=>"3",
"1"=>{"year_before"=>"2014", "day_before"=>"22", "year_now"=>"2015", "day_now"=>"20"},
"2"=>{"year_before"=>"", "day_before"=>"", "year_now"=>"", "day_now"=>""},
"3"=>{"year_before"=>"2014", "day_before"=>"10", "year_now"=>"2015", "day_now"=>"30"}}
}
How can I iterate on this hash and get the values of "1,2,3" ?
I want to change it so it looks like the following:
{"utf8"=>"✓",
"authenticity_token"=>"WwNhv6pbXMvQWamzcKTm6gixDEUvvbrsZ7OrMR8RSAA=",
"form_holiday"=>{
"1"=>{"user_id"=>"1", "year_before"=>"2014", "day_before"=>"22", "year_now"=>"2015", "day_now"=>"20"},
"2"=>{"user_id"=>"2", "year_before"=>"", "day_before"=>"", "year_now"=>"", "day_now"=>""},
"3"=>{"user_id"=>"3", "year_before"=>"2014", "day_before"=>"10", "year_now"=>"2015", "day_now"=>"30"}}
}
and here is my current code:
year_before = ""
year_now = ""
holiday = ""
#users = User.find(:all)
if params[:form_holiday]
hash_params = params[:form_holiday]
hash_params.each do |key, value|
if value
value.each do |key2, value2|
if key2 == "user_id"
holiday = Holiday.where("user_id = #{value2}") rescue nil
end
if key2 == "year_before"
year_before += "#{value2},"
end
if key2 == "day_before"
year_before += "#{value2}"
end
if key2 == "year_now"
year_now += "#{value2},"
end
if key2 == "day_now"
year_now += "#{value2}"
end
end
holiday.year_before = year_before
holiday.year_now = year_now
holiday.save
end
end
end - it doesnt work :(
i solved my problem, this solution is good but when i get holiday i got a set of hash and i using year_before on set of hash, it was stupid mistake and i sorry for that and thanks for help
To get the values, you may try like this:
h = params[:form_holiday]
h.each do |key, value|
value.values.each do |v|
puts v # v are the values for 1,2,3 etc
end
end
And if you want to get both key and value, you may do something like this:
h = params[:form_holiday]
h.each do |key, value|
value.each do |k, v|
puts k # k is the key, which would be 1 or 2 or 3 etc.
puts v # v are the values for 1 or 2 or 3 etc respectively.
# any operation you perform on k,v
end
end

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