How to define new loop function - ruby

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

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.

Why is delete_at not removing elements at supplied index?

I have a Worker and Job example, where each Job has an expensive/slow perform method.
If I have 10 Jobs in my #job_table I'd like to work them off in batches of 5, each within their own process.
After the 5 processes (one batch) have exited I'm trying to remove those Jobs from the #job_table with delete_at.
I'm observing something unexpected in my implementation (see code below) though:
jobs:
[#<Job:0x007fd2230082a8 #id=0>,
#<Job:0x007fd223008280 #id=1>,
#<Job:0x007fd223008258 #id=2>,
#<Job:0x007fd223008208 #id=3>,
#<Job:0x007fd2230081e0 #id=4>,
#<Job:0x007fd2230081b8 #id=5>,
#<Job:0x007fd223008190 #id=6>,
#<Job:0x007fd223008168 #id=7>,
#<Job:0x007fd223008140 #id=8>,
#<Job:0x007fd223008118 #id=9>]
This is the #job_table before the first batch is run. I see that Jobs 0-4 have run and exited successfully (omitted output here).
So I'm calling remove_batch_1 and would expect jobs 0-4 to be removed from the #job_table, but this is what I'm observing instead:
jobs:
[#<Job:0x007fd223008280 #id=1>,
#<Job:0x007fd223008208 #id=3>,
#<Job:0x007fd2230081b8 #id=5>,
#<Job:0x007fd223008168 #id=7>,
#<Job:0x007fd223008118 #id=9>]
I've logged the i parameter in the method and it returns 0-4. But it looks like delete_at is removing other jobs (0,2,4,6,8).
I also wrote another method for removing a batch remove_batch_0 which uses slice! and behaves as expected.
BATCH_SIZE = 5 || ENV['BATCH_SIZE']
class Job
def initialize(id)
#id = id
end
def perform
puts "Job #{#id}> Start!"
sleep 1
puts "Job #{#id}> End!"
end
end
class Worker
def initialize
#job_table = []
fill_job_table
work_job_table
end
def fill_job_table
10.times do |i|
#job_table << Job.new(i)
end
end
def work_job_table
until #job_table.empty?
puts "jobs: "
pp #job_table
work_batch
Process.waitall
remove_batch_1
end
end
def work_batch
i = 0
while (i < #job_table.length && i < BATCH_SIZE)
fork { #job_table[i].perform }
i += 1
end
end
def remove_batch_1
i = 0
while (i < #job_table.length && i < BATCH_SIZE)
#job_table.delete_at(i)
i += 1
end
end
def remove_batch_0
#job_table.slice!(0..BATCH_SIZE-1)
end
end
Worker.new
You use delete_at in a while loop. Let's see what happens:
Image you have an array [0,1,2,3,4,5] and you call:
(1..3).each { |i| array.deleted_at(i) }
In the first iteration you will delete the first element from the array, the array will look like this after this step: [1,2,3,4,5] In the next iteration you will delete the second element, what leads to [1,3,4,5]. Then you delete the third: [1,3,5]
You might want to use Array#shift instead:
def remove_batch_1
#job_table.shift(BATCH_SIZE)
end

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

Ruby Put Periodic Progress Messages While Mapping

I am mapping an array of items, but the collection can be quite large. I would like to put a message to console every so often, to give an indication of progress. Is there a way to do that during the mapping process?
This is my map statement:
famgui = family_items.map{|i|i.getGuid}
I have a def that I use for giving an update when I am doing a for each or while loop.
This is the def:
def doneloop(saymyname, i)
if (i%25000 == 0 )
puts "#{i} #{saymyname}"
end
end
I normally put x = 0 before I start the loop, then x +=1 once I am in the loop and then at the end of my loop, I put saymyname = "specific type items gathered at #{Time.now}"
Then I put doneloop(saymyname, x)
I am not sure how to do that when I am mapping, as there is no loop to construct this around. Does anyone have a method to give updates when using map?
Thanks!
You can map with index:
famgui = family_items.with_index.map {|item, index| item.getGuid; doneloop('sth', index)}
Only the last expression is returned from a map, so you can do something like:
famgui = family_items.with_index.map do |i, idx|
if idx % 100 == 0
puts # extra linefeed
# report every 100th round
puts "items left: #{family_items_size - idx}"
STDOUT.flush
end
current_item += 1
print "."
STDOUT.flush
i.getGuid
end
This will print "." for each item and a status report after every 100 items.
If you want, you can use each_with and populate the array yourself like:
famgui = []
family_items.each_with_index do |i, idx|
famgui << i.getGuid
puts "just did: #{idx} of #{family_items.size}"
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