Merge two arrays into a Hash - ruby-on-rails

My desired outcome is something like this:
{date: 12/02/2014, minutes: 36}
I'm scraping with Nokogiri using:
dates = doc.css('td:nth-child(3)')
minutes = doc.css('td:nth-child(10)')
Then I do some filtering and pushing results into arrays:
dates.each do |x|
if x.text.length == 10
date_array << x.text
end
end
minutes.each do |x|
minutes_array << x.text
end
How can I zip these two arrays together to create my desired outcome?
i've tried something like this, but it's not quite right (gives me {"2013-10-29"=>"32:14"} )
result = Hash[date_array.zip(minutes_array)]
or even something like this:
result = Hash[date_array.zip(minutes_array).map {|d, m| {:date => d, :minutes => m}}
but i get this error: wrong element type Hash at 163
i've also tinkered with .flatten but to no avail. Can anybody help?

assuming you have 2 equal length arrays x and y
x = [:key1, :key2, :key3]
y = [:value1, :value2, :value3]
z = {}
x.each_with_index { |key,index| z[key] = y[index] }
puts z
=> {:key1=>:value1, :key2=>:value2, :key3=>:value3}
is that what you are looking for?
then maybe this:
x = [:key1, :key2, :key3]
y = [:value1, :value2, :value3]
z = []
x.each_with_index { |key,index| z << { date: key, minutes: y[index]} }
puts z
{:date=>:key1, :minutes=>:value1}
{:date=>:key2, :minutes=>:value2}
{:date=>:key3, :minutes=>:value3}

Stealing from nPn (I can't comment on his answer because I've got no reputation )
Assuming you have
x = [ "date1", "date2", "date3"]
y = [ "time1", "time2", "time3"]
Then you can do:
z = []
x.each_with_index { |k, i| z << { date: k, time: y[i] } }
puts z
=> [ { date: "date1", time: "time1" },
{ date: "date2", time: "time2" },
{ date: "date3", time: "time3" } ]
Is this what you are looking for ?

You are trying to have the same key (date, minutes) for multiple values. You can instead have an array of hash for all those date-minute combos though, with this -
date.zip(minutes).reduce([]) { |memo, combo| memo << Hash[*[:date, :minutes].zip(combo).flatten] }
Here is how it looks -
2.1.5 :035 > date=["10/10,2010","11/10/2010","12/10/2010","13/10/2010","14/10/2010"]
=> ["10/10,2010", "11/10/2010", "12/10/2010", "13/10/2010", "14/10/2010"]
2.1.5 :036 > minutes = [10,20,30,40,50]
=> [10, 20, 30, 40, 50]
2.1.5 :037 > date.zip(minutes).reduce([]) { |memo, combo| memo << Hash[*[:date, :minutes].zip(combo).flatten] }
=> [{:date=>"10/10,2010", :minutes=>10}, {:date=>"11/10/2010", :minutes=>20}, {:date=>"12/10/2010", :minutes=>30}, {:date=>"13/10/2010", :minutes=>40}, {:date=>"14/10/2010", :minutes=>50}]
2.1.5 :038 >
Word of caution - you should really use a Struct, and then create an array of that Struct instances, instead of working on arrays of hashes like this.

If
dates = ["12/02/14", "6/03/14"]
minutes = [12, 19]
then if I've not misunderstood the question, it's just:
dates.zip(minutes).map { |d,m| {date: d, minutes: m} }
#=> [{:date=>"12/02/14", :minutes=>12}, {:date=>"6/03/14", :minutes=>19}]

Related

Hash/Ruby - Select last 4 values (to average) in hash

I have a hash, that I select all the data for a dashboard to display performance, since displaying the latest value isn't always helpful, I'm trying to select the last 4 values from a hash.
I have attempted the thing.last(4), but to no avail.
Code is below, essentially trying to display the last 4 from top_points, or average points.
Note: Ruby 1.9
metric.sort.each do |key, value|
top_point = { x: Time.parse(key).to_time.to_i, y: value['top_10'] }
top_points << top_point
average_point = { x: Time.parse(key).to_time.to_i, y: value['average'] }
average_points << average_point
end
The following uses Hash#select to avoid the need to convert the hash to an array, manipulate the array and then convert it back to a hash.
h = { "b"=>1, "d"=>6, "f"=>3, "e"=>1, "c"=>3, "a"=>7 }
sz = h.size
#=> 6
h.select { (sz -= 1) < 4 }
#=> {"f"=>3, "e"=>1, "c"=>3, "a"=>7}
Alternatively, if using Ruby 2.5+ one could use Hash#slice:
h.slice(*h.keys[2..-1])
#=> {"f"=>3, "e"=>1, "c"=>3, "a"=>7}
and if using Ruby 2.6+ one could employ an Endless range:
h.slice(*h.keys[2..])
#=> {"f"=>3, "e"=>1, "c"=>3, "a"=>7}
in order to get the last four elements of your hash, you should first map it as an array, get the indexes desired and then transform again the array into an hash.
For example:
2.2.1 :001 > hash = {a: 1, b: 2, c: 3, d: 4, e: 5}
=> {:a=>1, :b=>2, :c=>3, :d=>4, :e=>5}
2.2.1 :002 > hash.map{|h| h}[-4..-1].to_h
=> {:b=>2, :c=>3, :d=>4, :e=>5}
In your specific case, the code might look like this:
metric.sort.map{|h| h}[-4..-1].to_h.each do |key, value|
top_point = { x: Time.parse(key).to_time.to_i, y: value['top_10'] }
top_points << top_point
average_point = { x: Time.parse(key).to_time.to_i, y: value['average'] }
average_points << average_point
end
Another way to write it could be:
last_four_metrics = metric.sort.map{|h| h}[-4..-1].to_h
top_points = last_four_metrics.map{|k, v| { x: Time.parse(k).to_time.to_i, y: v['top_10'] }}
average_points = last_four_metrics.map{|k, v| { x: Time.parse(k).to_time.to_i, y: v['average'] }}
Update: compatibility with Ruby 1.9
last_four_metrics = Hash[ metric.sort.map{|h| h}[-4..-1] ]
top_points = last_four_metrics.map{|k, v| { x: Time.parse(k).to_time.to_i, y: v['top_10'] }}
average_points = last_four_metrics.map{|k, v| { x: Time.parse(k).to_time.to_i, y: v['average'] }}
metrics.sort.last(4).to_h
Will give you a hash with the last four elements.
Assuming you didn't originally want to sort, use the same idea:
metrics.to_a.last(4).to_h
Update: Given you added the 1.9 restriction and Array#to_h comes from 2.1 onward, you can replace x.to_h with Hash[x].
Or if you don't need the hash and want to iterate over the key/value pairs, omitting the .to_h part and continuing with .each do |key, value| will pretty much do the same.
You can convert the hash to a 2-element array, select the last for elements and convert back to hash:
top_points = {}
(1..10).each { |i| top_points[i] = i*2 }
# => top_points == {1=>2, 2=>4, 3=>6, 4=>8, 5=>10, 6=>12, 7=>14, 8=>16, 9=>18, 10=>20}
Hash[top_points.to_a[-4..-1]]
# => {7=>14, 8=>16, 9=>18, 10=>20}
You need to use ruby 1.9+ for this to work (since this version it keeps hash keys in the given order).

Calculating avg. in deeply nested hash and then group by another field

I'm trying to work out the most efficient way to loop through some deeply nested data, find the average of the values and return a new hash with the data grouped by the date.
The raw data looks like this:
[
client_id: 2,
date: "2015-11-14",
txbps: {
"22"=>{
"43"=>17870.153846153848,
"44"=>15117.866666666667
}
},
client_id: 1,
date: "2015-11-14",
txbps: {
"22"=>{
"43"=>38113.846153846156,
"44"=>33032.0
}
},
client_id: 4,
date: "2015-11-14",
txbps: {
"22"=>{
"43"=>299960.0,
"44"=>334182.4
}
},
]
I have about 10,000,000 of these to loop through so I'm a little worried about performance.
The end result, needs to look like this. The vals need to be the average of the txbps:
[
{
date: "2015-11-14",
avg: 178730.153846153848
},
{
date: "2015-11-15",
avg: 123987.192873978987
},
{
date: "2015-11-16",
avg: 126335.982123876283
}
]
I've tried this to start:
results.map { |val| val["txbps"].values.map { |a| a.values.sum } }
But that's giving me this:
[[5211174.189281798, 25998.222222222223], [435932.442835184, 56051.555555555555], [5718452.806735582, 321299.55555555556]]
And I just can't figure out how to get it done. I can't find any good references online either.
I also tried to group by the date first:
res.map { |date, values| values.map { |client| client["txbps"].map { |tx,a| { date: date, client_id: client[':'], tx: (a.values.inject(:+) / a.size).to_i } } } }.flatten
[
{
: date=>"2015-11-14",
: client_id=>"2",
: tx=>306539
},
{
: date=>"2015-11-14",
: client_id=>"2",
: tx=>25998
},
{
: date=>"2015-11-14",
: client_id=>"2",
: tx=>25643
},
{
: date=>"2015-11-14",
: client_id=>"2",
: tx=>56051
},
{
: date=>"2015-11-14",
: client_id=>"1",
: tx=>336379
},
{
: date=>"2015-11-14",
: client_id=>"1",
: tx=>321299
}
]
If possible, how can I do this in a single run.
---- EDIT ----
Got a little bit further:
res.map { |a,b|
{
date: a[:date], val: a["txbps"].values.map { |k,v|
k.values.sum / k.size
}.first
}
}.
group_by { |el| el[:date] }.map { |date,list|
{
key: date, val: list.map { |elem| elem[:val] }.reduce(:+) / list.size
}
}
But that's epic - is there a faster, simpler way??
Try #inject
Like .map, It's a way of converting a enumerable (list, hash, pretty much anything you can loop in Ruby) into a different object. Compared to .map, it's a lot more flexible, which is super helpful. Sadly, this comes with a cost of the method being super hard to wrap your head around. I think Drew Olson explains it best in his answer.
You can think of the first block argument as an accumulator: the result of each run of the block is stored in the accumulator and then passed to the next execution of the block. In the case of the code shown above, you are defaulting the accumulator, result, to 0. Each run of the block adds the given number to the current total and then stores the result back into the accumulator. The next block call has this new value, adds to it, stores it again, and repeats.
Examples:
To sum all the numbers in an array (with #inject), you can do this:
array = [5,10,7,8]
# |- Initial Value
array.inject(0) { |sum, n| sum + n } #=> 30
# |- You return the new value for the accumulator in this block.
To find the average of an array of numbers, you can find a sum, and then divide. If you divide the num variable inside the inject function ({|sum, num| sum + (num / array.size)}), you multiply the amount of calculations you will have to do.
array = [5,10,7,8]
array.inject(0.0) { |sum, num| sum + num } / array.size #=> 7.5
Method
If creating methods on classes is your style, you can define a method on the Array class (from John Feminella's answer). Put this code somewhere before you need to find the sum or mean of an array:
class Array
def sum
inject(0.0) { |result, el| result + el }
end
def mean
sum / size
end
end
And then
array = [5,10,7,8].sum #=> 30
array = [5,10,7,8].mean #=> 7.5
Gem
If you like putting code in black boxes, or really precious minerals, then you can use the average gem by fegoa89: gem install average. It also has support for the #mode and #median
[5,10,7,8].mean #=> 7.5
Solution:
Assuming your objects look like this:
data = [
{
date: "2015-11-14",
...
txbps: {...},
},
{
date: "2015-11-14",
...
txbps: {...},
},
...
]
This code does what you need, but it's somewhat complex.
class Array
def sum
inject(0.0) { |result, el| result + el }
end
def mean
sum / size
end
end
data = (data.inject({}) do |hash, item|
this = (item[:txbps].values.map {|i| i.values}).flatten # Get values of values of `txbps`
hash[item[:date]] = (hash[item[:date]] || []) + this # If a list already exists for this date, use it, otherwise create a new list, and add the info we created above.
hash # Return the hash for future use
end).map do |day, value|
{date: day, avg: value.mean} # Clean data
end
will merge your objects into arrays grouped by date:
{:date=>"2015-11-14", :avg=>123046.04444444446}
Data Structure
I assume your input data is an array of hashes. For example:
arr = [
{
client_id: 2,
date: "2015-11-14",
txbps: {
"22"=>{
"43"=>17870.15,
"44"=>15117.86
}
}
},
{
client_id: 1,
date: "2015-11-15",
txbps: {
"22"=>{
"43"=>38113.84,
"44"=>33032.03,
}
}
},
{
client_id: 4,
date: "2015-11-14",
txbps: {
"22"=>{
"43"=>299960.0,
"44"=>334182.4
}
}
},
{
client_id: 3,
date: "2015-11-15",
txbps: {
"22"=>{
"43"=>17870.15,
"44"=>15117.86
}
}
}
]
Code
Based on my understanding of the problem, you can compute averages as follows:
def averages(arr)
h = arr.each_with_object(Hash.new { |h,k| h[k] = [] }) { |g,h|
g[:txbps].values.each { |f| h[g[:date]].concat(f.values) } }
h.merge(h) { |_,v| (v.reduce(:+)/(v.size.to_f)).round(2) }
end
Example
For arr above:
avgs = averages(arr)
#=> {"2015-11-14"=>166782.6, "2015-11-15"=>26033.47}
The value of the hash h in the first line of the method was:
{"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
"2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]}
Convert hash returned by averages to desired array of hashes
avgs is not in the form of the output desired. It's a simple matter to do the conversion, but you might consider leaving the hash output in this format. The conversion is simply:
avgs.map { |d,avg| { date: d, avg: avg } }
#=> [{:date=>"2015-11-14", :avg=>166782.6},
# {:date=>"2015-11-15", :avg=>26033.47}]
Explanation
Rather than explain in detail how the method works, I will instead give an alternative form of the method does exactly the same thing, but in a more verbose and slightly less Ruby-like way. I've also included the conversion of the hash to an array of hashes at the end:
def averages(arr)
h = {}
arr.each do |g|
vals = g[:txbps].values
vals.each do |f|
date = g[:date]
h[date] = [] unless h.key?(date)
h[date].concat(f.values)
end
end
keys = h.keys
keys.each do |k|
val = h[k]
h[k] = (val.reduce(:+)/(val.size.to_f)).round(2)
end
h.map { |d,avg| { date: d, avg: avg } }
end
Now let me insert some puts statements to print out various intermediate values in the calculations, to help explain what's going on:
def averages(arr)
h = {}
arr.each do |g|
puts "g=#{g}"
vals = g[:txbps].values
puts "vals=#{vals}"
vals.each do |f|
puts " f=#{f}"
date = g[:date]
puts " date=#{date}"
h[date] = [] unless h.key?(date)
puts " before concat, h=#{h}"
h[date].concat(f.values)
puts " after concat, h=#{h}"
end
puts
end
puts "h=#{h}"
keys = h.keys
puts "keys=#{keys}"
keys.each do |k|
val = h[k]
puts " k=#{k}, val=#{val}"
puts " val.reduce(:+)=#{val.reduce(:+)}"
puts " val.size.to_f=#{val.size.to_f}"
h[k] = (val.reduce(:+)/(val.size.to_f)).round(2)
puts " h[#{k}]=#{h[k]}"
puts
end
h.map { |d,avg| { date: d, avg: avg } }
end
Execute averages once more:
averages(arr)
g={:client_id=>2, :date=>"2015-11-14", :txbps=>{"22"=>{"43"=>17870.15, "44"=>15117.86}}}
vals=[{"43"=>17870.15, "44"=>15117.86}]
f={"43"=>17870.15, "44"=>15117.86}
date=2015-11-14
before concat, h={"2015-11-14"=>[]}
after concat, h={"2015-11-14"=>[17870.15, 15117.86]}
g={:client_id=>1, :date=>"2015-11-15", :txbps=>{"22"=>{"43"=>38113.84, "44"=>33032.03}}}
vals=[{"43"=>38113.84, "44"=>33032.03}]
f={"43"=>38113.84, "44"=>33032.03}
date=2015-11-15
before concat, h={"2015-11-14"=>[17870.15, 15117.86], "2015-11-15"=>[]}
after concat, h={"2015-11-14"=>[17870.15, 15117.86], "2015-11-15"=>[38113.84, 33032.03]}
g={:client_id=>4, :date=>"2015-11-14", :txbps=>{"22"=>{"43"=>299960.0, "44"=>334182.4}}}
vals=[{"43"=>299960.0, "44"=>334182.4}]
f={"43"=>299960.0, "44"=>334182.4}
date=2015-11-14
before concat, h={"2015-11-14"=>[17870.15, 15117.86],
"2015-11-15"=>[38113.84, 33032.03]}
after concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
"2015-11-15"=>[38113.84, 33032.03]}
g={:client_id=>3, :date=>"2015-11-15", :txbps=>{"22"=>{"43"=>17870.15, "44"=>15117.86}}}
vals=[{"43"=>17870.15, "44"=>15117.86}]
f={"43"=>17870.15, "44"=>15117.86}
date=2015-11-15
before concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
"2015-11-15"=>[38113.84, 33032.03]}
after concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
"2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]}
h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
"2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]}
keys=["2015-11-14", "2015-11-15"]
k=2015-11-14, val=[17870.15, 15117.86, 299960.0, 334182.4]
val.reduce(:+)=667130.41
val.size.to_f=4.0
h[2015-11-14]=166782.6
k=2015-11-15, val=[38113.84, 33032.03, 17870.15, 15117.86]
val.reduce(:+)=104133.87999999999
val.size.to_f=4.0
h[2015-11-15]=26033.47
#=> [{:date=>"2015-11-14", :avg=>166782.6},
# {:date=>"2015-11-15", :avg=>26033.47}]

Crop hash structure: Ruby on rails

I want to delete data from a hash table using a specific range of values.
Example:
hash = { t:1, y:9, k:10, a:30, b:40, c:50, d:80, e:60, z:100, l:3, n:9, f:20 }
Given an array of numbers: array = [10, 30, 40, 50, 80, 60, 100] (is exactly the range of the center of the table)
I want the result to be:
hash: {k:10, a:30, b:40, c:50, d:80, e:60, z:100}
Notes that never eliminated data that is in the middle of the structure.
Look at the select method.
[6] pry(main)> hash.select { |k,v| array.include?(v) }
=> {:k=>10, :a=>30, :b=>40, :c=>50, :d=>80, :e=>60, :z=>100}
results = {}
hash.each { |k, v| results[k] = v if array.include?(v) }
puts results
output:
{:k=>10, :a=>30, :b=>40, :c=>50, :d=>80, :e=>60, :z=>100}
array.each_with_object({}){|e, h| h[hash.key(e)] = e if hash.value?(e)}

How do you double number in ruby for an iterator

Need to double each value in my array. I know double is not a command, but not sure what else to use.
odds = [1,3,5,7,9]
array.each do |x|
x += double
print "#{x}"
end
Use Array#map to create a new array
odds = [1,3,5,7,9]
arr = odds.map{|x| x*2}
arr.inspect
# => [2,6,10,14,18]
To modify the same array use Array#map!
odds = [1,3,5,7,9]
odds.map!{|x| x*2}
odds.inspect
# => [2,6,10,14,18]
Do you mean 'double' as in multiply by 2, or double as in duplicate?
array = [1,3,5,7,9]
array.each { |x| print "#{x*2}"; }
But you probably want either a new array, or to map your existing array,
result = []
result = array.map { |x| x*2 }
#or
result = array.map! { |x| x*2 }
Here is an example of duplicate,
result = []
array.map { |x| result << x; result << x; } #duplicate
see here: http://www.natontesting.com/2011/01/01/rubys-each-select-and-reject-methods/
Doing the Simplest Thing Possible
The simplest thing to do is to simply iterate over your array with Array#map and Kernel#p. For example:
odds = [1, 3, 5, 7, 9]
odds.map { |i| p i*2 }
Defining a Custom Method
If you need more control, you can create a custom method that handles a block or returns an enumerator. For example:
def double *array
array.flatten!
block_given? ? array.map { |i| yield i*2 } : array.map { |i| i*2 }.to_enum
end
enumerator = double 1, 2, 3
#=> #<Enumerator: ...>
enumerator.each { |i| p i }
#=> [2, 4, 6]
double(1, 2, 3) { |i| p i }
#=> [2, 4, 6]
This is probably overkill for your use case, but it's a useful technique to know if you want to work with enumerators and blocks. Hope it helps!
The way that I have found to do it is to times / equal it by 2. For example:
odds = [1,3,5,7,9]
odds.each do |x|
x *= 2
print x
end

Creating a new hash with default keys

I want to create a hash with an index that comes from an array.
ary = ["a", "b", "c"]
h = Hash.new(ary.each{|a| h[a] = 0})
My goal is to start with a hash like this:
h = {"a"=>0, "b"=>0, "c"=>0}
so that later when the hash has changed I can reset it with h.default
Unfortunately the way I'm setting up the hash is not working... any ideas?
You should instantiate your hash h first, and then fill it with the contents of the array:
h = {}
ary = ["a", "b", "c"]
ary.each{|a| h[a] = 0}
Use the default value feature for the hash
h = Hash.new(0)
h["a"] # => 0
In this approach, the key is not set.
h.key?("a") # => false
Other approach is to set the missing key when accessed.
h = Hash.new {|h, k| h[k] = 0}
h["a"] # => 0
h.key?("a") # => true
Even in this approach, the operations like key? will fail if you haven't accessed the key before.
h.key?("b") # => false
h["b"] # => 0
h.key?("b") # => true
You can always resort to brute force, which has the least boundary conditions.
h = Hash.new.tap {|h| ["a", "b", "c"].each{|k| h[k] = 0}}
h.key?("b") # => true
h["b"] # => 0
You can do it like this where you expand a list into zero-initialized values:
list = %w[ a b c ]
hash = Hash[list.collect { |i| [ i, 0 ] }]
You can also make a Hash that simply has a default value of 0 for any given key:
hash = Hash.new { |h, k| h[k] = 0 }
Any new key referenced will be pre-initialized to the default value and this will avoid having to initialize the whole hash.
This may not be the most efficient way, but I always appreciate one-liners that reveal a little more about Ruby's versatility:
h = Hash[['a', 'b', 'c'].collect { |v| [v, 0] }]
Or another one-liner that does the same thing:
h = ['a', 'b', 'c'].inject({}) {|h, v| h[v] = 0; h }
By the way, from a performance standpoint, the one-liners run about 80% of the speed of:
h = {}
ary = ['a','b','c']
ary.each { |a| h[a]=0 }
Rails 6 added index_with on Enumerable module. This will help in creating a hash from an enumerator with default or fetched values.
ary = %w[a b c]
hash = ary.index_with(0) # => {"a"=>0, "b"=>0, "c"=>0}
Another option is to use the Enum#inject method which I'm a fan of for its cleanliness. I haven't benchmarked it compared to the other options though.
h = ary.inject({}) {|hash, key| hash[key] = 0; hash}
Alternate way of having a hash with the keys actually added
Hash[[:a, :b, :c].zip([])] # => {:a=>nil, :b=>nil, :c=>nil}
Hash[[:a, :b, :c].zip(Array.new(3, 0))] # => {:a=>0, :b=>0, :c=>0}

Resources