I need to group a hash by keys and concatenate the values. For example, given this hash:
[
{"name": "FT002", "data": {"2017-11-01": 1392.0}},
{"name": "FT004", "data": {"2017-11-01": 4091.0}},
{"name": "FT002", "data": {"2017-12-01": 1279.0}},
{"name": "FT004", "data": {"2017-12-01": 3249.0}}
]
I want to produce this hash:
[
{"name": "FT002", "data": {"2017-11-01": 1392.0, "2017-12-01": 1279.0}},
{"name": "FT004", "data": {"2017-11-01": 4091.0, "2017-12-01": 3249.0}}
]
Any help would be appreciated.
I tried various iterations of inject, group_by, and merge, but can't seem to get the right result.
You can accomplish this in three short one-liners, first producing a hash mapping names to data, and then producing your desired structure:
data = [
{"name":"FT002","data":{"2017-11-01":1392.0}},
{"name":"FT004","data":{"2017-11-01":4091.0}},
{"name":"FT002","data":{"2017-12-01":1279.0}},
{"name":"FT004","data":{"2017-12-01":3249.0}}
]
hash = Hash.new { |hash,key| hash[key] = {} }
data.each { |name:, data:| hash[name].merge!(data) }
hash = hash.map { |k,v| { name: k, data: v } }
data.group_by { |h| h[:name] }.map do |k,arr|
{ name: k, data: arr.each_with_object({}) { |g,h| h.update(g[:data]) } }
end
#=> [{:name=>"FT002", :data=>{:"2017-11-01"=>1392.0, :"2017-12-01"=>1279.0}},
# {:name=>"FT004", :data=>{:"2017-11-01"=>4091.0, :"2017-12-01"=>3249.0}}]
The first step is to use Enumerable#group_by to produce the following hash.
data.group_by { |h| h[:name] }
#=> {"FT002"=>[
# {:name=>"FT002", :data=>{:"2017-11-01"=>1392.0}},
# {:name=>"FT002", :data=>{:"2017-12-01"=>1279.0}}
# ],
# "FT004"=>[
# {:name=>"FT004", :data=>{:"2017-11-01"=>4091.0}},
# {:name=>"FT004", :data=>{:"2017-12-01"=>3249.0}}
# ]
# }
The second step is to simply manipulate the keys and values of this hash. See Hash#update (aka merge!).
An alternative to the second step is the following.
data.group_by { |h| h[:name] }.map do |k,arr|
{ name: k, data: arr.map { |g| g[:data].flatten }.to_h }
end
Note that this uses Hash#flatten, not Array#flatten.
This should generate the the results you're looking for:
data = [
{"name":"FT002","data":{"2017-11-01":1392.0}},
{"name":"FT004","data":{"2017-11-01":4091.0}},
{"name":"FT002","data":{"2017-12-01":1279.0}},
{"name":"FT004","data":{"2017-12-01":3249.0}}
]
newData = {}
data.each do |x|
newData[x[:name]] = [] unless newData[x[:name]].present?
newData[x[:name]].push x[:data]
end
combined = []
newData.each do |index,value|
dateData = {}
value.each do |dateStuff|
dateStuff.each do |dateIndex, dateValue|
dateData[dateIndex] = dateValue
end
end
values = {"name": index, "data": dateData}
combined.push values
end
combined
Related
I have some thing like this
[
{
"key": "55ffee8b6a617960010e0000",
"doc_count": 1
},
{
"key": "55fff0376a61794e190f0000",
"doc_count": 1
},
{
"key": "55fff0dd6a61794e191f0000",
"doc_count": 1
}
]
i want to separate :key values and :doc_count values into separate arrays like
["55ffee8b6a617960010e0000", "55fff0376a61794e190f0000", "55fff0dd6a61794e191f0000"]
and like [1,1,1]. How to achieve this?
You can use transpose here:
keys, doc_counts = array_of_hashes.map(&:values).transpose
As D-side points out this relies on the ordering of the keys being the same for each hash. If you cannot ensure this (for instance your data is being created via an API) you would have to perform the additional step of sorting the hash's keys. That would look something like:
keys, doc_counts = array_of_hashes.map{|h| Hash[h.sort].values }.transpose
In either case you'll end up with something like:
keys # => ["55ffee8b6a617960010e0000", "55fff0376a61794e190f0000", "55fff0dd6a61794e191f0000"]
doc_counts # => [1, 1, 1]
You can use some of these
a = [
{
"key" => "55ffee8b6a617960010e0000",
"doc_count" => 1
},
{
"key" => "55fff0376a61794e190f0000",
"doc_count" => 1
},
{
"key" => "55fff0dd6a61794e191f0000",
"doc_count" => 1
}
]
1.
hash = Hash[a.map { |h| [h["key"], h["doc_count"]] }]
hash.keys
hash.values
2.
exp = Hash.new { |k, v| k[v] = [] }
a.map { |h| h.each { |k, v| exp[k] << v } }
3.
hash = a.each_with_object({}) { |arr_h, h| h[arr_h["key"]] = arr_h["doc_count"] }
hash.keys
hash.values
You could iterate and assign it to new arrays doc_counts and keys.
array = [{"key"=>"55ffee8b6a617960010e0000", "doc_count"=>1}, {"key"=>"55fff0376a61794e190f0000", "doc_count"=>1}, {"key"=>"55fff0dd6a61794e191f0000", "doc_count"=>1}]
doc_counts, keys = [],[]
array.each do |a|
doc_counts << a["doc_count"]
keys << a["key"]
end
Result
>> doc_counts
=> [1, 1, 1]
>> keys
=> ["55ffee8b6a617960010e0000", "55fff0376a61794e190f0000", "55fff0dd6a61794e191f0000"]
Or
doc_counts = []
keys = array.map do |a|
doc_counts << a["doc_count"]
a["key"]
end
Given a list key-value pairs, in the form of an array of arrays - e.g. [ ["key1","value1"], ["key2","value2"], ["key1", "value3"] ], how to convert these to a Hash that stores all the values, in the most elegant way?
For the above example, I would want to get { "key1" => [ "value1", "value3" ], "key2" => [ "value2" ] }.
[["key1","value1"], ["key2","value2"], ["key1", "value3"]]
.group_by(&:first).each{|_, v| v.map!(&:last)}
Another way is to use the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are in both hashes being merged.
arr = [ ["key1","value1"], ["key2","value2"], ["key1", "value3"] ]
arr.each_with_object({}) { |(k,v),h| h.update(k=>[v]) { |_,o,n| o+n } }
#=> {"key1"=>["value1", "value3"], "key2"=>["value2"]}
Hash.new{ |h,k| h[k]=[] }.tap{ |h| array.each{ |k,v| h[k] << v } }
OR
c = Hash.new {|h,k| h[k] = [] }
array.each{|k, v| c[k] << v}
My best solution so far is this:
kvlist.inject(Hash.new([])) do |memo,a|
memo[a[0]] = (memo[a[0]] << a[1])
memo
end
Which I think is not very good.
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.
I have this data:
input = [ [ 'abc', '1.1' ], [ 'abc', '1.2' ], [ 'xyz', '3.14' ] ]
I would like output like the following:
[ { 'abc' => [ '1.1', '1.2' ] }, { 'xyz' => '3.14' } ]
Is it possible to achieve this in one chained expression?
I'd do it like this:
input = [['abc', '1.1'], ['abc','1.2'], ['xyz', '3.14']]
output = input.each_with_object(Hash.new{ |h, k| h[k] = [] }) { |(k, v), h| h[k] << v }
output # => {"abc"=>["1.1", "1.2"], "xyz"=>["3.14"]}
An alternate, which isn't as straightforward is:
input.group_by{ |k,v| k }.map{ |k, v| [k, v.map(&:last)] }.to_h # => {"abc"=>["1.1", "1.2"], "xyz"=>["3.14"]}
Your output structure
output: [{'abc' => ['1.1', '1.2']}, {'xyz' => '3.14'}]
is a very poor way to use a hash. Instead, you should have one hash, with multiple elements, since you're combining the like-key's values into one key.
If you REALLY need the output that way, as some seem to think, then simply append a map to the returned value:
input.each_with_object(Hash.new{ |h, k| h[k] = [] }) { |(k, v), h| h[k] << v }.map{ |k, v| {k => v} }
# => [{"abc"=>["1.1", "1.2"]}, {"xyz"=>["3.14"]}]
Simplest way I could think of:
input.group_by {|x| x[0]}.each_pair.map { |k, v| {k => (a=v.map(&:last)).count > 1 ? a : a[0]} }
The simplest way to do it is like this:
def convert_marray_to_hash(input)
hash = Hash.new { |hash, key| hash[key] = [] }
output = []
input.each { |array| hash[array[0]] << array[1] }
hash.keys.each { |key| output << { key => hash[key] } }
output
end
There are many ways to do it, but that way presents itself nicely and is readable.
This works pretty well:
input = [ [ 'abc', '1.1' ], [ 'abc','1.2' ], [ 'xyz', '3.14' ] ]
input.each_with_object({}) do |(key, val), hsh|
hsh[key] = val and next unless hsh.key?(key)
hsh[key] = [ *hsh[key], val ]
end
.map {|key, val| { key => val } }
# => [ { "abc" => [ "1.1", "1.2" ] },
# { "xyz" => "3.14" }
# ]
If you omit the final map you end up with a hash, which seems like a more natural result, but this matches the output specified, at least.
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?.