Array of hashes whose key is an array containing ranges - arrays

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.

Related

In Ruby, during iteration of an array, how do I send multiple array elements as values for one specific hash key?

I know how to iterate through an array and I know how to create hash keys and values. I do not know how to create an array for the value and pass it multiple elements. My desired value for hash below is:
{'abc' => [1, 2, 3] , 'def' => [4,5,6,7]}
How would I achieve this hash, while iterating through array a and b below using each?
a = [1,2,3,4,5,6,7]
c = [1,2,3]
b = ['abc', 'def']
hash = {}
From your guidelines given in the comment:
While iterating through array a, if the element of iteration is included in array c, it is passed to the array value within key 'abc'. Otherwise, it is passed to other array value in key 'def'
You can do this:
hash = {}
hash['abc'] = a.select { |x| c.include?(x) }
hash['def'] = a.reject{ |x| c.include?(x) }
See Enumerable#select and Enumerable#reject. Also can take a look at Enumerable#partition which would be another good choice here, where you want to split an array into two arrays based on some condition:
in_a, not_in_a = a.partition { |x| c.include?(x) }
hash = { 'abc' => in_a, 'def' => not_in_a }
You can also do it with regular each if these fancy enumerable methods are bit too much for you:
hash = { 'abc' => [], 'def' => [] }
a.each do |x|
if c.include?(x)
hash['abc'].push(x)
else
hash['def'].push(x)
end
end
Unfortunately this question turned out not to be as interesting as I was hoping. I was hoping that the problem was this:
Knowing the hash key and a value, how can I make sure the key's value is an array and that the given value is appended to that?
For instance, start with h being {}. I have a key name :k and a value 1. I want h[:k], if it doesn't already exist, to be [1]. But if it does already exist, then it's an array and I want to append 1 to that array; for instance, if h[:k] is already [3,2], now it should be [3,2,1].
I can think of various ways to ensure that, but one possibility is this:
(hash[key] ||= []) << value
To see that this works, let's make it a method:
def add_to_hash_array_value(hash:, key:, value:)
(hash[key] ||= []) << value
end
Now I'll just call that a bunch of times:
h = {}
add_to_hash_array_value(hash:h, key:"abc", value:1)
add_to_hash_array_value(hash:h, key:"abc", value:2)
add_to_hash_array_value(hash:h, key:"def", value:4)
add_to_hash_array_value(hash:h, key:"def", value:5)
add_to_hash_array_value(hash:h, key:"abc", value:3)
puts h #=> {"abc"=>[1, 2, 3], "def"=>[4, 5]}
We got the right answer.
This is nice because suppose I have some way of knowing, given a value, what key it should be appended to. I can just repeatedly apply that decision-making process.
However, the trouble with trying to apply that to the original question is that the original question seems not to know exactly how to decide, given a value, what key it should be appended to.

How to check if two identical array exist in a single two-dimensional array? [Swift]

if I have a two-dimensional array like this: [[1,2,3], [3,2,1], [4,9,3]], I want to be able to find out that there are two identical arrays inside this array, which are [1,2,3] and [3,2,1]. How can I achieve this?
Thank you for all your answers, I was focusing on the leetCode threeSum problem so I didn't leave any comment. But since I am a programming noobie, my answer exceeded the time limit.. so I actually wanted to find the duplicated arrays and remove all the duplicates and leave only one unique array in the multi-dimensional array. I have added some extra code based on #Oleg's answer, and thought I would put my function here :
func removeDuplicates(_ nums: inout [[Int]] ) -> [[Int]]{
let sorted = nums.map{$0.sorted()}
var indexs = [Int]()
for (pos,item) in sorted.enumerated() {
for i in pos+1..<sorted.count {
if item == sorted[i] {
if nums.indices.contains(i){
indexs.append(i)
}
}
}
}
indexs = Array(Set<Int>(indexs))
indexs = indexs.sorted(by: {$0 > $1})
for index in indexs{
nums.remove(at: index)
}
return nums
}
My solution is quite simple and easy to understand.
let input = [[1,2,3], [3,2,1], [4,9,3]]
First let sort all elements of the nested arrays. (It gives us a bit more efficiency.)
let sorted = input.map{$0.sorted()}
Than we should compare each elements.
for (pos,item) in sorted.enumerated() {
for i in pos+1..<sorted.count {
if item == sorted[i] {
print(input[pos])
print(input[i])
}
}
}
Output:
[1, 2, 3]
[3, 2, 1]
One simple and easy brute force approach that comes to my mind is:
Iterate over each row and sort its values. So 1,2,3 will become 123 and 3,2,1 will also become 1,2,3.
Now store it in a key value pair i.e maps. So your key will be 123 and it will map to array 1,2,3 or 3,2,1.
Note:- Your key is all the sorted elements combined together as string without commas.
This way you will know that how may pairs of arrays are there inside a 2d array are identical.
There is a very efficient algorithm using permutation hashing method.
1) preprocess the 2-dim array so that all elements are non-negative. (by subtracting the smallest element from all elements)
2) with each sub-array A:
compute hash[A] = sum(base^A[i] | with all indexes i of sub-array A). Choose base to be a very large prime (1e9+7 for example). You can just ignore the integer-overflow problem when computing, because we use only additions and multiplications here.
3) now you have array "hash" of each sub-array. If the array has 2 identical sub-arrays, then they must have the same hash codes. Find all pairs of sub-arrays having equal hash codes(using hash again, or sorting, ... whatever).
4) For each pair, check again if these sub-arrays actually match (sort and compare, ... whatever). Return true if you can find 2 sub-arrays that actually match, false otherwise.
Practically, this method runs extremely fast even though it is very slow theoretically. This is because of the hashing step will prune most of search space, and this hash function is super strong. I am sure 99.99% that if exist, the pair of corresponding sub-arrays having the same hash codes will actually match.

Splitting a hash variable with reduce

When invoking reduce on an array of hashes, I thought I could split the hash by key and value within the parameters using the () technique. But in this case, it does not appear to work:
columns = [
{"lead"=>["source", 2]},
{"parent"=>["name", 4]}
]
columns.reduce({}) do |acc, (k,v)|
puts "k #{k} v #{v}"
end
# k {"lead"=>["source", 2]} v
# k {"parent"=>["name", 4]} v
# => nil
I expected k to be "lead" and v to be ["source", 2]. Because columns is an array and not a hash, I cannot do this (k,v) to get the key/value pair of hash. Is there another technique I can use in argument list in order to pass the k/v pair rather than having to dissect it in the block?
It happens because the logic behind this parenthesis is basically a parallel assignment, which doesn't work in your case because each item in the collection is a hash. So practically what you can do it convert each item to array instead:
columns = columns.flat_map(&:to_a)
Demonstration

Mapping array values to hash with incrementing key name

I have an array of strings that are hex color codes like so:
["#121427", "#291833", "#4B2E4D", "#5D072F", "#BB2344", "#ED9F90"]
and I want to convert them into a hash with distinct key name where there is a distinct name "color" and then a integer value which increments as it transverses the array adding them like so:
{"color1" => "#121427", "color2" => "#291833", "color3" => "#4B2E4D", "color4" => "#5D072F", "color5" => "#BB2344", "color6" => "#ED9F90"}
The integer value can be 0 based or 1 based, it doesn't matter which ever is cleaner.
I've tried using the map method along with the to_h method, although I cannot figure out how to create an incremental key name as described.
It's not too hard to do this using the each_with_index method which is zero-indexed by default:
Hash[colors.each_with_index.map { |c, i| [ 'color%d' % i, c ] }]
You were close with map, you just needed to expand it into value/index pairs.
Another possible way:
colors.each_with_object({}).with_index(1){|(e, h), i| h["color#{i}"] = e}
It uses:
Enumerable#each_with_object to skip one temporary array and modify the resulting hash directly
Enumerator#with_index to also supply an auto-incrementing integer (it can even start from 1)
If arr is your array, you could do this:
idx = 1.step
# => #<Enumerator: 1:step>
arr.each_with_object({}) { |s,h| h["color#{idx.next}"] = s }
#=> {"color1"=>"#121427", "color2"=>"#291833", "color3"=>"#4B2E4D",
# "color4"=>"#5D072F", "color5"=>"#BB2344", "color6"=>"#ED9F90"}

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.

Resources