Custom comparison operator for difference between arrays in Ruby - ruby

I have two arrays of some class. I want to find the difference between them. But I want it to be based on the value of a specific method call to each instance of this class, instead of the entire instance. Here is some example code where I use Hash for the class.
#!/usr/bin/ruby
require 'awesome_print'
class Hash
def <=> other
puts 'space ship'
self[:a] <=> other[:a]
end
def == other
puts 'double equal'
self[:a] == other[:a]
end
def > other
puts 'greater than'
self[:a] > other[:a]
end
def < other
puts 'less than'
self[:a] < other[:a]
end
def >= other
puts 'greater equal'
self[:a] >= other[:a]
end
def <= other
puts 'less equal'
self[:a] <= other[:a]
end
def eql? other
puts 'eql?'
self[:a].eql? other[:a]
end
def equal? other
puts 'equal?'
self[:a].equal? other[:a]
end
end
c = { a: 1, b: 2, c: 3}
d = { a: 2, b: 3, c: 4}
e1 = { a: 3, b: 4, c: 5}
e2 = { a: 3, b: 4, c: 5}
e3 = { a: 3, b: 5, c: 4}
f1 = { a: 4, b: 5, c: 6}
f2 = { a: 4, b: 5, c: 6}
f3 = { a: 4, b: 6, c: 5}
g = { a: 5, b: 6, c: 7}
h = { a: 6, b: 7, c: 8}
a = [c, d, e1, f1]
b = [e3, f3, g, h]
ap (a - b)
I expect to see 2 elements in the final array, but still seeing 4. Tried overriding all the various comparison operators for the class of each element, Hash in this case, and I can see some calls to the 'double equal', but it still doesn't have the proper effect. What am I doing wrong?

Array#- uses the eql? / hash protocol, just like Hash, Set and Array#uniq:
class Hash
def eql? other
puts 'eql?'
self[:a].eql? other[:a]
end
def hash
puts 'hash'
self[:a].hash
end
end
c = { a: 1, b: 2, c: 3}
d = { a: 2, b: 3, c: 4}
e1 = { a: 3, b: 4, c: 5}
e2 = { a: 3, b: 4, c: 5}
e3 = { a: 3, b: 5, c: 4}
f1 = { a: 4, b: 5, c: 6}
f2 = { a: 4, b: 5, c: 6}
f3 = { a: 4, b: 6, c: 5}
g = { a: 5, b: 6, c: 7}
h = { a: 6, b: 7, c: 8}
a = [c, d, e1, f1]
b = [e3, f3, g, h]
a - b
# => [{a: 1, b: 2, c: 3}, {a: 2, b: 3, c: 4}]

Related

Add previous value to each hash value

I have a hash with integer values:
h = {
a: 1,
b: 1,
c: 1,
d: 2,
e: 2,
}
I need to add 100 to the first value, and for the second value and on, I need to add the preceding value to the original value to get:
{
a: 101,
b: 102,
c: 103,
d: 105,
e: 107,
}
Is there a good way to achieve this?
You could use inject to calculate the total sum:
h = { a: 1, b: 1, c: 1, d: 2, e: 2}
h.inject(100) { |s, (k, v)| s + v }
#=> 107
And while doing so, you can also set the hash values to get an accumulated sum:
h.inject(100) { |s, (k, v)| h[k] = s + v }
h #=> {:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}
Immutable solution that does not modify the input:
h.each_with_object({sum: 100, outcome: {}}) do |(k, v), acc|
acc[:outcome][k] = acc[:sum] += v
end
#⇒ {:sum=>107, :outcome=>{:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}}
You can just keep track of the sum as an external variable:
sum = 100
h.transform_values{|v| sum += v} # => {:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}
Maybe this is not the most efficient solution, but it is definitely nice and readable.
accumulated_sum = 0
h.each do |key, value|
accumulated_sum += value
hash[key] = 100 + accumulated_sum
end

How to sum values inside array of hash with same keys

I want to sum the values of same keys like
arr = [{"69120090" => [1, 2, 3]}, {"69120090" => [4, 5, 6]}]
I need to result in:
result = [{"69120090" => [5, 7, 9]}]
Reduce by Hash#merge! with a block:
arr = [{"69120090"=> [1, 2, 3] }, {"69120090"=> [4, 5, 6] }]
arr.each_with_object({}) do |h, acc|
acc.merge!(h) { |_, v1, v2| v1.zip(v2).map(&:sum) }
end
#⇒ {"69120090"=>[5, 7, 9]}
The above accepts any number of hashes with any number of keys each.
Just to have another option, given the array:
arr = [{ a: [1, 2, 3], b:[8,9,0] }, { a: [4, 5, 6], c: [1,2,3] }, { b: [0,1,2], c: [1,2,3] } ]
You could write:
tmp = Hash.new{ |k,v| k[v] = [] }
arr.each { |h| h.each { |k,v| tmp[k] << v } }
tmp.transform_values { |k| k.transpose.map(&:sum) }
Which returns
tmp #=> {:a=>[5, 7, 9], :b=>[8, 10, 2], :c=>[2, 4, 6]}
As one liner:
(arr.each_with_object(Hash.new{ |k,v| k[v] = [] }) { |h, tmp| h.each { |k,v| tmp[k] << v } }).transform_values { |k| k.transpose.map(&:sum) }

easier and shorter way to sum values in a hash of hashes in ruby

Having a hash of hashes like this:
d = {a: {c: 1, d:3 }, b: {c: 2, d: 6}, ...}
What is the easier way to sum all values in c:?
d.values.map { |val| val[:c] }.reduce(&:+)
To explain:
d.values
=> [{:c=>1, :d=>3}, {:c=>2, :d=>6}]
d.values.map { |val| val[:c] }
=> [1, 2]
From this point you can use reduce(&:+) to get the sum, or if you're using rails (or have required active support), you can use Array#sum
reduce(&:+), by the way, is a shorthand for reduce { |memo, val| memo + val }
Just go over the hashes and sum their :c values.
d.values.sum { |h| h[:c] }
=> 3
Even shorter (from Sagar Pandya's comment):
d.sum { |_, v| v[:c] }
=> 3
If you wish to permit nested hashes of arbitrary depth you can use the following recursive method.
def sum_cees(h)
h.sum { |k,v| v.is_a?(Hash) ? sum_cees(v) : k == :c ? v : 0 }
end
sum_cees({ a: { c: 1, d:3 }, b: { d: { m: { c: 2, e: 6 } }, f: { c: 3} },
g: { c: 4 }, n: { r: 3 } })
#=> 10

What is the most idiomatic way to perform multiple tests in ruby?

I have the following logic:
some_array.each do |element|
if element[:apples] == another_hash[:apples] &&
element[:oranges] == another_hash[:oranges] &&
element[:pineapple] == another_hash[:pineapple]
match = element
break
end
end
I iterate through a list of key value pairs. If I can match the required keys (3 of 5), then I toss the element in a var for later use. If I find a match, I break out of the loop.
I am looking for the most idiomatic way to optimize this conditional. Thank you in advance.
How about:
match = some_array.find do |element|
[:apples, :oranges, :pinapple].all? {|key| element[key] == another_hash[key]}
end
If you want to select any element which has at least 3 matching keys from 5 keys given then:
match = some_array.find do |element|
element.keys.select {|key| element[key| == another_hash[key]}.size > 2
end
This is how I'd do it.
Code
def fruit_match(some_array, another_hash, fruit)
other_vals = another_hash.values_at(*fruit)
return nil if other_vals.include?(nil)
some_array.find { |h| h.values_at(*fruit) == other_vals }
end
Examples
some_array = [ { apple: 1, orange: 2, pineapple: 3, plum: 4 },
{ apple: 1, cherry: 7, pineapple: 6, plum: 2 },
{ apple: 6, cherry: 2, pineapple: 8, fig: 3 } ]
another_hash = { apple: 6, cherry: 4, pineapple: 8, quamquat: 5 }
fruit = [:apple, :pineapple]
fruit_match(some_array, another_hash, fruit)
#=> { :apple=>6, :cherry=>2, :pineapple=>8, :fig=>3 }
fruit = [:apple, :plum]
fruit_match(some_array, another_hash, fruit)
#=> nil
[Edit: I didn't notice the "3-5" matches until I saw #7stud's answer. Requiring the number of matches to fall within a given range is an interesting variation. Here's how I would address that requirement.
Code
def fruit_match(some_array, another_hash, fruit, limits)
other_vals = another_hash.values_at(*fruit)
some_array.select { |h| limits.cover?(h.values_at(*fruit)
.zip(other_vals)
.count {|e,o| e==o && e}) }
end
Example
some_array = [ { apple: 1, orange: 2, pineapple: 1, cherry: 1 },
{ apple: 2, cherry: 7, pineapple: 6, plum: 2 },
{ apple: 6, cherry: 1, pineapple: 8, fig: 3 },
{ apple: 1, banana: 2, pineapple: 1, fig: 3 } ]
another_hash = { apple: 1, cherry: 1, pineapple: 1, quamquat: 1 }
fruit = [:apple, :pineapple, :cherry]
limits = (1..2)
fruit_match(some_array, another_hash, fruit, limits)
#=> [{:apple=>6, :cherry=>1, :pineapple=>8, :fig=>3},
# {:apple=>1, :banana=>2, :pineapple=>1, :fig=>3}]
tidE]
If I can match the required keys (3 of 5)
I don't think any of the posted answers addresses that.
target_keys = %i[
apples
oranges
pineapples
strawberries
bananas
]
data = [
{beer: 0, apples: 1, oranges: 2, pineapples: 3, strawberries: 4, bananas: 5},
{beer: 1, apples: 6, oranges: 7, pineapples: 8, strawberries: 9, bananas: 10},
{beer: 2, apples: 6, oranges: 2, pineapples: 3, strawberries: 9, bananas: 10},
]
match_hash = {
apples: 6, oranges: 2, pineapples: 3, strawberries: 9, bananas: 10
}
required_matches = 3
required_values = match_hash.values_at(*target_keys).to_enum
found_match = nil
catch :done do
data.each do |hash|
found_values = hash.values_at(*target_keys).to_enum
match_count = 0
loop do
match_count += 1 if found_values.next == required_values.next
if match_count == required_matches
found_match = hash
throw :done
end
end
required_values.rewind
end
end
p found_match
--output:--
{:beer=>1, :apples=>6, :oranges=>7, :pineapple=>8, :strawberry=>9, :banana=>10
More readable version I could think is slice:
keys = [:apples, :oranges, :pinapple]
match = some_array.find {|e| e.slice( *keys ) == another_hash.slice( *keys )}
UPDATE
Slice is not a pure ruby method of Hash, it includes in Rails' ActiveSupport library.
If you don't want to be using Rails, you can just load Active Support.
Add active_support to your Gemfile and require "active_support/core_ext/hash/slice".
Or you could just paste the contents of slice.rb into your app somewhere. The URL can be found here.

ruby - group by repeating key of multiple hashes

I have the following array
t = [
{nil => 1, 10 => 2, 16 => 4, 5=> 10},
{nil => 9, 5 => 2, 17 => 3, 10 => 2},
{10 => 4, 5 => 9, 17 => 1}
]
how can I get this as result?
{nil => [1,9,0],10 => [2,2,4], 16 => [4,0,0], 5 => [10,2,9], 17=>[0,3,1]}
I've seen that I can use something like this
t.group_by{|h| h['key']}
but I'm not sure if I can put a regexp inside the brackets
Thanks in advance
Javier
EDIT:
Is just want to group by each key of each hash inside the array, if the key is not present then the value is 0 for that hash
How about this one for illegibility:
t = [
{nil => 1, 10 => 2, 16 => 4, 5=> 10},
{nil => 9, 5 => 2, 17 => 3, 10 => 2},
{10 => 4, 5 => 9, 17 => 1}
]
# Create hash of possible keys
keys = t.reduce({}) { |m, h| h.each_key { |k| m[k] = [] }; m }
# Iterate through array, for each hash, for each key, append the
# value if key is in hash or zero otherwise
t.reduce(keys) { |m, h| m.each_key { |k| m[k] << (h[k] || 0) }; m }
puts keys
#=> {nil=>[1, 9, 0], 10=>[2, 2, 4], 16=>[4, 0, 0], 5=>[10, 2, 9], 17=>[0, 3, 1]}
Not the most elegant code I've ever written, but it does the job and is easy to understand:
def jqq(a)
keys = []
result = {}
a.each do |h|
keys += h.keys
end
keys.uniq.each do |key|
result[key] = []
a.each do |h|
h.default = 0
result[key] << h[key]
end
end
result
end
t = [
{nil => 1, 10 => 2, 16 => 4, 5=> 10},
{nil => 9, 5 => 2, 17 => 3, 10 => 2},
{10 => 4, 5 => 9, 17 => 1}
]
puts jqq(t)
# {nil=>[1, 9, 0], 10=>[2, 2, 4], 16=>[4, 0, 0], 5=>[10, 2, 9], 17=>[0, 3, 1]}
I do not think there is any any function available
Just gave a try with hash
def do_my_work(data)
hash = {}
#get all keys first
arr.map{|m| m.keys}.flatten.uniq.each {|a| hash[a]=[]}
# Now iterate and fill the values
arr.each do |elm|
hash.each do |k,v|
hash[k] << (elm[k].nil? ? 0 : elm[k])
end
end
end
hash = do_my_work(t)
puts hash
# => {nil=>[1, 9, 0], 10=>[2, 2, 4], 16=>[4, 0, 0], 5=>[10, 2, 9], 17=>[0, 3, 1]}

Resources