Ruby - Hash function with unknown number of arguments - ruby

I'm trying to make a calorie counter for the below hash menu. in this example I've passed 3 arguments - what would the function need to look like it the number of parameters/arguments is unknown?
#menu = {
"hamburger" => 250,
"Cheese burger" => 350,
"cola" => 35,
"salad" => 120,
"dessert" => 350
}
def order(a, b, c)
return #menu[a] + #menu[b] + #menu[c]
end
puts order("hamburger", "Cheese burger", "cola")
tried
def order(**a)
total = 0
total += #menu[**a]
end
i know (*a) works for arrays.
I'd like to be able to get results for
puts order("hamburger")
and equally for
puts order("Cheese burger", "salad"), for example

In Ruby, it is often possible to write the code exactly the same way you would describe the solution in English. In this case, you need to get the values at specific keys of the hash and then compute the sum of the values.
You can use the Hash#values_at method to get the values at the specific keys and you can use the Array#sum method to compute the sum of the values:
def order(*items)
#menu.values_at(*items).sum
end
Note that it is strange to use an instance variable of the top-level main object. It would make much more sense to use a constant:
MENU = {
'hamburger' => 250,
'Cheese burger' => 350,
'cola' => 35,
'salad' => 120,
'dessert' => 350,
}
def order(*items)
MENU.values_at(*items).sum
end
It would also make sense to freeze the hash:
MENU = {
'hamburger' => 250,
'Cheese burger' => 350,
'cola' => 35,
'salad' => 120,
'dessert' => 350,
}.freeze
And last but not least, I find the name of the order method somewhat misleading. It is also ambiguous: is order meant to be a noun and this is meant to be a getter method that retrieves an order? Or is it meant to be a verb and it is meant to be a command method which tells the object to execute an order?
Either way, it does not seem that the method is doing either of those two things, rather it seems to compute a total. So, the name should probably reflect that.

I would do:
MENU = {
"hamburger" => 250,
"Cheese burger" => 350,
"cola" => 35,
"salad" => 120,
"dessert" => 350
}
def order(*args)
MENU.values_at(*args).sum
end
order("hamburger", "Cheese burger", "cola")
#=> 635
Read about the Ruby Splat Operator, Hash#values_at and Array#sum.
When you really want to use each (what I would not recommend), like mentioned in the comment, then you can implement it like this:
def order(*args)
total = 0
args.each { |name| total += MENU[name] }
total
end
or
def order(*args)
total = 0
MENU.values_at(*args).each { |value| total += value }
total
end

Related

To find out uniq values from an array of hashes based on an input hash

I have an array and a hash:
a = [
{ :color => "blue", :name => "wind" },
{ :color => "red", :name => "fire" },
{ :color => "white", :name => "wind" },
{ :color => "yellow", :name => "wind" },
{ :color => "green", :name => nil },
{ :color => "black", :name => "snow" }
]
b = { blue: 'blue', white: 'white', red: 'red', green: 'green', black: 'black' }
I need to find out unique names based on the input hash to get this:
['wind', 'fire', 'snow']
I've tried:
names = a.map { |i| [i[:color], i[:name]] }
.delete_if { |key, value| value.nil? }
resultant_names = []
b.values.each do |c|
if names[c]
resultant_names << names[c]
end
end
resultant_names.uniq
I need a better approach for this. This one has too many loops.
While your result does not make sense to me (e.g. it is missing snow) this will work
a.map(&:values).reverse.to_h.values_at(*b.values).compact.uniq
#=> ["wind","fire"]
To break it down:
a.map(&:values).reverse.to_h
#=> {"white"=>"wind", "green"=>nil, "yellow"=>"wind", "red"=>"fire", "blue"=>"wind"}
You'll notice snow is missing because when we reverse the list ["white","wind"] will overwrite ["white","snow"] when converted to a Hash
Then we just collect the values for the given colors from
b.values
#=> ["blue", "white", "red", "green"]
a.map(&:values).reverse.to_h.values_at(*b.values)
#=> ["wind", "wind", "fire", nil]
Then Array#compact will remove the nil elements and Array#uniq will make the list unique.
If snow was intended you could skip the reversal
a.map(&:values).to_h.values_at(*b.values).compact.uniq
#=> ["wind", "snow", "fire"]
Either way this is a strange data structure and these answers are only to help with the problem provided as the duplicate colors can cause differing results based on the order in a.
I believe you want 'snow' to be in your output array, as there is no other logical explanation. Your code would work if you were to add .to_h on the end of line 2, but as you note, it is not very clean or efficient. Also, by converting to a Hash, as a result of duplicate keys, you would potentially lose data.
Here's a tighter construct that avoids the data loss problem:
def unique_elements(a, b)
color_set = b.values.to_set
a.map { |pair| pair[:name] if color_set.include?(pair[:color]) }.compact.uniq
end
First we take the values of b and convert them to a set, so that we can efficiently determine if a given element is a member of the set.
Next we map over a choosing the names of those members of a for which the [:color] is included in our color set.
Finally we eliminate nils (using compact) and choose unique values.
>> unique_elements(a, b)
#> ["wind", "fire", "snow"]
I would begin by converting a to a more useful data structure.
h = a.each_with_object({}) { |g,h| h[g[:color]] = g[:name] }
#=> {"blue"=>"wind", "red"=>"fire", "white"=>"wind", "yellow"=>"wind",
# "green"=>nil, "black"=>"snow"}
We may then simply write
h.values_at(*b.values).compact.uniq
# => ["wind", "fire", "snow"]
This approach has several desireable characteristics:
the creation of h makes the method easier to read
debugging and testing is made easier by creating h as a separate step
h need only be created once even if several values of b are to be evaluated (in which case we may wish to make h an instance variable).
h could be chained to the second statement but I've chosen not to do so for the reasons given above (especially the last one).

summing values in a hash

What is the best way to sum hash values in ruby:
#price = { :price1 => "100", :price2 => "100", :price3 => "50" }
I do something like this right now:
#pricepackage = #price[:price1] + #price[:price2] + #price[:price3] + 500
Please explain your answer, I want to learn why and not just how. =)
You can do:
#price.values.map(&:to_i).inject(0, &:+)
EDIT: adding explanation
So #price.values returns an array that collects all the values of the hash. That is for instance ["1", "12", "4"]. Then .map(&:to_i) applies to_i to each of the elements of this array and thus you get [1,12,4]. Lastly .inject(0,&:+) preforms inject with initial value 0 and accumulating using the function +, so it sums all the elements of the array in the last step.
If your data set looks like this:
prices = {price1: 100, price2: 100, price3: 50}
then this will sum the values:
prices.values.inject(0) {|total, v| total += v}
Keep it simple
#prices = ....
#price = 0.0
#prices.each do |p|
#price += p
end

Ruby syntax for calling methods in classes

I'm working on an exercise where I have to create a roman to arabic number converter. As far as I can tell, the code below is totally kosher, but I keep getting an error when I run my tests. Ruby thinks there's an undefined method or variable on line 37 (noted by comment below).
I'm wondering if my snytax is off or if it's something else. Suggestions?
class ArabicNumeral
def replace_troublesome_roman_numerals(letters)
tough_mappings = {"CM" => "DCCCC", "CD" => "CCCC", "XC" => "LXXXX", "XL" => "XXXX", "IX"=> "VIIII", "IV" => "IIII"}
tough_mappings.each { |roman, arabic| letters = letters.gsub(roman, arabic) }
letters
end
def convert_and_add(letters)
digits = { "M" => 1000, "CM" => 900, "D" => 500, "C" => 100, "XC" => 90, "L" => 50, "XL" => 40, "X" => 10, "IX" => 9, "V" => 5, "IV" => 4, "I" => 1}
letters = letters.split("")
letters.inject(0) do |sum, letter|
arabic = digits[letter]
sum += arabic
end
end
def self.convert(letters)
roman_string = replace_troublesome_roman_numerals(letters) ###LINE 37!
arabic_number = convert_and_add(roman_string)
arabic_number
end
end
The problem here is the method you are calling on line 37. replace_troublesome_roman_numerals(letters). The problem is the method self.convert(letters) is a class method. You can call it like this:
ArabicNumeral.convert(letters)
However, it contains a call to an instance variable (being that replace_troublesome_roman_numerals(letters) I mentioned earlier.
def self.convert(letters)
roman_string = ArabicNumeral.new.replace_troublesome_roman_numerals(letters)
ArabicNumeral.new.convert_and_add(roman_string)
end
This creates an instance of ArabicNumeral and calls the method you need without saving it to a variable and taking up memory. I also removed the variable arabic_number from your method because you are calling convert_and_add(roman_string), adding it to the variable, and then returning the variable. since convert_and_add(roman_string) is the last thing handled by the method, it will return this anyway without the variable.
If you never plan on using those methods in an instance of ArabicNumeral then I would suggest making all the methods class level or wrapping them in a Module that you would include in your projects. If you don't plan on using them outside of the ArabicNumeral class at all, consider putting them behind a protected or private while leaving convert(letters) available.
class ArabicNumberal
def self.convert(letters)
# Code...
end
private
def self.replace_troublesome_roman_numerals(letters)
# Code...
end
def self.convert_and_add(roman_string)
# Code...
end
end
Ok... First of all, you're trying to use an instance method from a class one.
The problem could be solved by changing the method convert from:
def self.convert(letters)
roman_string = replace_troublesome_roman_numerals(letters) ###LINE 37!
arabic_number = convert_and_add(roman_string)
arabic_number
end
To:
def convert(letters)
roman_string = replace_troublesome_roman_numerals(letters) ###LINE 37!
arabic_number = convert_and_add(roman_string)
arabic_number
end
Then you'll need to create an instance and call convert method:
x = ArabicalNumeral.new()
x.convert('param')
And that's it.
By the way, I suggest you to add a constructor method (in Ruby is named initialize).
The complete script below:
class ArabicNumeral
def replace_troublesome_roman_numerals(letters)
tough_mappings = {"CM" => "DCCCC", "CD" => "CCCC", "XC" => "LXXXX", "XL" => "XXXX", "IX"=> "VIIII", "IV" => "IIII"}
tough_mappings.each { |roman, arabic| letters = letters.gsub(roman, arabic) }
letters
end
def convert_and_add(letters)
digits = { "M" => 1000, "CM" => 900, "D" => 500, "C" => 100, "XC" => 90, "L" => 50, "XL" => 40, "X" => 10, "IX" => 9, "V" => 5, "IV" => 4, "I" => 1}
letters = letters.split("")
letters.inject(0) do |sum, letter|
arabic = digits[letter]
sum += arabic
end
end
def convert(letters)
roman_string = replace_troublesome_roman_numerals(letters) ###LINE 37!
arabic_number = convert_and_add(roman_string)
arabic_number
end
end
x = ArabicNumeral.new()
puts x.convert('MDC')

How to force a Float to display with full precision w/o scientific notation and not as a string?

How do you force a float to display with all significant places / full precision without scientific notation in Ruby?
Presently I convert a BigDecimal to Float, BigDecimal(0.000000001453).to_f, however this yields a resultant float of 1.453e-09. If I do something like "%14.12f" % BigDecimal("0.000000001453").to_f I get a string. In this case, however, a string as output is unacceptable as I need it as an actual numeric float without scientific notation.
--- Edit---
Alright, let me give some context here, which will probably require a change of my original question.
I'm attempting to create a graph with Highstock & lazy_high_chart. Earlier today I was able to draw graphs just fine when the floats were emitting to the resultant js as full precision floats vs showing up in scientific notation. Hence I felt that the problem resides in this issue.
But after the few inputs I'm getting here, perhaps I need some further review of the source and my assumption is misplaced. I'll let you decide:
#h = LazyHighCharts::HighChart.new('graph') do |f|
hours_of_readings = 1
reading_intervals = 1 #hour
readings_per_hour = 60
readings = ModelName.order("date DESC").select('data_readings.data2, data_readings.data1, data_readings.date').limit(hours_of_readings * readings_per_hour).all
data1_and_date_series = Array.new
data2_and_date_series = Array.new
dates = Array.new
# I have been thinking that the problem lies here in the "row.data1.to_f" and
# "row.data2.to_f" and thus this is the root of my initial question in terms
# of it emitting scientific notation to the js output in the format of:
# [[1.0e-09], [1.04e-09],[9.4e-09], ... [3.68e-09]]
data1_and_date_series = readings.map{|row| [(row.date.to_i * 1000), (row.data1.to_f if BigDecimal(row.data1) != BigDecimal("-1000.0"))] }
data2_and_date_series = readings.map{|row| [(row.date.to_i * 1000), (row.data2.to_f if BigDecimal(row.data2) != BigDecimal("-1000.0"))] }
f.series(
:name => 'Data1',
:data => data1_and_date_series,
:pointStart => Time.now.to_i * 1000,
:pointEnd => hours_of_readings.hours.ago.to_i * 1000,
:pointInterval => reading_intervals.hour * 1000,
:color => 'blue'
)
f.series(
:name => 'Data2)',
:data => data2_and_date_series,
:pointStart => Time.now.to_i * 1000,
:pointEnd => hours_of_readings.hours.ago.to_i * 1000,
:pointInterval => reading_intervals.hour.to_i * 1000,
:color => 'red'
)
f.chart({:defaultSeriesType=>"spline" })
f.yAxis [
{:title => { :text => "Label 1", :margin => 10} },
{:title => { :text => "Label 2 (groups)"}, :opposite => true},
{:max => 0},
{:min => -0.000000001}
]
f.options[:xAxis] = {
:title => { :text => "Time"},
:type => "datetime"
}
f.title(:text => "Title")
f.legend(:align => 'right', :verticalAlign => 'top', :y => 75, :x => -50, :layout => 'vertical') # override the default values
end
The string representation and the actual value of a float are two different things.
What you see on screen/print-out is always a string representation, be it in scientific notation or "normal" notation. A float is converted to its string representation by to_s, puts, "%.10f" % and others.
The float value itself is independent of that. So your last sentence does not make much sense. The output is always a string.
To enforce a certain float format in Rails' to_json you can overwrite Float#encode_json, e.g.
class ::Float
def encode_json(opts = nil)
"%.10f" % self
end
end
Put this before your code above. Note that -- depending on your actual values -- you might need more sophisticated logic to produce reasonable strings.
Will this work for you -
>> 0.000000001453
=> 1.453e-09 # what you are getting right now
>> puts "%1.12f" % 0.000000001453
0.000000001453 # what you need
=> nil

I thought that I could increment the value of a hash key in ruby. Why is this failing?

hash = { "bill" => '39', 'kim' => '35', 'larry' => '47' }
for word in hash hash[word] += 1 end
puts "Bill is now #{hash['bill]']}"
This is the error message
undefined method `+' for nil:NilClass (NoMethodError)
This isn't working because word is going to represent an array for each key/value pair in the hash. So, on the first pass through the loop, word is going to be ["bill", "39"]. That's why hash[word] is returning nil.
Illustrated:
ruby-1.9.2-p180 :001 > for word in hash
ruby-1.9.2-p180 :002?> puts word.inspect
ruby-1.9.2-p180 :003?> end
["bill", 40]
["kim", 36]
["larry", 48]
What you probably want is:
hash.keys.each do |k|
hash[k] += 1
end
The second problem is that you're storing the values as Strings instead of Ints. So, the += 1 operation will fail. Either change your hash to this:
hash = { "bill" => 39, 'kim' => 35, 'larry' => 47 }
Or convert the value to an integer before doing the + 1.
You need to specify 2 variables for a hash in the for in loop:
hash = { "bill" => 39, 'kim' => 35, 'larry' => 47 }
for word, key in hash
hash[word] += 1
end
puts "Bill is now #{hash['bill]'}"
You should be using the native each enumerator instead:
friend_ages = { 'Bill' => 39, 'Kim' => 35, 'Larry' => 47 }
friend_ages.each { |name, age| friend_ages[name] += 1 }
puts "Bill is now #{friend_ages['Bill']}."

Resources