Parsing an array of hashes into a tree structure - ruby

I have this structure in Ruby:
[
{“type"=>"Action", "parameterized"=>false, "groups"=>["feed"], "value"=>"feed”},
{"type"=>"State", "parameterized"=>true, "groups"=>["group"], "value"=>"group:%1s”},
{"type"=>"Action", "parameterized"=>false, "groups"=>["challenge", "build"], "value"=>"challenge:build”},
{"type"=>"Action", "parameterized"=>false, "groups"=>["challenge", "decorate"], "value"=>"challenge:decorate”},
{"type"=>"Action", "parameterized"=>false, "groups"=>["report"], "value"=>"report”},
{"type"=>"Action", "parameterized"=>false, "groups"=>["report", "complete"], "value"=>"report:complete”},
]
The elements in the array can come in any order, and I want it to be parsed into a tree structure so that if there are matching groups (each groups array do come in order where the first is the root and the next is a child of the first and the next again is the child of the one before and so on) are merged into the same branch in the tree. If there is a "parameterized" parameter, this also result in a extra branch in the tree. I want the structure above to end up line the following:
{
"Action" => {
"feed" => {
"value" => "feed"
},
"challenge" => {
"build" => {
"value" => "challenge:build"
}
"decorate" => {
"value" => "challenge:decorate"
}
},
"report" => {
"value" => "report",
"complete" => {
"value" => "report:complete"
}
}
},
"State" => {
"group" => {
"parameterized" => {
"value" => "group:%1s"
}
}
}
}
Any idea. how to do this in Ruby?
Best regards
Søren

First, let’s define the proc that will be finding buried groups inside the same type
deep_find = ->(hash, name) {
hash.detect { |k, v| k == name || v.is_a?(Hash) && deep_find.(v, name) }
}
Now we are ready to walk through the input hash either appending a tail to the found key or inserting the new key.
Objects in Ruby are mutable, and that will help us a lot; traversing the beast we’ll detect the needed hash and modify it inplace. This changes will be reflected in the whole.
input.each_with_object(Hash.new { |h, k| h[k] = h.dup.clear }) do |h, acc|
# let’s dig into the hash and find the _branch_ of interest
# it will return the hash _or_ `nil`
buried = deep_find.(acc[h['type']], '')
# if the target was found, we pivk it up for further modifications
# otherwise we start from the top level
target = buried ? buried.last : acc[h['type']]
# going down the tree of groups; each call to `inner[g]`
# will produce the nested hash, thanks to `default_proc`
# of the accumulator
target = h['groups'].reduce(target) { |inner, g| inner[g] }
# determine whether we should wrap the value with `parametrized`
# and eventually set it
(h['parameterized'] ? target['parameterized'] : target)['value'] = h['value']
end
#⇒ {"type1"=>{"group1"=>{"group2"=>{"value"=>"value1",
# "group7"=>{"group8"=>{"parameterized"=>{"value"=>"value3"}}}}}},
# "type2"=>{"group3"=>{"group4"=>{"group5"=>{"value"=>"value2"}}}}}
NB the trick with accumulator’s default proc (Hash.new { |h, k| h[k] = h.dup.clear }) allows not to bother with new values initialization, no matter how deep they are.

Related

Manipulate hash in Ruby

I have a hash that looks like
{
"lt"=>"456",
"c"=>"123",
"system"=>{"pl"=>"valid-player-name", "plv"=>"player_version_1"},
"usage"=>{"trace"=>"1", "cq"=>"versionid", "stream"=>"od",
"uid"=>"9", "pst"=>[["0", "1", "10"]], "dur"=>"0", "vt"=>"2"}
}
How can I go about turning it into a hash that looks like
{
"lt"=>"456",
"c"=>"123",
"pl"=>"valid-player-name",
"plv"=>"player_version_1",
"trace"=>"1",
"cq"=>"versionid",
"stream"=>"od",
"uid"=>"9",
"pst"=>[["0", "1", "10"]], "dur"=>"0", "vt"=>"2"
}
I basically want to get rid of the keys system and usage and keep what's nested inside them
"Low-tech" version :)
h = { ... }
h.merge!(h.delete('system'))
h.merge!(h.delete('usage'))
Assuming no rails:
hash.reject { |key, _| %w(system usage).include? key }.merge(hash['system']).merge(hash['usage'])
With active support:
hash.except('system', 'usage').merge(hash['system']).merge(hash['usage'])
A more generic version.
Merge any key that contains a hash:
h = { ... }
hnew = h.inject(h.dup) { |h2, (k, v)|
h2.merge!(h2.delete(k)) if v.is_a?(Hash)
h2
}
Assuming that your data has the same structure each time, I might opt for something simple and easy to understand like this:
def manipulate_hash(h)
{
"lt" => h["lt"],
"c" => h["c"],
"pl" => h["system"]["pl"],
"plv" => h["system"]["plv"],
"trace" => h["usage"]["trace"],
"cq" => h["usage"]["cq"],
"stream" => h["usage"]["stream"],
"uid" => h["uid"],
"pst" => h["pst"],
"dur" => h["dur"],
"vt" => h["vt"]
}
end
I chose to make the hash using one big hash literal expression that spans multiple lines. If you don't like that, you could build it up on multiple lines like this:
def manipulate_hash
r = {}
r["lt"] = h["lt"]
r["c"] = h["c"]
...
r
end
You might consider using fetch instead of the [] angle brackets. That way, you'll get an exception if the expected key is missing from the hash. For example, replace h["lt"] with h.fetch("lt").
If you plan to have an arbitrarily large list of keys to merge, this is an easily scaleable method:
["system", "usage"].each_with_object(myhash) do |key|
myhash.merge!(myhash.delete(key))
end

Ruby: how to replace key within multi dimensional hash without changing value [duplicate]

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

Hash of arrays to filepath-like array [duplicate]

This question already has answers here:
Converting a nested hash into a flat hash
(8 answers)
Closed 8 years ago.
Here is a structure of hash of arrays:
[
{
"key1" => [
"value1",
{"key2" => ["value2"]},
{"key3" => [
"value3",
{
"key4" => "value4"
}
]
}
]
},
{
"anotherKey1" => [],
}
]
I want desired output for that structure like filepaths:
/key1/value1
/key1/key2/value2
/key3/value3
/key3/key4/value4
How can I do that without inventing a wheel? Simple recursion could help, but is there any ready-to-go modules?
I do not think you would be reinventing any wheels to do this. You would like to traverse a nested structure of arrays and hashes and react completely different to the elements depending on whether something is an Array or a Hash. No library function is going to do exactly that for you, as you would need to vary more than one thing with blocks in order to be as flexible as you might like to be.
In short: write your recursive function to do this.
(Btw: The top level of your data structure is an array of hashes, not a hash of arrays …)
I decided to write my own wheel (thanks for Patru, vote up).
And I have this function:
def flat_hash_of_arrays(hash,string = "",delimiter="/",result = [])
# choose delimiter
hash.each do |key,value|
# string dup for avoid string-reference (oh, Ruby)
newString = string + delimiter + key
# if value is array
if value.is_a?(Array)
# if array not empty
value.each do |elementOfArray|
# if a string, I dont need recursion, hah
if elementOfArray.is_a?(String)
resultString = newString + delimiter + elementOfArray
# add new object
result << resultString
end
# if a hash, I need recursion
if elementOfArray.is_a?(Hash)
flat_hash_of_arrays(elementOfArray,newString,delimiter,result)
end
end
end
end
end
and test it:
flatten_hash = {
"key1" => [
"value1",
{"key2" => ["value2"]},
{"key3" => [
"value3",
{
"key4" => "value4"
}
]
},
"value4",
{
"key4" => ["value5"],
}
]
}
result = []
flat_hash_of_arrays(flatten_hash,"","/",result)
puts result
output is:
/key1/value1
/key1/key2/value2
/key1/key3/value3
/key1/value4
/key1/key4/value5
fine!

What is an eloquent way to sort an array of hashes based on whether a key is empty in Ruby?

array = [{ name:'Joe', foo:'bar' },
{ name:'Bob', foo:'' },
{ name:'Hal', foo:'baz' }
]
What is an eloquent way to sort so that if foo is empty, then put it at the end, and not change the order of the other elements?
Ruby 1.9.3
array.partition { |h| !h[:foo].empty? }.flatten
array.find_all{|elem| !elem[:foo].empty?} + array.find_all{|elem| elem[:foo].empty?}
returns
[{:name=>"Joe", :foo=>"bar"}, {:name=>"Hal", :foo=>"baz"}, {:name=>"Bob", :foo=>""}]
array = [
{ name:'Joe', foo:'bar' },
{ name:'Bob', foo:'' },
{ name:'Hal', foo:'baz' }
]
arraydup = array.dup
array.delete_if{ |h| h[:foo].empty? }
array += (arraydup - array)
Which results in:
[
[0] {
:name => "Joe",
:foo => "bar"
},
[1] {
:name => "Hal",
:foo => "baz"
},
[2] {
:name => "Bob",
:foo => ""
}
]
With a little refactoring:
array += ((array.dup) - array.delete_if{ |h| h[:foo].empty? })
One can produce keys as tuples, where the first part indicates null/not-null, and the second part is the original index, then sort_by [nulls_last, original_index].
def sort_nulls_last_preserving_original_order array
array.map.with_index.
sort_by { |h,i| [ (h[:foo].empty? ? 1 : 0), i ] }.
map(&:first)
end
Note this avoids all the gross array mutation of some of the other answers and is constructed from pure functional transforms.
array.each_with_index do |item, index|
array << (array.delete_at(index)) if item[:foo].blank?
end
Use whatever you have in place of blank?.

Replace hash value based on the value in another hash

Given two hashes, I'm trying to replace a value in the first hash for a key that the second hash also has. To be specific, I have these two hashes:
data = {
"study" => "Lucid Study",
"name" => "Lucid Plan",
"studyWillBe" => "Combination"
}
conditions = { "study" => "((current))" }
I want data to have its "study" key updated since conditions has that key. I want data to end up like this:
data = {
"study" => "((current))",
"name" => "Lucid Plan",
"studyWillBe" => "Combination"
}
I got this far:
data = Hash[data.map {|k, v| [conditions[k] || k, v] }]
but that's not quite doing the trick. Can anyone point me in the right direction?
You can do this
data.each {|k, v| data[k] = conditions[k] if conditions[k]}
It's called merge.
data = {"study"=>"Lucid Study", "name"=>"Lucid Plan", "studyWillBe"=>"Combination"}
conditions = {"study"=>"((current))"}
data.merge(conditions)
#{"study"=>"((current))", "name"=>"Lucid Plan", "studyWillBe"=>"Combination"}
The method merge can take a block where you can make some specific operation not only assign new value
data.merge(conditions) do |key, oldvalue, newvalue|
newvalue
end
=> {"study"=>"((current))", "name"=>"Lucid Plan", "studyWillBe"=>"Combination"}

Resources