Comparing a hashes value against array in order - arrays

If I have the following hash and array
hash = {'i' => 'i', 'av' => 'av', 'deviceName' => 'Genymotionvbox86p'}
array = ['i', 'av', 'Genymotionvbox86p']
How could I compare that each item in the array matches the hashes value in the same order
So far I have
array.each do |value|
hash.each do |k, v|
expect(v).to eq(value), "expected #{k} to equal #{v}, instead got #{value}"
end
end
This is failing as I get
expected av to equal av, instead got i (RSpec::Expectations::ExpectationNotMetError)
I'm not quite there yet and imagine that a loop within a loop is not the best thing to do either?
I would like to know how to efficiently approach this.

The reason this fails is because you compare every array value with every hash value. To solve this, you can take advantage of the fact that two arrays arrays are equal if all their values in order are equal:
expect(array).to eq hash.values

If you would really want to compare item-by-item, you rightfully noticed that a loop within a loop is not the way to go. You need a single loop to iterate both structures.
For that, you can, for example, use zip, to combine hash and array:
hash.zip(array).each do |(hash_key, hash_value), array_item|
expect(hash_value).to eq array_item
end
or you can resort to using an index:
hash.each_with_index do |(k, v), i|
expect(v).to eq array[i]
end

How could I compare that each item in the array matches the hashes
value in the same order
how about this?
> array == hash.values
#=> true
> array = ["i", "Genymotionvbox86p", "av"] # change the order
> array == hash.values
#=> false

Related

Array of hashes whose key is an array containing ranges

I need to map a number of IDs and ranges of IDs to single values. I would like to store the whole structure in one object.
Is the following a sensible way to do this? What is the best way to search the keys by a single integer value to return the corresponding value?
large array = [
{[3110..3220, 10200, 43680] => 'A0300'},
{[5200, 7100..8990, 9100..9900] => 'B0400'},
{[17110..18121, 20160, 2210..22290] => 'C0600'}
]
Reasonable format to store the data would be a plain old simple hash:
input = { [3110..3220, 10200, 43680] => 'A0300',
[5200, 7100..8990, 9100..9900] => 'B0400',
[17110..18121, 20160, 2210..22290] => 'C0600' }
To lookup the element one might use case-equal aka triple-equal. Luckily it’s implemented for integers as well:
value = 7300
result = input.detect { |k, _| k.any? { |r| r === value } }
result ? result.last : nil
#⇒ "B0400"
No. There is no need to separate each key-value pair into a different hash as you did. Furthermore, you are using the hash in a wrong way. A hash key-value need not be one-to-one. It is better to have something like this:
{
3110..3220 => 'A0300',
10200 => 'A0300',
43680 => 'A0300',
5200 => 'B0400',
7100..8990 => 'B0400',
9100..9900 => 'B0400',
17110..18121 => 'C0600',
20160 => 'C0600',
2210..22290 => 'C0600',
}
But my guess is that you are actually asking a X-Y question, and my answer above is still not the best solution for you.
If the values were hardwired, one could use a case statement.
def look_it_up(n)
case n
when 7100..8990, 9100..9900, 5200
'B0400'
when 17110..18121, 22100..22290, 20160
'C0600'
when 3110..3220, 10200, 43680
'A0300'
else
nil
end
end
look_it_up 10200 #=> "A0300"
look_it_up 9135 #=> "B0400"
look_it_up 22100 #=> "C0600"
look_it_up 3079 #=> nil
Assuming n is somewhat uniformly distributed, I have, for reasons of efficiency, ordered the when clauses by decreasing numbers of numbers included, and within each when clause, I put the ranges first and ordered those ranges by decreasing size.
If the values are not necessarily hardwired, one could use find/detect, as #mudasobwa has done.
If many lookups were to be performed, I would suggest constructing a hash whose keys are integers.
def init_arr(*args)
args.each do |*a, value|
a.each do |e|
if e.is_a?(Range)
e.each { |n| #h[n] = value }
else
#h[e] = value
end
end
end
end
Then for any n, #h[n] gives its value. For example,
#h = {}
init_arr [1..4, 8..10, 11, "cat"]
#h #=> {1=>"cat", 2=>"cat", 3=>"cat", 4=>"cat", 8=>"cat", 9=>"cat",
# 10=>"cat", 11=>"cat"}
init_arr [13..14, 7, "dog"]
#h #=> {1=>"cat", 2=>"cat", 3=>"cat", 4=>"cat", 8=>"cat", 9=>"cat",
# 10=>"cat", 11=>"cat", 13=>"dog", 14=>"dog", 7=>"dog"}
Then (say),
#h[13]
#=> "dog"
#h[11]
#=> "cat"
#h[12]
#=> nil
Naturally, this assumes #h.size is manageable.
One could alternatively construct a look-up array, say #arr, which should be faster than using a hash (though it might consume much more memory than would a hash). #arr[n] would return the value for n or nil if there is no corresponding value. If some values were negative, or if the smallest n were quite large, one would instead write #arr[n+#offset], where #offset is the obvious value.
It is pointless to use arrays and/or ranges as hash key, when you want to look up the data given a single value. The advantage of a hash is that, given a key, you can find a value quickly. In your case, you would have to traverse the hash sequentially to find the entry. In this case, it would make more sense to use an arrays of pairs, the first component describing the range and the second component describing the ID.
If you want to use a hash, you should make a corresponding entry for each possible integer value.
If you know that the integer values can not be higher than a certain size, you could create a sparse array, where the integer value is the index position, and the ID is the content of the array cell. This is fastest, but of course takes up some space.

Ruby - elegantly flatten an array but don't ignore empty sub-arrays

Using .flatten is a handy little trick to take an array of sub-arrays and turn it into a single array.
For example: [[1,3],2,[5,8]].flatten => [1,3,2,5,8]
You can even include nil [1,[2,nil],3].flatten will result in [1,2,nil,3].
This kind of method is very useful when nesting a .map method, but how would you account for an empty sub-array? For example: [1,[2,3],[],4].flatten would return [1,2,3,4]... but what if I need to keep track of the empty sub array maybe turn the result into [1,2,3,0,4] or [1,2,3,nil,4]
Is there any elegant way to do this? Or would I need to write some method to iterate through each individual sub-array and check it one by one?
If you don't need to recursively check nested sub-arrays:
[1,[2,3],[],4].map { |a| a == [] ? nil : a }.flatten
First map the empty arrays into nils, then flatten
[1,2,[1,2,3],[]].map{|x| if x.is_a? Array and x.empty? then nil else x end}.flatten

Convert Array to Array of Arrays

I have an array like this one:
array = ['john', 'jennifer', 'kristen', 'ted']
I would like to convert it to an array of arrays of k elements.
For example, if k = 2 the result should be:
[['john', 'jennifer'], ['kristen', 'ted']]
Is it possible to do so in one line?
each_slice might help:
array.each_slice(2).to_a
#=> [["john", "jennifer"], ["kristen", "ted"]]
If you want to create two arrays from one with a predicate (an expression which evaluates to either true or false), I would recommend partition
array.partition{ |name| name[0] == 'j' }
#=> [["john", "jennifer"], ["kristen", "ted"]]

Add key value pair to Array of Hashes when unique Id's match

I have two arrays of hashes
sent_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY"},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
active_array = [{:price=>39.99, :asin1=>"B00ND80WKY"},
{:price=>7.99, :asin1=>"B00YSN9QOG"},
{:price=>10, :asin1=>"B00HXZLB4E"}]
I want to loop through sent_array, and find where the value in :asin is equal to the value in :asin1 in active_array, then copy the key & value of :price to sent_array. Resulting in this:
final_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I tried this, but I get a TypeError - no implicit conversion of Symbol into Integer (TypeError)
sent_array.each do |x|
x.detect { |key, value|
if value == active_array[:asin1]
x[:price] << active_array[:price]
end
}
end
For reasons of both efficiency and readability, it makes sense to first construct a lookup hash on active_array:
h = active_array.each_with_object({}) { |g,h| h[g[:asin1]] = g[:price] }
#=> {"B00ND80WKY"=>39.99, "B00YSN9QOG"=>7.99, "B00HXZLB4E"=>10}
We now merely step through sent_array, updating the hashes:
sent_array.each { |g| g[:price] = h[g[:asin]] if h.key?(g[:asin]) }
#=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
# {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
# {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
Retrieving a key-value pair from a hash (h) is much faster, of course, than searching for a key-value pair in an array of hashes.
This does the trick. Iterate over your sent array and attempt to find a record in your active_array that has that :asin. If you find something, set the price and you are done.
Your code I believe used detect/find incorrectly. What you want out of that method is the hash that matches and then do something with that. You were trying to do everything inside of detect.
sent_array.each do |sent|
item = active_array.find{ |i| i.has_value? sent[:asin] }
sent[:price] = item[:price] if item
end
=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99}, {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I am assuming second element of both sent_array and active_array has B00WTEF9FG as asin and asin1 respectively. (seeing your final result)
Now:
a = active_array.group_by{|a| a[:asin1]}
b = sent_array.group_by{|a| a[:asin]}
a.map { |k,v|
v[0].merge(b[k][0])
}
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00WTEF9FG", :sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Why were you getting TypeError?
You are doing active_array[:asin1]. Remember active_array itself is an Array. Unless you iterate over it, you cannot look for keys.
Another issue with your approach is, you are using Hash#detect
find is implemented in terms of each. And each, when called on a
Hash, returns key-value pairs in form of arrays with 2 elements
each. That's why find returns an array.
source
Same is true for detect. So x.detect { |key, value| .. } is not going to work as you are expecting it to.
Solution without assumption
a.map { |k,v|
b[k] ? v[0].merge(b[k][0]) : v[0]
}.compact
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00YSN9QOG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Here since asin1 => "B00ND80WKY" has no match, it cannot get sellersku from other hash.

Modify hashes in an array based on another array

I have two arrays like this:
a = [{'one'=>1, 'two'=>2},{'uno'=>1, 'dos'=>2}]
b = ['english', 'spanish']
I need to add a key-value pair to each hash in a to get this:
a = [{'one'=>1, 'two'=>2, 'language'=>'english'},{'uno'=>1, 'dos'=>2, 'language'=>'spanish'}]
I attempted this:
(0..a.length).each {|c| a[c]['language']=b[c]}
and it does not work. With this:
a[1]['language']=b[1]
(0..a.length).each {|c| puts c}
an error is shown:
NoMethodError (undefined method '[]=' for nil:NilClass)
How can I fix this?
a.zip(b){|h, v| h["language"] = v}
a # => [
# {"one"=>1, "two"=>2, "language"=>"english"},
# {"uno"=>1, "dos"=>2, "language"=>"spanish"}
# ]
When the each iterator over your Range reaches the last element (i.e. a.length), you will attempt to access a nonexisting element of a.
In your example, a.length is 2, so on the last iteration of your each, you will attempt to access a[2], which doesn't exist. (a only contains 2 elements wich indices 0 and 1.) a[2] evaluates to nil, so you will now attempt to call nil['language']=b[2], which is syntactic sugar for nil.[]=('language', b[2]), and since nil doesn't have a []= method, you get a NoMethodError.
The immediate fix is to not iterate off the end of a, by using an exclusive Range:
(0...a.length).each {|c| a[c]['language'] = b[c] }
By the way, the code you posted:
(0..a.length).each {|c| puts c }
should clearly have shown you that you iterate till 2 instead of 1.
That's only the immediate fix, however. The real fix is to simply never iterate over a datastructure manually. That's what iterators are for.
Something like this, where Ruby will keep track of the index for you:
a.each_with_index do |hsh, i| hsh['language'] = b[i] end
Or, without fiddling with indices at all:
a.zip(b.zip(['language'].cycle).map(&:reverse).map(&Array.method(:[])).map(&:to_h)).map {|x, y| x.merge!(y) }
[Note: this last one doesn't mutate the original Arrays and Hashes unlike the other ones.]
The problem you're having is that your (0..a.length) is inclusive. a.length = 2 so you want to modify it to be 0...a.length which is exclusive.
On a side note, you could use Array#each_with_index like this so you don't have to worry about the length and so on.
a.each_with_index do |hash, index|
hash['language'] = b[index]
end
Here is another method you could use
b.each_with_index.with_object(a) do |(lang,i),obj|
obj[i]["language"] = lang
obj
end
#=>[
{"one"=>1, "two"=>2, "language"=>"english"},
{"uno"=>1, "dos"=>2, "language"=>"spanish"}
]
What this does is creates an Enumerator for b with [element,index] then it calls with_object using a as the object. It then iterates over the Enumerator passing in each language and its index along with the a object. It then uses the index from b to find the proper index in a and adds a language key to the hash that is equal to the language.
Please know this is a destructive method where the objects in a will mutate during the process. You could make it non destructive using with_object(a.map(&:dup)) this will dup the hashes in a and the originals will remain untouched.
All that being said I think YAML would be better suited for a task like this but I am not sure what your constraints are. As an example:
yml = <<YML
-
one: 1
two: 2
language: "english"
-
uno: 1
dos: 2
language: "spanish"
YML
require 'yaml'
YAML.load(yml)
#=>[
{"one"=>1, "two"=>2, "language"=>"english"},
{"uno"=>1, "dos"=>2, "language"=>"spanish"}
]
Although using YAML I would change the structure for numbers to be more like language => Array of numbers by index e.g. {"english" => ["zero","one","two"]}. That way you can can access them like ["english"][0] #=> "zero"

Resources