I have this array [{:foo=>[{:bar=>[:baz]}]}, :foo, {:foo=>[{:bar=>[:bat]}]}, :bar]
As you can see there are symbols and hashes. What I am trying to do is avoid repetition for each key or keys inside values.
My desired output is :
[{:foo=>[{:bar=>[:baz, :bat]}]}, :bar]
As you can see, there's no repetition of key foo or repetition of foo=>bar.
I've been stuck for hours on this one and I cannot achieve it. Any idea?
The below would somehow work:
input = [{:foo=>[{:bar=>[:baz]}]},
:foo,
{:foo=>[{:bar=>[:bat]}]},
:bar]
builder = ->(value, acc = {}) {
case value
when Hash
value.each_with_object(acc) do |(k, v), acc|
builder.(v, acc[k] ||= {})
end
when Array
value.each_with_object(acc) do |v, acc|
builder.(v, acc)
end
else acc[value] ||= {}
end
}
The above already produces more or less acceptable result:
puts (built = builder.(input)).inspect
#⇒ {:foo=>{:bar=>{:baz=>{}, :bat=>{}}}, :bar=>{}}
To return exactly what you wanted one needs to chain lambdas (there is no way to tell in advance whether the object is a leaf or not):
fixer = ->(acc) {
result = acc.all? { |*v| v.last.empty? } ? acc.keys :
acc.map { |k, v| v.empty? ? k : { k => fixer.(v) } }
result.size == 1 ? result.first : result
}
puts fixer.(built).inspect
#⇒ [{:foo=>{:bar=>[:baz, :bat]}}, :bar]
I believe it’s up to you to play with this code to manage it to suit your needs better.
Related
Given I have this hash:
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
And I convert to OpenStruct:
o = OpenStruct.new(h)
=> #<OpenStruct a="a", b="b", c={:d=>"d", :e=>"e"}>
o.a
=> "a"
o.b
=> "b"
o.c
=> {:d=>"d", :e=>"e"}
2.1.2 :006 > o.c.d
NoMethodError: undefined method `d' for {:d=>"d", :e=>"e"}:Hash
I want all the nested keys to be methods as well. So I can access d as such:
o.c.d
=> "d"
How can I achieve this?
You can monkey-patch the Hash class
class Hash
def to_o
JSON.parse to_json, object_class: OpenStruct
end
end
then you can say
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
o = h.to_o
o.c.d # => 'd'
See Convert a complex nested hash to an object.
I came up with this solution:
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
json = h.to_json
=> "{\"a\":\"a\",\"b\":\"b\",\"c\":{\"d\":\"d\",\"e\":\"e\"}}"
object = JSON.parse(json, object_class:OpenStruct)
object.c.d
=> "d"
So for this to work, I had to do an extra step: convert it to json.
personally I use the recursive-open-struct gem - it's then as simple as RecursiveOpenStruct.new(<nested_hash>)
But for the sake of recursion practice, I'll show you a fresh solution:
require 'ostruct'
def to_recursive_ostruct(hash)
result = hash.each_with_object({}) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
OpenStruct.new(result)
end
puts to_recursive_ostruct(a: { b: 1}).a.b
# => 1
edit
Weihang Jian showed a slight improvement to this here https://stackoverflow.com/a/69311716/2981429
def to_recursive_ostruct(hash)
hash.each_with_object(OpenStruct.new) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
end
Also see https://stackoverflow.com/a/63264908/2981429 which shows how to handle arrays
note
the reason this is better than the JSON-based solutions is because you can lose some data when you convert to JSON. For example if you convert a Time object to JSON and then parse it, it will be a string. There are many other examples of this:
class Foo; end
JSON.parse({obj: Foo.new}.to_json)["obj"]
# => "#<Foo:0x00007fc8720198b0>"
yeah ... not super useful. You've completely lost your reference to the actual instance.
Here's a recursive solution that avoids converting the hash to json:
def to_o(obj)
if obj.is_a?(Hash)
return OpenStruct.new(obj.map{ |key, val| [ key, to_o(val) ] }.to_h)
elsif obj.is_a?(Array)
return obj.map{ |o| to_o(o) }
else # Assumed to be a primitive value
return obj
end
end
My solution is cleaner and faster than #max-pleaner's.
I don't actually know why but I don't instance extra Hash objects:
def dot_access(hash)
hash.each_with_object(OpenStruct.new) do |(key, value), struct|
struct[key] = value.is_a?(Hash) ? dot_access(value) : value
end
end
Here is the benchmark for you reference:
require 'ostruct'
def dot_access(hash)
hash.each_with_object(OpenStruct.new) do |(key, value), struct|
struct[key] = value.is_a?(Hash) ? dot_access(value) : value
end
end
def to_recursive_ostruct(hash)
result = hash.each_with_object({}) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
OpenStruct.new(result)
end
require 'benchmark/ips'
Benchmark.ips do |x|
hash = { a: 1, b: 2, c: { d: 3 } }
x.report('dot_access') { dot_access(hash) }
x.report('to_recursive_ostruct') { to_recursive_ostruct(hash) }
end
Warming up --------------------------------------
dot_access 4.843k i/100ms
to_recursive_ostruct 5.218k i/100ms
Calculating -------------------------------------
dot_access 51.976k (± 5.0%) i/s - 261.522k in 5.044482s
to_recursive_ostruct 50.122k (± 4.6%) i/s - 250.464k in 5.008116s
My solution, based on max pleaner's answer and similar to Xavi's answer:
require 'ostruct'
def initialize_open_struct_deeply(value)
case value
when Hash
OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value })
when Array
value.map { |element| send __method__, element }
else
value
end
end
Here is one way to override the initializer so you can do OpenStruct.new({ a: "b", c: { d: "e", f: ["g", "h", "i"] }}).
Further, this class is included when you require 'json', so be sure to do this patch after the require.
class OpenStruct
def initialize(hash = nil)
#table = {}
if hash
hash.each_pair do |k, v|
self[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
end
end
end
def keys
#table.keys.map{|k| k.to_s}
end
end
Basing a conversion on OpenStruct works fine until it doesn't. For instance, none of the other answers here properly handle these simple hashes:
people = { person1: { display: { first: 'John' } } }
creds = { oauth: { trust: true }, basic: { trust: false } }
The method below works with those hashes, modifying the input hash rather than returning a new object.
def add_indifferent_access!(hash)
hash.each_pair do |k, v|
hash.instance_variable_set("##{k}", v.tap { |v| send(__method__, v) if v.is_a?(Hash) } )
hash.define_singleton_method(k, proc { hash.instance_variable_get("##{k}") } )
end
end
then
add_indifferent_access!(people)
people.person1.display.first # => 'John'
Or if your context calls for a more inline call structure:
creds.yield_self(&method(:add_indifferent_access!)).oauth.trust # => true
Alternatively, you could mix it in:
module HashExtension
def very_indifferent_access!
each_pair do |k, v|
instance_variable_set("##{k}", v.tap { |v| v.extend(HashExtension) && v.send(__method__) if v.is_a?(Hash) } )
define_singleton_method(k, proc { self.instance_variable_get("##{k}") } )
end
end
end
and apply to individual hashes:
favs = { song1: { title: 'John and Marsha', author: 'Stan Freberg' } }
favs.extend(HashExtension).very_indifferent_access!
favs.song1.title
Here is a variation for monkey-patching Hash, should you opt to do so:
class Hash
def with_very_indifferent_access!
each_pair do |k, v|
instance_variable_set("##{k}", v.tap { |v| v.send(__method__) if v.is_a?(Hash) } )
define_singleton_method(k, proc { instance_variable_get("##{k}") } )
end
end
end
# Note the omission of "v.extend(HashExtension)" vs. the mix-in variation.
Comments to other answers expressed a desire to retain class types. This solution accommodates that.
people = { person1: { created_at: Time.now } }
people.with_very_indifferent_access!
people.person1.created_at.class # => Time
Whatever solution you choose, I recommend testing with this hash:
people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }
If you are ok with monkey-patching the Hash class, you can do:
require 'ostruct'
module Structurizable
def each_pair(&block)
each do |k, v|
v = OpenStruct.new(v) if v.is_a? Hash
yield k, v
end
end
end
Hash.prepend Structurizable
people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }
puts OpenStruct.new(people).person1.display.first
Ideally, instead of pretending this, we should be able to use a Refinement, but for some reason I can't understand it didn't worked for the each_pair method (also, unfortunately Refinements are still pretty limited)
Given:
h = {foo: {bar: 1}}
How to set bar if you don't know how many keys you have?
For example: keys = [:foo, :bar]
h[keys[0]][keys[1]] = :ok
But what if keys can be of arbitrary length and h is of arbitrary depth?
If you're using Ruby 2.3+ then you can use dig thusly:
h.dig(*keys[0..-2])[keys.last] = :ok
dig follows a path through the hash and returns what it finds. But dig won't copy what it finds so you get the same reference that is in h. keys[0..-2] grabs all but the last element of keys so h.dig(*keys[0..-2]) gives you the {bar: 1} Hash from inside h, then you can modify it in-place with a simple assignment.
You could also say:
*head, tail = keys
h.dig(*head)[tail] = :ok
if that's clearer to you than [0..-2].
If you don't have dig then you can do things like:
*head, tail = keys
head.inject(h) { |m, k| m[k] }[tail] = :ok
Of course, if you're not certain that the path specified by keys exists then you'll need to throw in a some nil checks and decide how you should handle keys that don't specify a path into h.
You could do it recursively like this:
h = {foo: {bar: 1}}
keys = [:foo, :bar]
h2 = {
foo: {
bar: {
baz: {
setting: :bad
}
}
}
}
keys2 = [:foo, :bar, :baz, :setting]
def set_nested_value(hash, keys, new_val)
if keys.length == 1
hash[keys.first] = new_val
return
end
keys.each_with_index do |key, i|
if hash[key]
set_nested_value(hash[key], keys[i+1..-1], new_val)
end
end
end
set_nested_value(h, keys, :ok)
p h
=> {:foo=>{:bar=>:ok}}
set_nested_value(h2, keys2, :good)
p h2
=> {:foo=>{:bar=>{:baz=>{:setting=>:good}}}}
Suppose I have following hash or nested hash:
h = { :a1 => { :b1 => "c1" },
:a2 => { :b2 => "c2"},
:a3 => { :b3 => "c3"} }
I want to create a method that takes hash as a parameter and recursively convert all the keys (keys that are symbol eg. :a1) to String (eg. "a1"). So far I have come up with the following method which doesn't work and returns {"a1"=>{:b1=>"c1"}, "a2"=>{:b2=>"c2"}, "a3"=>{:b3=>"c3"}}.:
def stringify_all_keys(hash)
stringified_hash = {}
hash.each do |k, v|
stringified_hash[k.to_s] = v
if v.class == Hash
stringify_all_keys(stringified_hash[k.to_s])
end
end
stringified_hash
end
What am I doing wrong and how do a get all the keys converted to string like this:
{"a1"=>{"b1"=>"c1"}, "a2"=>{"b2"=>"c2"}, "a3"=>{"b3"=>"c3"}}
If you are using ActiveSupport already or are open to using it, then deep_stringify_keys is what you're looking for.
hash = { person: { name: 'Rob', age: '28' } }
hash.deep_stringify_keys
# => {"person"=>{"name"=>"Rob", "age"=>"28"}}
Quick'n'dirty if your values are basic objects like strings, numbers, etc:
require 'json'
JSON.parse(JSON.dump(hash))
Didn't test this, but looks about right:
def stringify_all_keys(hash)
stringified_hash = {}
hash.each do |k, v|
stringified_hash[k.to_s] = v.is_a?(Hash) ? stringify_all_keys(v) : v
end
stringified_hash
end
using plain ruby code, the below code could help.
you can monkey patched it to the ruby Hash, to use it like this my_hash.deeply_stringfy_keys
however, I do not recommend monkey batching ruby.
you can adjust the method to provide the deeply_strigify_keys! (bang) version of it.
in case you want to make a different method witch does not stringify recursively, or to control the level of stringifying then consider re-writing the below method logic so you can have it written better with considering the other variation mentioned above.
def deeply_stringify_keys(hash)
stringified_hash = {}
hash.each do |k, v|
if v.is_a?(Hash)
stringified_hash[k.to_s] = deeply_stringify_keys(v)
elsif v.is_a?(Array)
stringified_hash[k.to_s] = v.map {|i| i.is_a?(Hash)? deeply_stringify_keys(i) : i}
else
stringified_hash[k.to_s] = v
end
end
stringified_hash
end
Consider this extension to Enumerable:
module Enumerable
def hash_on
h = {}
each do |e|
h[yield(e)] = e
end
h
end
end
It is used like so:
people = [
{:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
]
people_hash = people.hash_on { |person| person[:name] }
p people_hash['fred'] # => {:age=>32, :name=>"fred"}
p people_hash['barney'] # => {:age=>42, :name=>"barney"}
Is there a built-in function which already does this, or close enough to it that this extension is not needed?
Enumerable.to_h converts a sequence of [key, value]s into a Hash so you can do:
people.map {|p| [p[:name], p]}.to_h
If you have multiple values mapped to the same key this keeps the last one.
[ {:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
].group_by { |person| person[:name] }
=> {"fred"=>[{:name=>"fred", :age=>32}],
"barney"=>[{:name=>"barney", :age=>42}]}
Keys are in form of arrays to have a possibility to have a several Freds or Barneys, but you can use .map to reconstruct if you really need.
I have this method in helper:
def get_hash_keys(hash)
hash.delete_if{|k, v| v.class != Hash}
return hash.keys
end
When I call get_hash_keys from another method in the same helper, it returns a blank array :
def datas_size
sum = 0
new_hash = {title: "This is a test", datas: {firstname: "foo", lastname: "bar"}}
new_hash.each do |k, v|
sum += v.size if get_hash_keys(new_hash).includes? k
end
return sum
end
I tested to change return hash.keys with fixed array, and I get it. Only keys function seems not to work. I also double checked my array in params.
Is there some specifications I ignore working inside helpers ?
Edit for #DRSE :
I start with hash that contains others hashes. I need to know size of each children hash. The point is when I call this function (get_hash_keys) from the views, it fails (return blank array), but from console it works (return keys).
More investigation this morning drive me to conclude may be this is a wrong usage of delete_if. My ununderstood solution is to replace :
def get_hash_keys(hash)
hash.delete_if{|k, v| v.class != Hash}
return hash.keys
end
with
def get_hash_keys(hash)
tmp_hash = hash.clone
tmp_hash.delete_if{|k, v| v.class != Hash}
return tmp_hash.keys
end
Nothing is wrong with hash.keys:
def get_new_keys(hash)
hash.delete_if{|k,v| k == :bar}
return hash.keys
end
get_new_keys({qux: :bar, bar: :baz}) # => [:qux]
This, on the other hand, is busted.
def display_keys(hash)
return "foo " + get_new_keys(hash)
end
Are you expecting an array to be returned? If so, that's not the way to go about it at all. You could do something like:
def display_keys(hash)
return [:foo].concat(get_new_keys(hash))
end
In which case, it'd do something like:
def display_keys(hash)
return [:foo].concat(get_new_keys(hash))
end
display_keys({qux: :bar, bar: :baz}) # => [:foo, :qux]
The .keys method returns and Array of keys in the Hash.
Your question is impossible to answer for two reasons:
You have not included the condition in your delete_if block. My
guess is that you are deleting all keys in the hash, so .keys is
returning [].
You have not described the output you are expecting.
On a side note, your method display_keys does not work in ruby 2.3. You cannot concatenate an Array and a String like that. It will throw a TypeError: no implicit conversion of Array into String
But taking my best guess at it the following code works just fine:
def get_new_keys(hash)
hash.delete_if{ |k,v| v % 2 == 0 }
return hash.keys
end
def display_keys(hash)
return ["foo"] + get_new_keys(hash)
end
hash = {
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
seven: 7,
eight: 8,
nine: 9,
ten: 10
}
Outputs:
pry(main)> display_keys( hash )
=> ["foo", :one, :three, :five, :seven, :nine]