How can I avoid using class variables in Ruby - ruby

I have a piece of code that uses class variables. I've read that class variables should generally be avoided in Ruby.
The class variables are ##cost and ##kwh.
How can I rewrite the following without using class variables?
class Device
attr_accessor :name, :watt
##cost = 0.0946
def initialize(name, watt)
#name = name
#watt = watt
end
def watt_to_kwh(hours)
##kwh = (watt / 1000) * hours
end
def cost_of_energy
puts "How many hours do you use the #{self.name} daily?"
hours = gets.chomp.to_i
self.watt_to_kwh(hours)
daily_cost = ##kwh * ##cost
montly_cost = daily_cost * 30
puts "Dayly cost: #{daily_cost}€"
puts "montly_cost: #{montly_cost}€"
end
end

##cost behaves more like a constant (i.e. it won't change during runtime), so you should use one instead:
COST = 0.0946
##kwh should be an instance variable, since it is used only within the instantiated object, so you could use #kwh instead:
#kwh = (watt / 1000) * hours
And daily_cost = ##kwh * ##cost will become:
daily_cost = #kwh * COST
That will avoid the use of class variables, but you could also eliminate #kwh altogether since you don't use it anywhere else.
So, instead of:
def watt_to_kwh(hours)
#kwh = (watt / 1000) * hours
end
You could just do:
def watt_to_kwh(hours)
(watt / 1000) * hours
end
And use it like this in cost_of_energy method:
def cost_of_energy
puts "How many hours do you use the #{self.name} daily?"
hours = gets.chomp.to_i
daily_cost = watt_to_kwh(hours) * COST
montly_cost = daily_cost * 30
puts "Dayly cost: #{daily_cost}€"
puts "montly_cost: #{montly_cost}€"
end

Try this.
class Device
singleton_class.send(:attr_accessor, :cost_per_kwh)
def initialize(name, watts)
#name = name
#watts = watts
end
def daily_cost(hours_per_day)
self.class.cost_per_kwh * kwh_per_day(hours_per_day)
end
def monthly_cost(hours_per_day)
30 * daily_cost(hours_per_day)
end
private
def kwh_per_day(hours_per_day)
hours_per_day * #watts / 1000
end
end
singleton_class.send(:attr_accessor, :cost_per_kwh) creates a setter and getter for the class instance variable #cost_per_kwh.
First, obtain and save the cost per kwh, which will be used in the calculation of cost for all devices of interest.
puts "Please enter the cost per kwh in $"
Device.cost_per_kwh = gets.chomp.to_f
Suppose
Device.cost_per_kwh = 0.0946
Calculate the costs for each device of interest.
puts "What is the name of the device?"
name = gets.chomp
puts "How many watts does it draw?"
watts = gets.chomp.to_f
Suppose
name = "chair"
watts = 20000.0
We may now create a class instance.
device = Device.new(name, watts)
#=> #<Device:0x007f9d530206f0 #name="chair", #watts=20000.0>
Lastly, obtain hours per days, the only variable likely to change in future calculations of costs for the given device.
puts "How many hours do you use the #{name} daily?"
hours_per_day = gets.chomp.to_f
Lastly, suppose
hours_per_day = 0.018
then we may compute the costs.
puts "Daily cost: $#{ device.daily_cost(hours_per_day)}"
Daily cost: $0.034056€
puts "Monthly_cost (30 days/month): $#{ 30 * device.daily_cost(hours_per_day) }"
Monthly_cost (30 days/month): $1.0216800000000001
Suppose circumstances change1 and use of the device increases. We need only update hours per day. For example,
puts "How many hours do you use the #{name} daily?"
hours_per_day = gets.chomp.to_f
Suppose now
hours_per_day = 1.5
Then
puts "Daily cost: $#{ device.daily_cost(hours_per_day)}"
Daily cost: $2.838
puts "Monthly_cost (30 days/month): $#{ 30 * device.daily_cost(hours_per_day) }"
Monthly_cost (30 days/month): $85.14
1 The election of a new president, for example.

Related

Infinity is returned when calculating average in array

Why does the following method return infinity when trying to find the average volume of a stock:
class Statistics
def self.averageVolume(stocks)
values = Array.new
stocks.each do |stock|
values.push(stock.volume)
end
values.reduce(:+).to_f / values.size
end
end
class Stock
attr_reader :date, :open, :high, :low, :close, :adjusted_close, :volume
def initialize(date, open, high, low, close, adjusted_close, volume)
#date = date
#open = open
#high = high
#low = low
#close = close
#adjusted_close = adjusted_close
#volume = volume
end
def close
#close
end
def volume
#volume
end
end
CSV.foreach(fileName) do |stock|
entry = Stock.new(stock[0], stock[1], stock[2], stock[3], stock[4], stock[5], stock[6])
stocks.push(entry)
end
Here is how the method is called:
Statistics.averageVolume(stocks)
Output to console using a file that has 251 rows:
stock.rb:32: warning: Float 23624900242507002003... out of range
Infinity
Warning is called on the following line: values.reduce(:+).to_f / values.size
When writing average functions you'll want to pay close attention to the possibility of division by zero.
Here's a fixed and more Ruby-like implementation:
def self.average_volume(stocks)
# No data in means no data out, can't calculate.
return if (stocks.empty?)
# Pick out the `volume` value from each stock, then combine
# those with + using 0.0 as a default. This forces all of
# the subsequent values to be floating-point.
stocks.map(&:volume).reduce(0.0, &:+) / values.size
end
In Ruby it's strongly recommended to keep variable and method names in the x_y form, like average_volume here. Capitals have significant meaning and indicate constants like class, module and constant names.
You can test this method using a mock Stock:
require 'ostruct'
stocks = 10.times.map do |n|
OpenStruct.new(volume: n)
end
average_volume(stocks)
# => 4.5
average_volume([ ])
# => nil
If you're still getting infinity it's probably because you have a broken value somewhere in there for volume which is messing things up. You can try and filter those out:
stocks.map(&:value).reject(&:nan?)...
Where testing vs. nan? might be what you need to strip out junk data.

How to tackle this Ruby candy store homework?

I have a class in university that asks students to learn three languages in
one semester. Like one is from really old languages such as Haskell, the other one should be from interpreter languages.
So, now I have to learn Ruby, and I need help. Let's say there is class that has
class Help
##array = Array.new
##count = 0
#store
#chocolate
#candy
#store_code
store is string (name of store)
chocolate, candy, store_code are integer (price, and code number)
Lets consider that I have an add function and call it twice
def add (s, i, i, i)
array = [s, i, i, i]
count += 1
end
store_a = Help.new
store_a.add (A, 20, 1, 100)
store_b = Help.new
store_b.add (B, 50, 1, 100)
Anyway, store_a chocolate price is 20
store_b chocolate price is 50 now
How do I make a function inside of class that calculates average of chocolate price? (I make the count variable for this, but I don't know if I need it or not).
This can be refactored and made shorter, also you can make use of class variables like you mentioned in the question using "##", but my goal here is to keep it basic so you can start grasping it and slowly moving to more advanced techniques and designs:
class Warehouse
attr_accessor :products_stores
def initialize
#products_stores = []
end
def add_product(args)
#products_stores << args
end
def product_price_avg
avg = 0
#products_stores.each do |o|
avg += o[:product].price
end
avg / #products_stores.count
end
end
class Store
attr_accessor :code
def initialize(code)
#code = code
end
end
class Chocolate
attr_accessor :price
def initialize(price)
#price = price
end
end
store_a = Store.new(100)
store_b = Store.new(200)
cheap_chocolate = Chocolate.new(20)
expensive_chocolate = Chocolate.new(50)
warehouse = Warehouse.new
warehouse.add_product({store: store_a, product: cheap_chocolate})
warehouse.add_product({store: store_b, product: expensive_chocolate})
puts warehouse.product_price_avg

Adding together string-based minutes and seconds values

I have a a Track model that has a duration attribute. The attribute is string based, and reads in minutes:seconds format. I was wondering what the best way would be to take these string-based values and add them together. For example, if there are duration values like this:
Duration 1: "1:00"
Duration 2: "1:30"
how could I get it to output "2:30"?
Most of the questions I found related to this issue start with an integer based value. What's the best way to get this done?
My suggestion is to store/manipulate them as seconds.
It's definitely easier to store them as the integer number of seconds, and apply a function to parse/format the value into the proper string representation.
Storing them as integer will make it very easy to sum and subtract them.
Here is one way this can be done:
class Track
attr_accessor :duration
def initialize(duration)
#duration = duration
end
end
arr = [Track.new("1:00"), Track.new("1:30")]
total_seconds = arr.reduce(0) do |a, i|
min, sec = i.duration.split(":").map(&:to_i)
a + min * 60 + sec
end
p total_duration = '%d:%02d' % total_seconds.divmod(60)
#=> "2:30"
Edit: I missed #Wand's earlier answer, which is the same as mine. I'll leave mine just for the way I've organized the calculations.
arr = %w| 1:30 3:07 12:53 |
#=> ["1:30", "3:07", "12:53"]
"%d:%2d" % arr.reduce(0) do |tot,str|
m,s = str.split(':')
tot + 60*m.to_i + s.to_i
end.divmod(60)
#=> "17:30"
I just had to implement something like this in a recent project. Here is a simple start. If you are sure you will always have this format 'H:S', you will not need to convert your duration to time objects:
entries = ["01:00", "1:30", "1:45"]
hours = 0
minutes = 0
entries.each do |e|
entry_hours, entry_minutes = e.split(':', 2)
hours += entry_hours.to_i
minutes += entry_minutes.to_i
end
hours += (minutes/60)
minutes = (minutes%60)
p "#{hours}:#{minutes}"
#=> "4:15"
I agree with #SimoneCarletti: store them as an integer number of seconds. However, you could wrap them in a duration value class that can output itself as a nicely formatted string.
class Duration
attr_accessor :seconds
def initialize(string)
minutes, seconds = string.split(':', 2)
#seconds = minutes.to_i * 60 + seconds.to_i
end
def to_s
sprintf("%d:%02d", #seconds / 60, #seconds % 60)
end
def self.sum(*durations)
Duration.new(durations.reduce(0) { |t,d| t + d.seconds })
end
end
EDIT: Added a sum method similar to that suggested by #CarySwoveland below. You can use this as follows:
durations = ["1:30", "2:15"].map { |str| Duration.new(str) }
total = Duration.sum *durations

class argument/variable scope application / argument result references in subsequent argument

I'm tasked with making an interest calculator with class arguments, but I'm having a hard time applying variables/arguments. I keep getting an argument error (4 for 0). Moreover, how can I properly refer to the amount argument result in my `statement' argument? Any suggestions? Can anyone offer insight to help me understand scoping better within this context?
class InterestCalculator
attr_accessor :principal, :rate, :years, :times_compounded
def intitialize(principal, rate, years, times_compounded)
#principal, #rate, #years, #times_compounded = principal, rate, years, times_compounded
end
def amount
amount = #principal * (1 + #rate / #times_compounded) ** (#times_compounded * #years)
end
def statement
"After #{#years} I'll have #{amount} dollars!"
end
end
These are the specs:
describe InterestCalculator do
before { #calc = InterestCalculator.new(500, 0.05, 4, 5) }
describe "#amount" do
it "calculates correctly" do
expect( #calc.amount ).to eq(610.1)
end
end
describe "#statement" do
it "calls amount" do
#calc.stub(:amount).and_return(100)
expect( #calc.statement ).to eq("After 4 years I'll have 100 dollars!")
end
end
end
You've typoed your initialize method ("intitialize") so ruby thinks you are still using the default initialize method which takes no arguments, hence the error.
Frederick Cheung is correct however they are also looking for the number to be rounded "eq(610.1)"
def amount
amount = #principal * (1 + #rate / #times_compounded) ** (#times_compounded * #years)
end
Should be...
def amount
amount = #principal * (1 + #rate / #times_compounded) ** (#times_compounded * #years)
amount.round(2)
end

Ruby Rspec Timer - refactoring solution

I solved TestFirst.org question 09_timer for Ruby Rspec testing. My code works but I do not like it. It is very long. Any comments and/or suggestions for improving it would be greatly appreciated. Please include an explanation to clarify any suggestions. The goal was to create the Timer with an #seconds instance variable initialized to 0, and then return all values as a string with hours, minutes, seconds format: 00:00:00. So 12 seconds => 00:00:12; 66 seconds => 00:01:06; and 4000 seconds => 01:06:40.
Thank you. Code below.
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def padded(n)
"0#{n}"
end
def time_string
hours = #seconds/3600
h_minutes = ((#seconds%3600)/60)
minutes = #seconds/60
m_seconds = #seconds%60
second = #seconds
seconds = ""
if #seconds < 60
if second < 10
second = padded(second)
end
seconds << "00:00:#{second}"
elsif #seconds > 3600
if hours < 10
hours = padded(hours)
end
if h_minutes < 10
h_minutes = padded(h_minutes)
end
if m_seconds < 10
m_seconds = padded(m_seconds)
end
seconds << "#{hours}:#{h_minutes}:#{m_seconds}"
else
if minutes < 10
minutes = padded(minutes)
end
if m_seconds < 10
m_seconds = padded(m_seconds)
end
seconds << "00:#{minutes}:#{m_seconds}"
end
#seconds = seconds
end
end
There are several little things you can do to simplify your class, and a few large organizational changes.
1) Use String#rjust to pad the numbers:
def padded(n)
"#{n}".rjust(2, '0')
end
This lets you apply it to every number, regardless or whether or not it already has two digits. As a consequence, you can get rid of all of the single-digit checks (if h_minutes < 10, etc).
2) Get rid of everything starting from the first if statement, as none of it is necessary. Just a few lines before, you have hours = #seconds / 3600, h_minutes = ((#seconds%3600)/60), and m_seconds = #seconds%60, which are the only three values you need. Apply a simple map (for padding), and join with ":" to arrive at your final string.
3) If you want an object-oriented approach, each of your hours/minutes/seconds variables could be a method, so you end up with something more like this:
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def time_string
[hours, minutes, m_seconds].map(&method(:padded)).join(":")
end
def hours
seconds / 3600
end
def minutes
(seconds % 3600)/60
end
def m_seconds
(seconds % 60)
end
def padded(n)
"#{n}".rjust(2, '0')
end
end

Resources