Ruby Rspec Timer - refactoring solution - ruby

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

Related

How can I avoid using class variables in 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.

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

Timer RSpec Test Ruby

I am trying to solve the timer problem from TestFirst Ruby.
I got the first two criteria correctly but the third one when tested for time = 12 seconds does not work. It does not look like Ruby is reading the time = 12 secs.
The codes are pretty lazy and not optimized, apparently. Also I did tried out the padded method but the test never worked. I had my padded method defined as
def padded(num)
if num<=9
return "0"<<num.to_s
else
return num.to_s
end
end
It would be great if someone can show me how to set that up correctly since that might have been the problems.
Here are my complete codes:
class Timer
#initialization of seconds
def seconds
return 0
end
#seconds= method
def seconds=(time)
#seconds = time_string(time)
end
#time_string method
def time_string(time=0)
#format of hour:minute:second
#minute must be less than 59 (or 59*60 seconds), otherwise it will convert to hour
minute = time/60 #note that this is integer math, so it will take the minute and not the remainder
hour = minute/60
remainder_seconds = time%60
if time<=9
return "00:00:0" << time.to_s
elsif time>9 && time<=60
return "00:00:" << time.to_s
elsif time>60 && time<=9*60 #9 minutes and greater than 1 min
#ensuring double XX seconds or 0X seconds (this would be easier to use the padded method)
if remainder_seconds >9
remainder_seconds_sd = remainder_seconds.to_s
else
remainder_seconds_sd = "0" << remainder_seconds.to_s
end
return "00:0" << minute.to_s << ":" << remainder_seconds_sd
end
end
end
RSpec below:
require '09_timer'
describe "Timer" do
before(:each) do
#timer = Timer.new
end
it "should initialize to 0 seconds" do
#timer.seconds.should == 0
end
describe 'time_string' do
it "should display 0 seconds as 00:00:00" do
#timer.seconds = 0
#timer.time_string.should == "00:00:00"
end
it "should display 12 seconds as 00:00:12" do
#timer.seconds = 12
#timer.time_string.should == "00:00:12"
end
it "should display 66 seconds as 00:01:06" do
#timer.seconds = 66
#timer.time_string.should == "00:01:06"
end
it "should display 4000 seconds as 01:06:40" do
#timer.seconds = 4000
#timer.time_string.should == "01:06:40"
end
end
# One way to implement the Timer is with a helper method.
# Uncomment these specs if you want to test-drive that
# method, then call that method from inside of time_string.
#
=begin
describe 'padded' do
it 'pads zero' do
#timer.padded(0).should == '00'
end
it 'pads one' do
#timer.padded(1).should == '01'
end
it "doesn't pad a two-digit number" do
#timer.padded(12).should == '12'
end
end
=end
end
The problem with your tests and Timer is that, in your tests you are setting the value of #timer.seconds, but the Timer#time_string does not rely on the #seconds variable set. Your time_string method is implemented the way it accepts the amount of seconds as an argument, not an attribute of Timer.
Try changing your tests as follows:
describe "Timer" do
# rest of your code
describe 'time_string' do
it "should display 0 seconds as 00:00:00" do
#timer.time_string(0).should == "00:00:00"
end
it "should display 12 seconds as 00:00:12" do
#timer.time_string(12).should == "00:00:12"
end
it "should display 66 seconds as 00:01:06" do
#timer.time_string(66).should == "00:01:06"
end
it "should display 4000 seconds as 01:06:40" do
#timer.time_string(4000).should == "01:06:40"
end
end
end
You might be wondering okay, but why the first test - 00:00:00 - did work in first place?. Well, this is, because your time_string method argument defaults to 0:
def time_string(time=0)
# Rest of the code
end
and because you were not passing any other value, the 0 has been used.
If you have any questions - I'm happy to help!
Good luck!
Edit
If you want to make it the other way around - make the class to work for your tests, change your Timer class:
class Timer
def initialize
#seconds = 0
end
def seconds
#seconds
end
def seconds=(time)
#seconds = time
end
def time_string
#format of hour:minute:second
#minute must be less than 59 (or 59*60 seconds), otherwise it will convert to hour
minute = #seconds/60 #note that this is integer math, so it will take the minute and not the remainder
hour = minute/60
remainder_seconds = #seconds%60
if #seconds<=9
return "00:00:0" << #seconds.to_s
elsif #seconds>9 && #seconds<=60
return "00:00:" << #seconds.to_s
elsif #seconds>60 && #seconds<=9*60 #9 minutes and greater than 1 min
#ensuring double XX seconds or 0X seconds (this would be easier to use the padded method)
if remainder_seconds >9
remainder_seconds_sd = remainder_seconds.to_s
else
remainder_seconds_sd = "0" << remainder_seconds.to_s
end
return "00:0" << minute.to_s << ":" << remainder_seconds_sd
end
end
end
We have added initialize method, we have changed def seconds=(time) method, and we have changed all occurrences of time in your time_string method.
If that works for you, consider posting the code to https://codereview.stackexchange.com/. There is a lot in the code to improve, and codereview is a great place to ask for help!
A cleaner version:
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def time_string
seconds = #seconds % 60
minutes = (#seconds / 60) % 60
hours = #seconds / (60**2)
"#{padded(hours)}:#{padded(minutes)}:#{padded(seconds)}"
end
def padded(num)
return '0' + num.to_s if num < 10
return num.to_s if num >= 10
end
end

How to define new loop function

I have a medhod like this.
def run
loop do
sleep 0.1
# do something
end
end
And I want to write it like this.
def run
every 100, :msec do
# do something
end
end
How can I write a method like this every?
def every(quantity, units = :sec)
# this could be improved but you get the idea
quantity = quantity / 1000.0 if units == :msec
loop do
sleep quantity
yield
end
end
every 100, :msec do
puts Time.now
end

ping function every x amount seconds

How would I do a specific task every x amount of seconds in ruby? I've tried using Time.now.to_i for epoch then once a Time.now_i hits that task second it executes, but I have not successfuly done this, can someone show me a small example on how to execute a function every x amount of seconds?
Attempt:
def interval(timeout,function,*data)
now = Time.now.to_i
tasktime = Time.now.to_i + timeout
taskfunction = function
taskdata = data
end
I stopped the code there because I do not know how/what to do next in ruby, so what it should do for example if someone can generate a code that can do something like this example,
def say(word)
puts word
end
If you set a interval for the function would be say, the data would be the "word" then it would execute that function every x amount of seconds
If you simply sleep for a constant amount of time as suggested in other answers, the error will contaminate as it keeps running, and will not be accurate. In fact, each iteration would take longer than the given interval.
The answer shown below adjusts the lag each time per iteration.
module Kernel
def tick_every sec, &pr
Thread.new do loop do
pr.call
t = Time.now.to_f
frac = t.modulo(sec.to_f)
sleep(sec - frac)
end end
end
end
thread = tick_every(2) do
puts "foo"
end
...
some_other_tasks
...
thread.kill
You can use Kernel#sleep method for the same.
Here is the post
Ruby sleep or delay less than a second?
Tell Ruby Program to Wait some amount of time
This method would puts the word every 2 seconds endless, synchronously (means other ruby code has to wait until this execution is finished (..endless..:)).
def say(word)
while true do
t = Time.now.to_f
puts word
frac = t.modulo(2.to_f)
sleep(2 - frac)
end
end

Resources