What's the "Rubyist" way to do the following data structure transformation:
I have
incoming = [ {:date => 20090501, :width => 2},
{:date => 20090501, :height => 7},
{:date => 20090501, :depth => 3},
{:date => 20090502, :width => 4},
{:date => 20090502, :height => 6},
{:date => 20090502, :depth => 2},
]
and I want to collapse these by :date, to end up with
outgoing = [ {:date => 20090501, :width => 2, :height => 7, :depth => 3},
{:date => 20090502, :width => 4, :height => 6, :depth => 2},
]
An array of arrays would also be fine at the last step, provided that the columns are in the same order in each row. Also, importantly, I do not know all the hash keys in advance (that is, I do not know :width, :height, or :depth -- they could be :cats, :dogs, and :hamsters).
If using Ruby 1.8.7 or Ruby 1.9+ the following code reads well:
incoming.group_by{|hash| hash[:date]}.map do |_, hashes|
hashes.reduce(:merge)
end
The underscore in the block attributes (_, hashes) indicates that we don't need/care about that particular attribute.
#reduce is an alias for #inject, which is used to reduce a collection into a single item. In the new Ruby versions it also accepts a symbol, which is the name of the method used to do the reduction.
It starts out by calling the method on the first item in the collection with the second item as the argument. It then calls the method again on the result with the third item as the argument and so on until there are no more items.
[1, 3, 2, 2].reduce(:+) => [4, 2, 2] => [6, 2] => 8
Here is a one liner :)
incoming.inject({}){ |o,i| o[i[:date]]||=[];o[i[:date]]<<i;o}.map{|a| a[1].inject(){|o,i| o.merge(i)}}
But actually the previous post is more clear, and might be faster too.
EDIT: with a bit of optimization:
p incoming.inject(Hash.new{|h,k| h[k]=[]}){ |o,i| o[i[:date]]<<i;o}.map{|a| a[1].inject(){|o,i| o.merge(i)}}
A concise solution:
incoming = [ {:date => 20090501, :width => 2},
{:date => 20090501, :height => 7},
{:date => 20090501, :depth => 3},
{:date => 20090502, :width => 4},
{:date => 20090502, :height => 6},
{:date => 20090502, :depth => 2},
]
temp = Hash.new {|hash,key| hash[key] = {}}
incoming.each {|row| temp[row[:date]].update(row)}
outgoing = temp.values.sort {|*rows| rows[0][:date] <=> rows[1][:date]}
The only thing that's at all tricky here is the Hash constructor, which allows you to supply a block that's called when you access a nonexistent key. So I have the Hash create an empty Hash for us to update with the values we're finding. Then I just use the date as the hash keys, sort the hash values by date and we're transformed.
Try this:
incoming = [ {:date => 20090501, :width => 2},
{:date => 20090501, :height => 7},
{:date => 20090501, :depth => 3},
{:date => 20090502, :width => 4},
{:date => 20090502, :height => 6},
{:date => 20090502, :depth => 2},
]
# Grouping by `:date`
temp = {}
incoming.each do |row|
if temp[row[:date]].nil?
temp[row[:date]] = []
end
temp[row[:date]] << row
end
# Merging it together
outcoming = []
temp.each_pair do |date, hashlist|
res = {}
hashlist.each do |hash|
res.merge!(hash)
end
outcoming << res
end
For information concerning the hash-members, see this page
When ordering is important, you must use jagged arrays:
incoming = [ {:date => 20090501, :width => 2},
{:date => 20090501, :height => 7},
{:date => 20090501, :depth => 3},
{:date => 20090502, :width => 4},
{:date => 20090502, :height => 6},
{:date => 20090502, :depth => 2},
]
# Grouping by `:date`
temp = {}
incoming.each do |row|
if temp[row[:date]].nil?
temp[row[:date]] = []
end
key = row[:date]
row.delete :date
temp[key] << row
end
# Merging it together
outcoming = []
temp.each_pair do |date, hashlist|
res = [:date, date]
hashlist.each do |hash|
hash.each_pair {|key, value| res << [key, value] }
end
outcoming << res
end
Related
I have an array containing hashes, like this:
[
{:id => 1, :week => 1, :year => 2014},
{:id => 2, :week => 2, :year => 2014},
{:id => 1, :week => 1, :year => 2015},
{:id => 2, :week => 5, :year => 2015},
]
What i need is a array of arrays, containing each two values:
The first value is a hash from the first array with year 2014, the second is a hash from the first array with year 2015, if it has the same week and id as the first hash.
If there is not hash with equal id and week from 2015, then the second value has to be nil, vice versa the first value is nil.
For the array above, the new array should look like:
[
[{:id => 1, :week => 1, :year => 2014}, {:id => 1, :week => 1, :year => 2015}],
[{:id => 2, :week => 2, :year => 2014}, nil],
[nil, {:id => 2, :week => 5, :year => 2015}],
]
-e-
my approach:
result = []
all_ids.each do |id|
all_weeks.each do |week|
v1 = array.select{ |v| v.id == id && v.week == week && v.year == 2014}
v2 = array.select{ |v| v.id == id && v.week == week && v.year == 2015}
v1 = v1.length == 1 ? v1.first : nil
v2 = v2.length == 1 ? v1.first : nil
result << [v1, v2]
end
end
this doesn't seem to be very efficient as i have to iterate the array multiple times
Use group_by to separate your hashes by week and id, then you just need to pad out the arrays that don't have two values:
array.group_by { |v| [v[:id], v[:week]] }.values.each do |a|
a.insert(2015 - a[0][:year], nil) if a.length == 1
end
Here is a way that is intended to emphasize readability (seriously).
Code
def convert(arr)
a14 = arr.select { |h| h[:year] == 2014 }
a15 = arr - a14
arr = a14.map do |h14|
i15 = a15.index { |h15| h14[:id]==h15[:id] && h14[:week]==h15[:week] }
[ h14, i15.nil? ? nil : a15.delete_at(i15) ]
end
arr.concat([nil].product(a15)) if a15.any?
end
If the value of :year can be other than 2014 or 2015, replace a15 = arr - a14 with:
a15 = arr.select { |h| h[:year] == 2015 }
Note that a15.delete_at(i15) serves a dual-function: it removes the value at index i15 from a15 and returns that value for inclusion in the tuple.
Example
arr = [ {:id => 1, :week => 1, :year => 2014},
{:id => 2, :week => 2, :year => 2014},
{:id => 1, :week => 1, :year => 2015},
{:id => 2, :week => 5, :year => 2015} ]
convert(arr)
#=> [[{:id=>1, :week=>1, :year=>2014}, {:id=>1, :week=>1, :year=>2015}],
# [{:id=>2, :week=>2, :year=>2014}, nil],
# [nil, {:id=>2, :week=>5, :year=>2015}]]
I have a hash:
a = { 21 => { 3 => {:x => 5, :y => 6}}}
I want to add another value to the key '21' so that the hash looks like this:
a = { 21 => { 3 => {:x => 5, :y => 6}, 4 => {:x => 8, :y => 7}}}
How can I do that?
You want to add an key-value pair to a hash (a[21]). a[21] will give you the inner hash object.
a = { 21 => { 3 => {:x => 5, :y => 6}}}
a[21]
# => {3=>{:x=>5, :y=>6}}
Associating key, values to the inner hash will solve your problem.
a[21][4] = {:x => 8, :y => 7}
a
# => {21=>{3=>{:x=>5, :y=>6}, 4=>{:x=>8, :y=>7}}}
Another way is:
a[21].update({ 4=>{:x => 8, :y => 7} })
a #=> {21=>{3=>{:x=>5, :y=>6}, 4=>{:x=>8, :y=>7}}}
I have a data structure in the following format:
data_hash = [
{ price: 1, count: 3 },
{ price: 2, count: 3 },
{ price: 3, count: 3 }
]
Is there an efficient way to get the values of :price as an array like [1,2,3]?
First, if you are using ruby < 1.9:
array = [
{:price => 1, :count => 3},
{:price => 2, :count => 3},
{:price => 3, :count => 3}
]
Then to get what you need:
array.map{|x| x[:price]}
There is a closed question that redirects here asking about handing map a Symbol to derive a key. This can be done using an Enumerable as a middle-man:
array = [
{:price => 1, :count => 3},
{:price => 2, :count => 3},
{:price => 3, :count => 3}
]
array.each.with_object(:price).map(&:[])
#=> [1, 2, 3]
Beyond being slightly more verbose and more difficult to understand, it also slower.
Benchmark.bm do |b|
b.report { 10000.times { array.map{|x| x[:price] } } }
b.report { 10000.times { array.each.with_object(:price).map(&:[]) } }
end
# user system total real
# 0.004816 0.000005 0.004821 ( 0.004816)
# 0.015723 0.000606 0.016329 ( 0.016334)
The input hash can have nests of any combo of Arrays and Hashes (AoA, AoH, HoH, and HoA). Flatting the hash elements to have the proper key and delimiter of _> is no problem.
However, I'm having trouble when an Array comes into the picture and I need to grab each element and stick it to the proper key while continuing to build the output. The final output should be a 1-D array of hashes with the only difference being the each array elements.
For example:
if the input hash is:
{:x => 333, :y => 13, :z => [1,2,{:zz => [40,50]},[10,20]], :a => {:o => "1", :p => "2"}}
The final result should be:
`[{:x => 333, :y => 13, :z => 1, :z_>zz => 40, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 1, :z_>zz => 50, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 2, :z_>zz => 40, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 2, :z_>zz => 50, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 10, :z_>zz => 40, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 10, :z_>zz => 50, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 20, :z_>zz => 40, :a_>o => 1, a_>p => 2},
{:x => 333, :y => 13, :z => 20, :z_>zz => 50, :a_>o => 1, a_>p => 2}]`
This is long and complicated, but at least it works:
my_hash = {:x => 333, :y => 13, :z => [1,2,{:zz => [40,50]},[10,20]], :a => {:o => "1", :p => "2"}}
# Create Recursive function to get values:
def advance_hash_flattener(input, parent=[])
case input
when Hash then input.flat_map{|key, val|
advance_hash_flattener(val, parent+[key])}
when Array then input.flat_map{|x| advance_hash_flattener(x, parent)}
else [parent.join('_>'), input]
end
end
#Some small transformations for the last step:
first_step = advance_hash_flattener(my_hash)
.each_slice(2)
.group_by{|x| x.first}
.map{|x| [x.first, x.last.map(&:last)]}
p first_step #=> [["x", [333]], ["y", [13]], ["z", [1, 2, 10, 20]], ["z_>zz", [40, 50]], ["a_>o", ["1"]], ["a_>p", ["2"]]]
# Create an array of Hashes:
final_array = [Hash.new]
first_step.each do |key,values|
new = []
values.each do |val|
if final_array.first.key?(key)
final_copy = final_array.map{|x|x.clone}
final_copy.each{|x| x[key] = val}
new += final_copy
else
final_array.each{|x| x[key] = val}
end
end
final_array += new
end
# result stored in final_array
Given a hash
z = [{'a' => 1, 'b' => 2}, {'a' => 3, 'b' => 4}, {'a' => 1, 'b' => 4}]
How do I search if the search parameter itself is a hash e.g.
{'a' => 3}
so that I can do something like z.find_by_hash({'a' => 3}) for it to return
{'a' => 3, 'b' => 4}
and also to get a collection of arrays like z.find_by_hash({'a' => 1}) for it to return
[{'a' => 1, 'b' => 2}, {'a' => 1, 'b => 4}]
Thanks
You can do this:
class Array
def find_by_hash(hash)
self.select { |h| h.includes_hash?(hash) }
end
end
class Hash
def includes_hash?(other)
included = true
other.each do |key, value|
included &= self[key] == other[key]
end
included
end
end
This extends Hash by a method to find out if a Hash includes another (with multiple keys and values). Array is extended with the method you wanted, but it's a more generic approach since you can do this:
ary = [ {:a => 1, :b => 3, :c => 5}, {:a => 5, :b => 2, :c => 8} ]
ary.find_by_hash( { :a => 1, :c => 5 } )
Note: You should also consider using Symbols for Hash keys since it is a common practice in Ruby, but my approach does also work with your keys.
z = [{'a' => 1, 'b' => 2}, {'a' => 3, 'b' => 4}, {'a' => 1, 'b' => 4}]
class Array
def search_hash(hash)
key = hash.keys.first
value = hash.values.first
select { |h| h[key] == value }
end
end
z.search_hash({'a' => 3}) #=> [{"a"=>3, "b"=>4}]
or you can type it without curly brackets
z.search_hash('a' => 3)
Basically what you need is something like this:
class Array
def find_by_hash(h)
h.collect_concat do |key, value|
self.select{|h| h[key] == value}
end
end
end
I didn't find an approach in API, so I think we have to implement it of our own.
(by the way, I think #megas' approach is better and more readable)
Code by TDD:
class SearchHashTest < Test::Unit::TestCase
def setup
#array_with_hash_elements = ArrayWithHashElements.new [{'a' => 1, 'b' => 2}, {'a' => 3, 'b' => 4}, {'a' => 1, 'b' => 4}]
end
def test_search_an_array_by_hash_parameter_and_return_single_hash
assert_equal( {'a' => 3, 'b' => 4}, #array_with_hash_elements.search({'a'=>3}) )
end
def test_search_an_array_by_hash_parameter_and_return_an_array
assert_equal( [{'a' => 1, 'b' => 2}, {'a'=> 1, 'b' => 4}], #array_with_hash_elements.search({'a'=>1}))
end
end
implemented code ( just for demo, not production)
class ArrayWithHashElements
def initialize some_array
#elements = some_array
end
def search( query_hash)
puts "search: #{query_hash.inspect}"
result = []
#elements.each do | array_element_in_hash_form|
query_hash.each_pair do | key, value |
if array_element_in_hash_form.has_key?(key) && array_element_in_hash_form[key] == value
puts "adding : #{array_element_in_hash_form.inspect} to result"
result << array_element_in_hash_form
end
end
result
end
return result.size != 1 ? result : result[0]
end
end
result:
sg552#siwei-moto:~/workspace/test$ ruby search_hash_test.rb
Loaded suite search_hash_test
Started
search: {"a"=>1}
adding : {"b"=>2, "a"=>1} to result
adding : {"b"=>4, "a"=>1} to result
.search: {"a"=>3}
adding : {"b"=>4, "a"=>3} to result
.
Finished in 0.000513 seconds.
2 tests, 2 assertions, 0 failures, 0 errors