Ruby : Generate a Hash of Hashes from an Array of Hashes - ruby

I have the following
friends = [{ name: "Jack", attr1:"def", attr2:"def" }, { name: "Jill", attr1:"def", attr2:"def" }]
I want to convert the above representation into a hash of hashes like this
friends = { "Jack" => { attr1: "def", attr2:"def" }, "Jill" => { attr1: "def", attr2: "def" } }
Any elegant way of doing this in Ruby ?

Hash[friends.map { |f| _f = f.dup; [_f.delete(:name), _f] }]
# => {"Jack"=>{:attr1=>"def", :attr2=>"def"}, "Jill"=>{:attr1=>"def", :attr2=>"def"}}

friends.each_with_object({}) do |f, o|
f = f.dup
o[f.delete :name] = f
end

hash = {}
friends.each{|h| hash[h.delete(:name)] = h }
# => {"Jack"=>{:attr1=>"def", :attr2=>"def"}, "Jill"=>{:attr1=>"def", :attr2=>"def"}}

When you want to transform one array into another, use collect:
friends = Hash[
friends.collect do |f|
_f = f.dup
name = _f.delete(:name)
[ name, _f ]
end
]
You can create a new hash easily using Hash[] and provide it an array with a series of key/value pairs in it. In this case the name field is removed from each.

If we understand "elegant" as the way to write concise code by leveraging reusable abstractions, I'd write:
require 'active_support/core_ext/hash'
require 'facets/hash'
friends.mash { |f| [f[:name], f.except(:name)] }
No need to add gem dependencies for these two fairly big libraries, you can always implement the individual methods in your extensions library.

Related

Create a new hash from an existing array of hashes

I am newbie to ruby . I have an array of hashes input_array
[{
"name"=>"test1",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>1000,
"name"=>"test2",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>5000,
"name"=>"test3",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>3000,
"name"=>"test4",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>2000}]
and an array
existing_names_array = ["test1","test2"]
The below line gets me all the names into input_names
input_array.each_with_index {|val, index| input_names << input_array[index]['name'] }
But how can I get the input_names to be a hash with name as key and its respective users as value?
Because my final goal is to check the names which are in input_names, but not in existing_names_array and get a count of those names and users
As the names tes1,test2 exists in existing_names_array, I need the count of rest of names and count of their respective users in input_array
Expected output:
output_names = test3, test 4
total_output_names = 2
output_users = 3000,2000
total_output_users = 5000
If you use ActiveSupport's Enumerable#index_by with Ruby core's Hash#transform_values it's pretty easy, if I'm understanding your question correctly:
# add gem 'activesupport' to Gemfile, or use Rails, then ..
require 'active_support/all'
users_and_counts = input_array.
index_by { |hsh| hsh["name"] }.
transform_values { |hsh| hsh["users"] }
# => { "test1" => 1000, "test2" => 5000, ... }
You can do this with Enumerable#reduce (or Enumerable#each_with_object) as well:
users_and_counts = input_array.reduce({}) do |memo, hsh|
memo[hsh["name"]] = hsh["users"]
memo
end
Or, the simplest way, with good old each:
users_and_counts = {}
input_array.each do |hsh|
users_and_counts[hsh["name"]] = hsh["users"]
end
in response to comment
In general, there are a few ways to check whether an element is found in an array:
array = ["foo", "bar"]
# 1. The simple standard way
array.include?("foo") # => true
# 2. More efficient way
require 'set'
set = Set.new(array)
set.member?("foo") # => true
So with this knowledge we can break up our task into a few steps:
Make a new hash which is a copy of the one we built above, but without the key-vals corresponding to users in the existing_names_array (see Hash#select and Hash#reject):
require 'set'
existing_names_set = Set.new(existing_names_array)
new_users_and_counts = users_and_counts.reject do |name, count|
existing_names_set.member?(name)
end
# => { "test3" => 3000, "test4" => 2000 }
Use Hash#keys to get the list of user names:
new_user_names = new_users_and_counts.keys
# => ["test3", "test4"]
Use Hash#values to get the list of counts:
new_user_counts = new_users_and_counts.values
# => [3000, 2000]
I assume input_array is to be as follows.
input_array = [
{ "name"=>"test1", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>1000 },
{ "name"=>"test2", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>5000 },
{ "name"=>"test3", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>3000 },
{ "name"=>"test4", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>2000}
]
We are also given:
names = ["test1", "test2"]
The information of interest can be represented nicely by the following hash (which I will construct):
summary = { "test1"=>1000, "test2"=>5000 }
We might use that to compute the following.
names = summary.keys
#=> ["test1", "test2"]
users = summary.values
#=> [1000, 5000]
total_users = users.sum
#=> 6000
There are many ways to construct the hash summary. I prefer the following.
summary = input_array.each_with_object({}) do |g,h|
key = g["name"]
h[key] = g["users"] if names.include?(key)
end
#=> {"test1"=>1000, "test2"=>5000}
Another way is as follows.
summary = input_array.map { |g| g.values_at("name", "users") }.
.to_h
.slice(*names)
See [Hash#values_at(https://ruby-doc.org/core-2.7.0/Hash.html#method-i-values_at), Array#to_h and Hash#slice. The steps are as follows.
arr = input_array.map { |g| g.values_at("name", "users") }
#=> [["test1", 1000], ["test2", 5000], ["test3", 3000], ["test4", 2000]]
h = arr.to_h
#=> {"test1"=>1000, "test2"=>5000, "test3"=>3000, "test4"=>2000}
summary = h.slice(*names)
#=> {"test1"=>1000, "test2"=>5000}
The splat operator converts h.slice(*names), which is h.slice(*["test1", "test2"]), to h.slice("test1", "test2"). That's because Hash#slice was defined to accept a variable number of keys as arguments, rather than an array of keys as the argument.

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

Sorting a hash of hashes in ruby

How can I sort this hash of hashes by "clients". I tried using sort_by, but this transforms it into an array of hashes. I am using JSON.parse to create this object from a json file. Thanks!
{
"default_attributes": {
"clients": {
"ABC": {
"db_name": "databaseabc"
},
"HIJ": {
"db_name": "databasehij"
},
"DEF": {
"db_name": "databasedef"
}
}
}
}
Why do you want to sort a hash? There's no advantage to it. Instead, get the keys, sort those, then use the keys to retrieve the data in the order you want.
For instance:
hash = {'z' => 26, 'a' => 1}
sorted_keys = hash.keys.sort # => ["a", "z"]
hash.values_at(*sorted_keys) # => [1, 26]
Using your example hash:
hash = {
"default_attributes": {
"clients": {
"ABC": {
"db_name": "databaseabc"
},
"HIJ": {
"db_name": "databasehij"
},
"DEF": {
"db_name": "databasedef"
}
}
}
}
clients = hash[:default_attributes][:clients]
sorted_keys = clients.keys.sort # => [:ABC, :DEF, :HIJ]
clients.values_at(*sorted_keys)
# => [{:db_name=>"databaseabc"},
# {:db_name=>"databasedef"},
# {:db_name=>"databasehij"}]
Or:
sorted_keys.each do |k|
puts clients[k][:db_name]
end
# >> databaseabc
# >> databasedef
# >> databasehij
Note: From looking at your "hash", it really looks like a JSON string missing the original surrounding { and }. If it is, this question becomes somewhat of an "XY problem". The first question should be "how do I convert a JSON string back to a Ruby object?":
require 'json'
hash = '{
"default_attributes": {
"clients": {
"ABC": {
"db_name": "databaseabc"
},
"HIJ": {
"db_name": "databasehij"
},
"DEF": {
"db_name": "databasedef"
}
}
}
}'
foo = JSON[hash]
# => {"default_attributes"=>
# {"clients"=>
# {"ABC"=>{"db_name"=>"databaseabc"},
# "HIJ"=>{"db_name"=>"databasehij"},
# "DEF"=>{"db_name"=>"databasedef"}}}}
At that point foo would contain a regular hash, and the inconsistent symbol definitions like "default_attributes": and "clients": would make sense because they ARE JSON hash keys, and the resulting parsed object would be a standard Ruby hash definition. And, you'll have to adjust the code above to access the individual nested hash keys.
If you are using Ruby <1.9, hashes are order-undefined. Sorting them makes no sense.
Ruby 1.9+ has ordered hashes; you would use sort_by, then convert your array of hashes back into a hash. Ruby 2.0+ provides Array#to_h for this.
data["default_attributes"]["clients"] = data["default_attributes"]["clients"].sort_by(&:first).to_h
hash = {
default_attributes: {
clients: {
ABC: {
"db_name": "databaseabc"
},
HIJ: {
"db_name": "databasehij"
},
DEF: {
"db_name": "databasedef"
}
}
}
}
If you do not wish to mutate hash, it's easiest to first make a deep copy:
h = Marshal.load(Marshal.dump(hash))
and then sort the relevant part of h:
h[:default_attributes][:clients] =
h[:default_attributes][:clients].sort.to_h
h
#=> {:default_attributes=>
# {:clients=>
# {:ABC=>{:db_name=>"databaseabc"},
# :DEF=>{:db_name=>"databasedef"},
# :HIJ=>{:db_name=>"databasehij"}}}}
Confirm hash was not mutated:
hash
#=> {:default_attributes=>
# {:clients=>
# {:ABC=>{:db_name=>"databaseabc"},
# :HIJ=>{:db_name=>"databasehij"},
# :DEF=>{:db_name=>"databasedef"}}}}
One of our interns came up with a pretty slick gem to perform deep sorts on hashes/arrays:
def deep_sort_by(&block)
Hash[self.map do |key, value|
[if key.respond_to? :deep_sort_by
key.deep_sort_by(&block)
else
key
end,
if value.respond_to? :deep_sort_by
value.deep_sort_by(&block)
else
value
end]
end.sort_by(&block)]
end
You can inject it into all hashes and then just call it like this:
[myMap.deep_sort_by { |obj| obj }][1]
The code would be similar for an array. We published "deepsort" as a gem for others to use. See "Deeply Sort Nested Ruby Arrays And Hashes" for additional details.
Disclaimer: I work for this company.

ruby set key of hash for array of hashes

Given the following array of hashes, how can I make a new hash with friend_id as the key and dist the value?
results = [
{"user_id"=>"18", "friend_id"=>"17", "dist"=>"1"},
{"user_id"=>"18", "friend_id"=>"42", "dist"=>"1"},
{"user_id"=>"18", "friend_id"=>"43", "dist"=>"1"},
{"user_id"=>"18", "friend_id"=>"46", "dist"=>"2"}
]
desired_hash = {"17" => "1", "42" => "1", "43" => "1", "46" => "2"}
I've tried map but the values are then in an array. I also tried to flatten that result but it flattened the key instead of the value
results.each_with_object({}) { |g,h| h[g["friend_id"]] = g["dist"] }
or
results.each_with_object({}) { |g,h| h.update(g["friend_id"]=> g["dist"]) }
For this use case, I think it would be simpler and more readable to just use #each:
desired_hash = Hash.new
results.each {|h| desired_hash[h["friend_id"]] = h["dist"]}
Then, desired_hash is:
#=> {"17"=>"1", "42"=>"1", "43"=>"1", "46"=>"2"}
You can use Enumerable#inject method.
results.inject({}) {|sum, e| sum.merge({e["friend_id"] => e["dist"]})}
# => {"17"=>"1", "42"=>"1", "43"=>"1", "46"=>"2"}
results.map {|h| [h['friend_id'], h['dist']]} .to_h
Although I probably like #CarySwoveland 's answer better, on the lines of:
results.each_with_object({}) {|h, n| n[h['friend_id']] = h['dist']}
Simply:
desired_hash = Hash[results.map{ |h| [ h['friend_id'], h['dist']] }]
or as Victor suggests
desired_hash = Hash[results.map{ |x| x.values_at('friend_id', 'dist') }]

ruby how to make a hash with new keys, and values from an array

I have an array of arrays like this:
arr = [["food", "eggs"],["beverage", "milk"],["desert", "cake"]]
And I need to turn it into an array of hashes where the keys are custom and new, and the values of the keys are the values in the array, like this:
hash = [{"category": "food", "item":"eggs"},
{"category": "beverage", "item":"milk"}
{"category": "desert", "item":"cake"}]
how would I do this?
thank you
Use Array#map:
arr = [["food", "eggs"], ["beverage", "milk"], ["desert", "cake"]]
arr.map { |category, item| { category: category, item: item } }
# => [
# {:category=>"food", :item=>"eggs"},
# {:category=>"beverage", :item=>"milk"},
# {:category=>"desert", :item=>"cake"}
# ]
arr = [["food", "eggs"],["beverage", "milk"],["desert", "cake"]]
arr.inject([]) do |hash, (v1, v2)|
hash << { category: v1, item: v2 }
end
I used inject to keep the code concise.
Next time you may want to show what you have tried in the question, just to demonstrate that you actually tried to do something before asking for code.
hash = array.map {|ary| Hash[[:category, :item].zip ary ]}
hash = arr.each_with_object({}){|elem, hsh|hsh[elem[0]] = elem[1]}

Resources