Weird behaviour with hashes in Ruby - ruby

I have a method that tries to parse a query into a a hash.
CONTACT_SEARCH_FIELDS = ['LastUpdate','Name','RecAdd','PhoneNumber','Tag_Group','FirstName','LastName','FamilyName','FamilyHead','ClientStatus','ContactType','ClientSource','TaxId']
CONTACT_SEARCH_OPERANDS = ['=','>','<','!=','Like','BeginsWith','IsEmpty']
def search (query, page = 1)
body = [{}]*query.length
query.each_with_index do |expr, i|
body[i]["Field"] = CONTACT_SEARCH_FIELDS.index expr[0]
body[i]["Operand"] = CONTACT_SEARCH_OPERANDS.index expr[1]
body[i]["Value"] = expr[2]
end
return body
end
The method is called like this
search([["FirstName", "=", "John"], ["LastName", "=", "Smith"]])
The problem is that running this gives a very weird output.
search([["FirstName", "=", "John"], ["LastName", "=", "Smith"]])
=> [{"Operand"=>0, "Value"=>"Smith", "Field"=>6}, {"Operand"=>0, "Value"=>"Smith", "Field"=>6}]
I did some debugging and the problem is that all the hashes in the array are get set on every iteration.
I dont understand what is the reason behind this. I would also welcome any shorter or better versions of this code.

Change the line
body = [{}]*query.length
The above means, you are creating an Array, whose elements are same Hash objects.
Example :
a = [{}]*3 # => [{}, {}, {}]
a.map(&:object_id) # => [18499356, 18499356, 18499356]
a[0]["a"] = 2
a # => [{"a"=>2}, {"a"=>2}, {"a"=>2}]
to
body = Array.new(query.length) { {} }
But the above means, you are creating an Array, whose elements are different Hash objects.
Example :
a = Array.new(3) { {} } # => [{}, {}, {}]
a.map(&:object_id) # => [17643864, 17643852, 17643840]
a[0]["a"] = 2
a # => [{"a"=>2}, {}, {}]

Related

Create a new hash from an existing array of hashes

I am newbie to ruby . I have an array of hashes input_array
[{
"name"=>"test1",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>1000,
"name"=>"test2",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>5000,
"name"=>"test3",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>3000,
"name"=>"test4",
"zone_status"=>"valid",
"certificate_status"=>"valid",
"users"=>2000}]
and an array
existing_names_array = ["test1","test2"]
The below line gets me all the names into input_names
input_array.each_with_index {|val, index| input_names << input_array[index]['name'] }
But how can I get the input_names to be a hash with name as key and its respective users as value?
Because my final goal is to check the names which are in input_names, but not in existing_names_array and get a count of those names and users
As the names tes1,test2 exists in existing_names_array, I need the count of rest of names and count of their respective users in input_array
Expected output:
output_names = test3, test 4
total_output_names = 2
output_users = 3000,2000
total_output_users = 5000
If you use ActiveSupport's Enumerable#index_by with Ruby core's Hash#transform_values it's pretty easy, if I'm understanding your question correctly:
# add gem 'activesupport' to Gemfile, or use Rails, then ..
require 'active_support/all'
users_and_counts = input_array.
index_by { |hsh| hsh["name"] }.
transform_values { |hsh| hsh["users"] }
# => { "test1" => 1000, "test2" => 5000, ... }
You can do this with Enumerable#reduce (or Enumerable#each_with_object) as well:
users_and_counts = input_array.reduce({}) do |memo, hsh|
memo[hsh["name"]] = hsh["users"]
memo
end
Or, the simplest way, with good old each:
users_and_counts = {}
input_array.each do |hsh|
users_and_counts[hsh["name"]] = hsh["users"]
end
in response to comment
In general, there are a few ways to check whether an element is found in an array:
array = ["foo", "bar"]
# 1. The simple standard way
array.include?("foo") # => true
# 2. More efficient way
require 'set'
set = Set.new(array)
set.member?("foo") # => true
So with this knowledge we can break up our task into a few steps:
Make a new hash which is a copy of the one we built above, but without the key-vals corresponding to users in the existing_names_array (see Hash#select and Hash#reject):
require 'set'
existing_names_set = Set.new(existing_names_array)
new_users_and_counts = users_and_counts.reject do |name, count|
existing_names_set.member?(name)
end
# => { "test3" => 3000, "test4" => 2000 }
Use Hash#keys to get the list of user names:
new_user_names = new_users_and_counts.keys
# => ["test3", "test4"]
Use Hash#values to get the list of counts:
new_user_counts = new_users_and_counts.values
# => [3000, 2000]
I assume input_array is to be as follows.
input_array = [
{ "name"=>"test1", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>1000 },
{ "name"=>"test2", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>5000 },
{ "name"=>"test3", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>3000 },
{ "name"=>"test4", "zone_status"=>"valid", "certificate_status"=>"valid",
"users"=>2000}
]
We are also given:
names = ["test1", "test2"]
The information of interest can be represented nicely by the following hash (which I will construct):
summary = { "test1"=>1000, "test2"=>5000 }
We might use that to compute the following.
names = summary.keys
#=> ["test1", "test2"]
users = summary.values
#=> [1000, 5000]
total_users = users.sum
#=> 6000
There are many ways to construct the hash summary. I prefer the following.
summary = input_array.each_with_object({}) do |g,h|
key = g["name"]
h[key] = g["users"] if names.include?(key)
end
#=> {"test1"=>1000, "test2"=>5000}
Another way is as follows.
summary = input_array.map { |g| g.values_at("name", "users") }.
.to_h
.slice(*names)
See [Hash#values_at(https://ruby-doc.org/core-2.7.0/Hash.html#method-i-values_at), Array#to_h and Hash#slice. The steps are as follows.
arr = input_array.map { |g| g.values_at("name", "users") }
#=> [["test1", 1000], ["test2", 5000], ["test3", 3000], ["test4", 2000]]
h = arr.to_h
#=> {"test1"=>1000, "test2"=>5000, "test3"=>3000, "test4"=>2000}
summary = h.slice(*names)
#=> {"test1"=>1000, "test2"=>5000}
The splat operator converts h.slice(*names), which is h.slice(*["test1", "test2"]), to h.slice("test1", "test2"). That's because Hash#slice was defined to accept a variable number of keys as arguments, rather than an array of keys as the argument.

Using a Ruby map on a range

I'm trying to use .map so I don't need to initialize a products array.
Here's the original code:
products = []
for page in (1..(ShopifyAPI::Product.count.to_f/150.0).ceil)
products += ShopifyAPI::Product.find(:all, :params => {:page => page, :limit => 150})
end
Here's what I've tried:
products = (1..(ShopifyAPI::Product.count.to_f/150.0).ceil).map do |page|
ShopifyAPI::Product.find(:all, :params => {:page => page.to_i, :limit => 150})
end
Which only returns the first product? What am I doing wrong?
The ShopifyAPI::Product returns a list of products based on the sent parameters page, and limit.
I'm not sure why you're finding the second snippet only returns the first product, but in order to make it functionally equivalent to the first, you could use flat_map instead of map here, or alternatively tack on a .flatten at the end (or flatten(1), if you want to be more specific)
Given that the .find call returns an array, you can see the difference in the following examples:
a = []
(0..2).each { |x| a += [x] }
# a == [0,1,2]
(0..2).map { |x| [x] }
# [[0], [1], [2]]
(0..2).flat_map { |x| [x] }
# [0, 1, 2]
That's because array + array combines the two of them.
If your first snippet instead used products.push(<find result>) then you'd see the same nested array result.
See Enumerable#flat_map and Array#flatten

Dashing reading out of 2 different CSV's

Hello everyone I'm currently trying to get my Dashboard working properly, however I cannot figure out a way to get the values into something my List Widget can read.
begin
id = 1
names.each do |item|
label = names[id][0] #names = names.csv path
value = host_status[id]['status'] #host_status = host_status.csv path
items = { label: label, value: value }
id += 1
end
rescue
end
send_event('hosts', { items: items })
So what this script should do is :
write the host_status.csv with the values it gets from the status.cgi (working)
iterate through both the host_status.csv and names.csv getting values from both of them
output should be something like this (label comes from names.csv, value from host_status.csv) =>
{label: "localhost", value: "UP"}, {label: "USV", value: "UP"}
The list widget needs something like an Array in a Hash with the keys label and value as far as I can tell, however my script doesnt return anything is there something like a push method for hashes?
I'm assuming here that names is an array of arrays [['foo'], ['bar']]
and that host_status is an array of objects [{'status' => 'foo'}, {'status' => 'bar'}]
you should just be able to do
names = [['foo'], ['bar']]
host_status = [{'status' => 'foo'}, {'status' => 'bar'}]
labels = names.map(&:first) # ['foo', 'bar']
values = host_status.map {|s| s['status'] } # ['foo', 'bar']
# zip zips together two arrays [['foo', 'foo'], ['bar', 'bar']]
# inject iterates over the array and returns a new data structure
items = labels.zip(values).inject([]) do |memo, (k,v)|
memo.push({label: k, value:v})
memo
end
You should just be able to run that code sample in an irb session.
Enumerable#map:http://www.ruby-doc.org/core-1.9.3/Enumerable.html#method-i-map
Enumerable#zip: http://www.ruby-doc.org/core-1.9.3/Enumerable.html#method-i-zip
Enumerable#inject: http://www.ruby-doc.org/core-1.9.3/Enumerable.html#method-i-inject

Am I able to use each_cons(2).any? with hash values?

If I have an array of integers and wish to check if a value is less than the previous value. I'm using:
array = [1,2,3,1,5,7]
con = array.each_cons(2).any? { |x,y| y < x }
p con
This returns true, as expected as 1 is less than 3.
How would I go about checking if a hash value is less than the previous hash value?
hash = {"0"=>"1", "1"=>"2","2"=>"3","4"=>"1","5"=>"5","6"=>"7"}
I'm still learning Ruby so help would be greatly appreciated.
If you want to find out whether all elements meet the criteria, starting with an array:
array = [1,2,3,1,5,7]
con = array.each_cons(2).all? { |x,y| x < y }
con # => false
Changing the array so the elements are all less than the next:
array = [1,2,3,4,5,7]
con = array.each_cons(2).all? { |x,y| x < y }
con # => true
A lot of the methods behave similarly for array elements and hashes, so the basic code is the same, how they're passed into the block changes. I reduced the hash to the bare minimum to demonstrate the code:
hash = {"3"=>"3","4"=>"1","5"=>"5"}
con = hash.each_cons(2).all? { |(_, x_value), (_, y_value) | x_value < y_value }
con # => false
Changing the hash to be incrementing:
hash = {"3"=>"3","4"=>"4","5"=>"5"}
con = hash.each_cons(2).all? { |(_, x_value), (_, y_value) | x_value < y_value }
con # => true
Using any? would work the same way. If you want to know whether any are >=:
hash = {"3"=>"3","4"=>"1","5"=>"5"}
con = hash.each_cons(2).any? { |(_, x_value), (_, y_value) | y_value >= x_value }
con # => true
Or:
hash = {"3"=>"3","4"=>"4","5"=>"5"}
con = hash.each_cons(2).any? { |(_, x_value), (_, y_value) | x_value >= y_value }
con # => false
I'm creating the hash by
stripped = Hash[x.scan(/(\w+): (\w+)/).map { |(first, second)| [first.to_i, second.to_i] }]
I'm then removing empty arrays by
new = stripped.delete_if { |elem| elem.flatten.empty? }
This isn't a good way to use scan. Consider these:
'1: 23'.scan(/\d+/) # => ["1", "23"]
'1: 23'.scan(/(\d+)/) # => [["1"], ["23"]]
'1: 23'.scan(/(\d+): (\d+)/) # => [["1", "23"]]
In the first, scan returns an array of values. In the second, it returns an array of arrays, where each sub-array is a single element. In the third it returns an array of arrays, where each sub-array contains both elements scanned. You are using the third form, which unnecessarily complicates everything done after that.
Don't complicate the pattern passed to scan, and, instead, rely on its ability to return multiple matching elements as it looks through the string and to return an array of those:
'1: 23'.scan(/\d+/) # => ["1", "23"]
Build on top of that:
'1: 23'.scan(/\d+/).map(&:to_i) # => [1, 23]
Hash[*'1: 23'.scan(/\d+/).map(&:to_i)] # => {1=>23}
Notice the leading * inside Hash[]. That "splat" tells Ruby to burst or explode the array into its components. Here's what happens if it's not there:
Hash['1: 23'.scan(/\d+/).map(&:to_i)] # => {} # !> this causes ArgumentError in the next release
And, finally, if you don't need the hash elements to be integers, which contradicts the hash you gave in your question, just remove .map(&:to_i) from the examples above.
First, isolate the values from the hash:
values = hash.map { |key, value| value.to_i } #=> [1, 2, 3, 1, 5, 7]
or:
values = hash.values.map(&:to_i) #=> to_i is a shortcut for:
values = hash.values.map { |value| value.to_i }
and then do the same thing you did for your array example.

dynamically finding value from key string in nested hash

I have a user inputed string called x_value whose value contains something like ticker|high. Whenever there is a |, that indicates that the latter is a child of the former. The purpose of the method is to return a specific value within a hash.
sections = []
object.x_value.split('|').each do |part|
sections << part.to_sym
end
I then want to drill down the data hash and retrieve the value of the last key.
data = {"ticker":{"high":529.5,"low":465,"avg":497.25,"vol":7520812.018}}
In this example
data[sections[0]][sections[1]] returns the expected 529.5 value. However, the user may have different hashes and different levels deep of nested key/values. How can I write this?
I have tried data[sections], but that didn't work.
Use Enumerable#reduce
data = {"ticker" => {"high" => 529.5, "low" => 465,"avg" => 497.25,"vol" => 7520812.018}}
"ticker|high".split('|').reduce(data) { |dat,val| dat[val] } #=> 592.5
more example:
data = {"more_ticker" => {"ticker" => {"high" => 529.5, "low" => 465,"avg" => 497.25,"vol" => 7520812.018}}}
"more_ticker|ticker|avg".split('|').reduce(data) { |dat,val| dat[val] }
#=> 497.25
You could also use recursion:
def getit(hash, x_value)
recurse(hash, x_value.split('|'))
end
def recurse(hash, keys)
k = keys.shift
keys.empty? ? hash[k] : recurse(hash[k], keys)
end
data = {"ticker" => {"high" => 529.5, "low" => 465}}
getit(data, "ticker|high") #=> 529.5
getit(data, "ticker") #=> {"high"=>529.5, "low"=>465}
data = {"more_ticker" => {"ticker" => {"high" => 529.5, "low" => 465}}}
getit(data, "more_ticker|ticker|low") #=> 465
getit(data, "more_ticker|ticker|avg") #=> nil

Resources