Creating a hash where the keys are strings, values are numbers - ruby

I have an array:
["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
I want to create a hash:
{"Melanie"=>[149], "Joe"=>[2, 16, 216] "Sarah"=>nil}
How would I accomplish this when the keys and values are in the same array?
All values would be integers (although they are in string form in the array.) All keys start and end with a letter.

Your expected hash is invalid. Therefore, it is impossible to get what you wrote that you want.
From your issue, it looks reasonable to expect the values to be array. In that case, you can do it like this:
["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
.slice_before(/[a-z]/i).map{|k, *v| [k, v.map(&:to_i)]}.to_h
# => {"Melanie"=>[149], "Joe"=>[2, 16, 216], "Sarah"=>[]}
With little modification, you can let the value be a number instead of an array when the array length is one, but that is not a good design; it would introduce exceptions.

Try this
def numeric?(x)
x.chars.all? { |y| ('0'..'9').include?(y) }
end
array = ["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
keys = array.select { |x| not numeric?(x) }
map = {}
keys.each do |k|
from = array.index(k) + 1
to = array.index( keys[keys.index(k) + 1] )
map[k] = to ? array[from...to] : array[from..from]
end
p map
Output:
{"Melanie"=>["149"], "Joe"=>["2", "16", "216"], "Sarah"=>[]}
[Finished in 0.1s]

Here's another way:
arr = ["Melanie", "149", "Joe", "2", "16", "216", "Sarah"]
class String
def integer?
!!(self =~ /^-?\d+$/)
end
end
Hash[*arr.each_with_object([]) { |s,a| s.integer? ? a[-1] << s.to_i : a<<s<<[] }].
tap { |h| h.each_key { |k| h[k] = nil if h[k].empty? } }
#=> {"Melanie"=>[149], "Joe"=>[2, 16, 216], "Sarah"=>nil}

There are three components to your question, and I will try to answer them separately.
Regarding storing a multi-valued mapping, while there are specialized solutions available, the most common recommendation is just to store a hash whose values are arrays. That is, for your use case, your primary data structure is a hash whose keys are strings and whose values are arrays of integers. Depending on your desired behavior for duplicates etc., etc, you may wish to substitute a different data structure for the value structure, possibly a set.
Regarding identifying strings containing numbers and strings not containing numbers, well, that depends on exactly what your non-number-containing strings could instead contain, but a good starting point would be to perform a regular expression match for digits. You didn't specify whether your allowable numeric strings represented integers, floating points, etc. The particular answer to that may affect your overall strategy. Unfortunately, input parsing and validation is a complex and messy topic in the general case.
Regarding the actual conversion process, I would recommend the following strategy. Iterate through your input array. Check each string for whether it is numeric or non-numeric. If it is non-numeric, store that as the current key in a local. Also, in your hash, create a mapping from that key to a new empty array. If, instead, the string is numeric, convert it into a number, and add it to the array under the appropriate key.

I don't know if there's a pretty way to do it. I'd do something like this:
def numeric?(string)
# `!!` converts parsed number to `true`
!!Kernel.Float(string)
rescue TypeError, ArgumentError
false
end
def my_method(input_array)
# associate values with proper key and stores result in output
curr_key = nil
output = {}
input_array.each do |e|
if !numeric?(e)
output[e] = []
curr_key = e
else
# use Float if values may be floating-point
output[curr_key] << Integer(e, 10)
end
end
output.each do |k, v|
output[k] = v.empty? ? nil : v
end
output
end
Source for numeric method.

Related

Ruby Hash: type casting

I’m trying to get a better grasp on writing in Ruby and working with Hash tables and their values.
1. Say you have a hash:
‘FOO’= {‘baz’ => [1,2,3,4,5]}
Goal: convert each value into a string in the ‘Ruby’ way.
I’ve come across multiple examples of using .each eg.
FOO.each = { |k,v| FOO[k] = v.to_s }
However this renders an array encapsulated in a string. Eg. "[1,2,3,4,5]" where it should be ["1", "2", "3", "4", "5"].
2. When type casting is performed on a Hash that’s holds an array of values, is the result a new array? Or simply a change in type of value (eg. 1 becomes “1” when .to_s is applied (say the value was placed through a each enumerator like above).
An explanation is greatly appreciated. New to Ruby.
In the each block, k and v are the key value pair. In your case, 'baz' is key and [1,2,3,4,5] is value. Since you're doing v.to_s, it converts the whole array to string and not the individual values.
You can do something like this to achieve what you want.
foo = { 'baz' => [1,2,3,4,5] }
foo.each { |k, v| foo[k] = v.map(&:to_s) }
You can use Hash#transform_values:
foo = { 'baz' => [1, 2, 3, 4, 5] }
foo.transform_values { |v| v.map(&:to_s) } #=> {"baz"=>["1", "2", "3", "4", "5"]}

Merge duplicate values in json using ruby

I have the following item.json file
{
"items": [
{
"brand": "LEGO",
"stock": 55,
"full-price": "22.99",
},
{
"brand": "Nano Blocks",
"stock": 12,
"full-price": "49.99",
},
{
"brand": "LEGO",
"stock": 5,
"full-price": "199.99",
}
]
}
There are two items named LEGO and I want to get output for the total number of stock for the individual brand.
In ruby file item.rb i have code like:
require 'json'
path = File.join(File.dirname(__FILE__), '../data/products.json')
file = File.read(path)
products_hash = JSON.parse(file)
products_hash["items"].each do |brand|
puts "Stock no: #{brand["stock"]}"
end
I got output for stock no individually for each brand wherein I need the stock to be summed for two brand name "LEGO" displayed as one.
Anyone has solution for this?
json = File.open(path,'r:utf-8',&:read) # in case the JSON uses UTF-8
items = JSON.parse(json)['items']
stock_by_brand = items
.group_by{ |h| h['brand'] }
.map do |brand,array|
[ brand,
array
.map{ |item| item['stock'] }
.inject(:+) ]
end
.to_h
#=> {"LEGO"=>60, "Nano Blocks"=>12}
It works like this:
Enumerable#group_by takes the array of items and creates a hash mapping the brand name to an array of all item hashes with that brand
Enumerable#map turns each brand/array pair in that hash into an array of the brand (unchanged) followed by:
Enumerable#map on the array of items picks out just the "stock" counts, and then
Enumerable#inject sums them all together
Array#to_h then turns that array of two-value arrays into a hash, mapping the brand to the sum of stock values.
If you want simpler code that's less functional and possibly easier to understand:
stock_by_brand = {} # an empty hash
items.each do |item|
stock_by_brand[ item['brand'] ] ||= 0 # initialize to zero if unset
stock_by_brand[ item['brand'] ] += item['stock']
end
p stock_by_brand #=> {"LEGO"=>60, "Nano Blocks"=>12}
To see what your JSON string looks like, let's create it from your hash, which I've denoted h:
require 'json'
j = JSON.generate(h)
#=> "{\"items\":[{\"brand\":\"LEGO\",\"stock\":55,\"full-price\":\"22.99\"},{\"brand\":\"Nano Blocks\",\"stock\":12,\"full-price\":\"49.99\"},{\"brand\":\"LEGO\",\"stock\":5,\"full-price\":\"199.99\"}]}"
After reading that from a file, into the variable j, we can now parse it to obtain the value of "items":
arr = JSON.parse(j)["items"]
#=> [{"brand"=>"LEGO", "stock"=>55, "full-price"=>"22.99"},
# {"brand"=>"Nano Blocks", "stock"=>12, "full-price"=>"49.99"},
# {"brand"=>"LEGO", "stock"=>5, "full-price"=>"199.99"}]
One way to obtain the desired tallies is to use a counting hash:
arr.each_with_object(Hash.new(0)) {|g,h| h.update(g["brand"]=>h[g["brand"]]+g["stock"])}
#=> {"LEGO"=>60, "Nano Blocks"=>12}
Hash.new(0) creates an empty hash (represented by the block variable h) with with a default value of zero1. That means that h[k] returns zero if the hash does not have a key k.
For the first element of arr (represented by the block variable g) we have:
g["brand"] #=> "LEGO"
g["stock"] #=> 55
Within the block, therefore, the calculation is:
g["brand"] => h[g["brand"]]+g["stock"]
#=> "LEGO" => h["LEGO"] + 55
Initially h has no keys, so h["LEGO"] returns the default value of zero, resulting in { "LEGO"=>55 } being merged into the hash h. As h now has a key "LEGO", h["LEGO"], will not return the default value in subsequent calculations.
Another approach is to use the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged:
arr.each_with_object({}) {|g,h| h.update(g["brand"]=>g["stock"]) {|_,o,n| o+n}}
#=> {"LEGO"=>60, "Nano Blocks"=>12}
1 k=>v is shorthand for { k=>v } when it appears as a method's argument.

Sorting an array of hashes by a date field

I have an object with many arrays of hashes, one of which I want to sort by a value in the 'date' key.
#array['info'][0] = {"name"=>"personA", "date"=>"23/09/1980"}
#array['info'][1] = {"name"=>"personB", "date"=>"01/04/1970"}
#array['info'][2] = {"name"=>"personC", "date"=>"03/04/1975"}
I have tried various methods using Date.parse and with collect but an unable to find a good solution.
Edit:
To be clear I want to sort the original array in place
#array['info'].sort_by { |i| Date.parse i['date'] }.collect
How might one solve this elegantly the 'Ruby-ist' way. Thanks
Another way, which doesn't require converting the date strings to date objects, is the following.
Code
def sort_by_date(arr)
arr.sort_by { |h| h["date"].split('/').reverse }
end
If arr is to be sorted in place, use Array#sort_by! rather than Enumerable#sort_by.
Example
arr = [{ "name"=>"personA", "date"=>"23/09/1980" },
{ "name"=>"personB", "date"=>"01/04/1970" },
{ "name"=>"personC", "date"=>"03/04/1975" }]
sort_by_date(arr)
#=> [{ "name"=>"personB", "date"=>"01/04/1970" },
# { "name"=>"personC", "date"=>"03/04/1975" },
# { "name"=>"personA", "date"=>"23/09/1980" }]
Explanation
For arr in the example, sort_by passes the first element of arr into its block and assigns it to the block variable:
h = { "name"=>"personA", "date"=>"23/09/1980" }
then computes:
a = h["date"].split('/')
#=> ["23", "09", "1980"]
and then:
b = a.reverse
#=> ["1980", "09", "23"]
Similarly, we obtain b equal to:
["1970", "04", "01"]
and
["1975", "04", "03"]
for each of the other two elements of arr.
If you look at the docs for Array#<=> you will see that these three arrays are ordered as follows:
["1970", "04", "01"] < ["1975", "04", "03"] < ["1980", "09", "23"]
There is no need to convert the string elements to integers.
Looks fine overall. Although you can drop the collect call since it's not needed and use sort_by! to modify the array in-place (instead of reassigning):
#array['info'].sort_by! { |x| Date.parse x['date'] }

Ruby how to return an element of a dictionary?

# dictionary = {"cat"=>"Sam"}
This a return a key
#dictionary.key(x)
This returns a value
#dictionary[x]
How do I return the entire element
"cat"=>"Sam"
#dictionary
should do the trick for you
whatever is the last evaluated expression in ruby is the return value of a method.
If you want to return the hash as a whole. the last line of the method should look like the line I have written above
Your example is a bit (?) misleading in a sense it only has one pair (while not necessarily), and you want to get one pair. What you call a "dictionary" is actually a hashmap (called a hash among Rubyists).
A hashrocket (=>) is a part of hash definition syntax. It can't be used outside it. That is, you can't get just one pair without constructing a new hash. So, a new such pair would look as: { key => value }.
So in order to do that, you'll need a key and a value in context of your code somewhere. And you've specified ways to get both if you have one. If you only have a value, then:
{ #dictionary.key(x) => x }
...and if just a key, then:
{ x => #dictionary[x] }
...but there is no practical need for this. If you want to process each pair in a hash, use an iterator to feed each pair into some code as an argument list:
#dictionary.each do |key, value|
# do stuff with key and value
end
This way a block of code will get each pair in a hash once.
If you want to get not a hash, but pairs of elements it's constructed of, you can convert your hash to an array:
#dictionary.to_a
# => [["cat", "Sam"]]
# Note the double braces! And see below.
# Let's say we have this:
#dictionary2 = { 1 => 2, 3 => 4}
#dictionary2[1]
# => 2
#dictionary2.to_a
# => [[1, 2], [3, 4]]
# Now double braces make sense, huh?
It returns an array of pairs (which are arrays as well) of all elements (keys and values) that your hashmap contains.
If you wish to return one element of a hash h, you will need to specify the key to identify the element. As the value for key k is h[k], the key-value pair, expressed as an array, is [k, h[k]]. If you wish to make that a hash with a single element, use Hash[[[k, h[k]]]].
For example, if
h = { "cat"=>"Sam", "dog"=>"Diva" }
and you only wanted to the element with key "cat", that would be
["cat", h["cat"]] #=> ["cat", "Sam"]
or
Hash[[["cat", h["cat"]]]] #=> {"cat"=>"Sam"}
With Ruby 2.1 you could alternatively get the hash like this:
[["cat", h["cat"]]].to_h #=> {"cat"=>"Sam"}
Let's look at a little more interesting case. Suppose you have an array arr containing some or all of the keys of a hash h. Then you can get all the key-value pairs for those keys by using the methods Enumerable#zip and Hash#values_at:
arr.zip(arr.values_at(*arr))
Suppose, for example,
h = { "cat"=>"Sam", "dog"=>"Diva", "pig"=>"Petunia", "owl"=>"Einstein" }
and
arr = ["dog", "owl"]
Then:
arr.zip(h.values_at(*arr))
#=> [["dog", "Diva"], ["owl", "Einstein"]]
In steps:
a = h.values_at(*arr)
#=> h.values_at(*["dog", "owl"])
#=> h.values_at("dog", "owl")
#=> ["Diva", "Einstein"]
arr.zip(a)
#=> [["dog", "Diva"], ["owl", "Einstein"]]
To instead express as a hash:
Hash[arr.zip(h.values_at(*arr))]
#=> {"dog"=>"Diva", "owl"=>"Einstein"}
You can get the key and value in one go - resulting in an array:
#h = {"cat"=>"Sam", "dog"=>"Phil"}
key, value = p h.assoc("cat") # => ["cat", "Sam"]
Use rassoc to search by value ( .rassoc("Sam") )

Ruby multiple arrays comparison

I am trying to replace a component in a legacy system with a Ruby script. One piece of this system accepts a string that contains ASCII '0's and '1's apparently to represent a bitfield of locations. It then converts these location to a string of comma separated 2 two codes (mostly US states).
I have a Ruby method that does this but it doesn't seem like I am doing it the best way Ruby could. Ruby has a ton of ways built in to iterate over and manipulated array and I feel I am not using them to their fullest:
# input "0100010010" should return "AZ,PR,WY"
def locations(bits)
# Shortened from hundreds for this post. :u? is for locations I have't figured out yet.
fields = [ :u?, :az, :de, :mi, :ne, :wy, :u?, :u?, :pr, :u? ]
matches = []
counter = 0
fields.each { |f|
case bits[counter]
when '1' then matches << f
when '0' then nil
else raise "Unknown value in location bit field"
end
counter += 1
}
if matches.include(:u?) then raise "Unknown field bit set" end
matches.sort.join(",").upcase
end
What would be a better way to do this?
It seems counter to the "Ruby way" to have counter variables floating around. I tried looking at ways to use Array#map, and I could find nothing obvious. I also tried Googling for Ruby Idioms pertaining to Arrays.
matches = fields.select.with_index { |_,i| bits[i] == '1' }
# => [:az, :wy, :pr]
To verify bits only holds 0s and 1s, you can still do
raise "Unknown value in location bit field" if !bits.match(/^[01]*$/)
Use Array#zip and Array#reduce
bits.split('').zip(fields).reduce([]) do |a, (k, v)|
k == '1' ? a << v.to_s.upcase : a
end.sort.join(',')
# => "AZ,PR,WY
Explanation:
1) split bits into an array of chars:
bits.split('') # => ["0", "1", "0", "0", "0", "1", "0", "0", "1", "0"]
2) zip both arrays to generate an array of pairs (by position)
bits.split('').zip(fields) # => [["0", :u?], ["1", :az], ["0", :de], ["0", :mi],
# ["0", :ne], ["1", :wy], ["0", :u?], ["0", :u?], ["1", :pr], ["0", :u?]]
3) reduce the array taking the desired elements according to the conditions
.reduce([]) do |a, (k, v)|
k == '1' ? a << v.to_s.upcase : a
end # => "[AZ,WY,PR]
4) sort the resulting array and join their elements to get the expected string
.sort.join(',') # => "AZ,PR,WY"
You could combine each_with_index, map andcompact:
fields.each_with_index.map do |v,i|
v if bits[i] == '1'
end.compact
each_with_index returns an iterator for each each value and its integer index.
map uses the return value of a passed block to yield an output value for each input value. The block returns the value if its corresponding bit is set, and implicitly returns nil if it is not.
compact returns a copy of the output array with all of the nil values removed.
For more detail see the docs for Enumerable and Array.

Resources