How to recursively remove all keys with empty values from (YAML) hash? - ruby-on-rails

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) }

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

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

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

Ruby how to modify parameters

so i have this code that and my aim was to convert any empty string to null
def convert_empty_strings_to_null
if request.patch? || request.post?
convert_empty_strings_to_null_rec(request.params)
end
end
def convert_empty_strings_to_null_rec(param)
param = nil if param.empty? if param.is_a?(String)
param.all?{|v| convert_empty_strings_to_null_rec v} if param.is_a?(Array)
param.all?{|k,v| convert_empty_strings_to_null_rec v} if param.is_a?(Hash)
end
But i'm new to ruby on rails and i found it that it sends params by value and not by reference, so no change in params is made, how do i fix this ?
I assume that by "empty" you mean zero-with strings, meaning that strings consisting only of whitespace should be left intact. (Otherwise blank? and strip would be your friends.)
def convert_empty_strings_to_nil
if request.patch? || request.post?
request.params.each do |key, value|
request.params[key] = convert_empty_strings_to_nil_rec(value)
end
end
end
def convert_empty_strings_to_nil_rec(param)
case param
when String
param.empty? ? nil : param
when Array
param.map{ |v| convert_empty_strings_to_nil_rec(v) }
when Hash
param.map{ |k,v| [k, convert_empty_strings_to_nil_rec(v)] }.to_h
else
param
end
end
First of all, this is how your convert_empty_strings_to_null_rec method should be, for keeping the changes persistent:
def convert_empty_strings_to_null_rec(param)
if param == ""
updated_param == nil
elsif param.is_a?(Array)
updated_param == param.map{|x| nil if x.empty? }
elsif param.is_a?(Hash)
updated_param = {}
param.each do |k, v|
if v.empty?
updated_param[k] = nil
else
updated_param[k] = v
end
end
end
return updated_param
end
Further, I am assuming from your question that convert_empty_strings_to_null is a action method. It should be updated to catch what convert_empty_strings_to_null_rec method is returning.
def convert_empty_strings_to_null
if request.patch? || request.post?
updated_params = convert_empty_strings_to_null_rec(request.params)
end
# you can use the updated_params here on in this action method
end
Hope it helps : )

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

Resources