Ruby's range step method causes very slow execution? - ruby-on-rails

I've got this block of code:
date_counter = Time.mktime(2011,01,01,00,00,00,"+05:00")
#weeks = Array.new
(date_counter..Time.now).step(1.week) do |week|
logger.debug "WEEK: " + week.inspect
#weeks << week
end
Technically, the code works, outputting:
Sat Jan 01 00:00:00 -0500 2011
Sat Jan 08 00:00:00 -0500 2011
Sat Jan 15 00:00:00 -0500 2011
etc.
But the execution time is complete rubbish! It takes approximately four seconds to compute each week.
Is there some grotesque inefficiency that I'm missing in this code? It seems straight-forward enough.
I'm running Ruby 1.8.7 with Rails 3.0.3.

Assuming MRI and Rubinius use similar methods to generate the range the basic algorithm used with all the extraneous checks and a few Fixnum optimisations etc. removed is:
class Range
def each(&block)
current = #first
while current < #last
yield current
current = current.succ
end
end
def step(step_size, &block)
counter = 0
each do |o|
yield o if counter % step_size = 0
counter += 1
end
end
end
(See the Rubinius source code)
For a Time object #succ returns the time one second later. So even though you are asking it for just each week it has to step through every second between the two times anyway.
Edit: Solution
Build a range of Fixnum's since they have an optimised Range#step implementation.
Something like:
date_counter = Time.mktime(2011,01,01,00,00,00,"+05:00")
#weeks = Array.new
(date_counter.to_i..Time.now.to_i).step(1.week).map do |time|
Time.at(time)
end.each do |week|
logger.debug "WEEK: " + week.inspect
#weeks << week
end

Yes, you are missing a gross inefficiency. Try this in irb to see what you're doing:
(Time.mktime(2011,01,01,00,00,00,"+05:00") .. Time.now).each { |x| puts x }
The range operator is going from January 1 to now in increments of one second and that's a huge list. Unfortunately, Ruby isn't clever enough to combine the range generation and the one-week chunking into a single operation so it has to build the entire ~6million entry list.
BTW, "straight forward" and "gross inefficiency" are not mutually exclusive, in fact they're often concurrent conditions.
UPDATE: If you do this:
(0 .. 6000000).step(7*24*3600) { |x| puts x }
Then the output is produced almost instantaneously. So, it appears that the problem is that Range doesn't know how to optimize the chunking when faced with a range of Time objects but it can figure things out quite nicely with Fixnum ranges.

Related

Ruby time multiplication

Can some break this down for me? In my mind 5 minutes squared is 25 minutes
irb(main):014:0> now = Time.now.utc
=> 2019-05-03 01:36:41 UTC
irb(main):015:0> now + (5.minutes ** 2)
=> 2019-05-04 02:36:41 UTC
There is no Numeric#minutes in ruby, that’s Rails monkeypatching everything.
Numeric#minutes is delegated to ActiveSupport::Duration#minutes which in turn constructs an ActiveSupport::Duratioon::Scalar instance, with an amount of seconds as a “number behind.” That number will be used in:
coercion that might be used in any arithmetics involving Numeric, amongst others.
That said, when foo.minutes meets the arithmetic operation with a Numeric as a RHO, it [using coercion] does the math using the number of seconds.
Even more, the comparison against Numeric would also work:
5.minutes == 300
#⇒ true
Hence my advise: never ever use these misleading monkeypatched crap. Use seconds explicitly to perform date/time operations.

Measure and Benchmark Time for Ruby Methods

How can i measure the time taken by a method and the individual statements in that method in Ruby. If you see the below method i want to measure the total time taken by the method and the time taken for database access and redis access. I do not want to write Benchmark.measure before every statement. Does the ruby interpreter gives us any hooks for doing this ?
def foo
# code to access database
# code to access redis.
end
The simplest way:
require 'benchmark'
def foo
time = Benchmark.measure {
code to test
}
puts time.real #or save it to logs
end
Sample output:
2.2.3 :001 > foo
5.230000 0.020000 5.250000 ( 5.274806)
Values are: cpu time, system time, total and real elapsed time.
Source: ruby docs.
You could use the Time object. (Time Docs)
For example,
start = Time.now
# => 2022-02-07 13:55:06.82975 +0100
# code to time
finish = Time.now
# => 2022-02-07 13:55:09.163182 +0100
diff = finish - start
# => 2.333432
diff would be in seconds, as a floating point number.
Use Benchmark's Report
require 'benchmark' # Might be necessary.
def foo
Benchmark.bm( 20 ) do |bm| # The 20 is the width of the first column in the output.
bm.report( "Access Database:" ) do
# Code to access database.
end
bm.report( "Access Redis:" ) do
# Code to access redis.
end
end
end
This will output something like the following:
user system total real
Access Database: 0.020000 0.000000 0.020000 ( 0.475375)
Access Redis: 0.000000 0.000000 0.000000 ( 0.000037)
<------ 20 -------> # This is where the 20 comes in. NOTE: This is not shown in output.
More information can be found here.
Many of the answers suggest the use of Time.now. But it is worth being aware that Time.now can change. System clocks can drift and might get corrected by the system's administrator or via NTP. It is therefore possible for Time.now to jump forward or back and give your benchmarking inaccurate results.
A better solution is to use the operating system's monotonic clock, which is always moving forward. Ruby 2.1 and above give access to this via:
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# code to time
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
diff = finish - start # gets time is seconds as a float
You can read more details here. Also you can see popular Ruby project, Sidekiq, made the switch to monotonic clock.
A second thought, define the measure() function with Ruby code block argument can help simplify the time measure code:
def measure(&block)
start = Time.now
block.call
Time.now - start
end
# t1 and t2 is the executing time for the code blocks.
t1 = measure { sleep(1) }
t2 = measure do
sleep(2)
end
In the spirit of wquist's answer, but a little simpler, you could also do it like below:
start = Time.now
# code to time
Time.now - start

Using distance_of_time_in_words in Rails

I have a start month (3), start year (2004), and I have an end year (2008). I want to calculate the time in words between the start and end dates. This is what I'm trying and it's not working..
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year + "/" + start_month + "/01").to_date
ed = (end_year + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
..this throws an error, "string can't me coerced into fixnum". Anyone seen this error?
You can't just concatenate strings and numbers in Ruby. You should either convert numbers to strings as mliebelt suggested or use string interpolation like that:
st = "#{start_year}/#{start_month}/01".to_date
But for your particular case I think there is no need for strings at all. You can do it like that:
st = Date.new(start_year, start_month, 1)
ed = Date.new(end_year, 1, 1)
distance_of_time_in_words(st, ed)
or even like that:
st = Date.new(start_year, start_month)
ed = Date.new(end_year)
distance_of_time_in_words(st, ed)
See Date class docs for more information.
Given that the context in which you are calling the method is one that knows the methods from ActionView::Helpers::DateHelper, you should change the following:
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year.to_s + "/" + start_month.to_s + "/01").to_date
ed = (end_year.to_s + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
=> "almost 3 years"
So I have added calls to to_s for the numbers, to ensure that the operation + is working. There may be more efficient ways to construct a date, but yours is sufficient.

How can i check whether the current time in between tonight 9pm and 9am(tomorrow) in Ruby on Rails

I need to check whether my current times is between the specified time interval (tonight 9pm and 9am tomorrow). How can this be done in Ruby on Rails.
Thanks in advance
Obviously this is an old question, already marked with a correct answer, however, I wanted to post an answer that might help people finding the same question via search.
The problem with the answer marked correct is that your current time may be past midnight, and at that point in time, the proposed solution will fail.
Here's an alternative which takes this situation into account.
now = Time.now
if (0..8).cover? now.hour
# Note: you could test for 9:00:00.000
# but we're testing for BEFORE 9am.
# ie. 8:59:59.999
a = now - 1.day
else
a = now
end
start = Time.new a.year, a.month, a.day, 21, 0, 0
b = a + 1.day
stop = Time.new b.year, b.month, b.day, 9, 0, 0
puts (start..stop).cover? now
Again, use include? instead of cover? for ruby 1.8.x
Of course you should upgrade to Ruby 2.0
Create a Range object having the two Time instances that define the range you want, then use the #cover? method (if you are on ruby 1.9.x):
now = Time.now
start = Time.gm(2011,1,1)
stop = Time.gm(2011,12,31)
p Range.new(start,stop).cover? now # => true
Note that here I used the explicit method constructor just to make clear that we are using a Range instance. You could safely use the Kernel constructor (start..stop) instead.
If you are still on Ruby 1.8, use the method Range#include? instead of Range#cover?:
p (start..stop).include? now
require 'date'
today = Date.today
tomorrow = today + 1
nine_pm = Time.local(today.year, today.month, today.day, 21, 0, 0)
nine_am = Time.local(tomorrow.year, tomorrow.month, tomorrow.day, 9, 0, 0)
(nine_pm..nine_am).include? Time.now #=> false
This might read better in several situations and the logic is simpler if you have 18.75 for "18:45"
def afterhours?(time = Time.now)
midnight = time.beginning_of_day
starts = midnight + start_hours.hours + start_minutes.minutes
ends = midnight + end_hours.hours + end_minutes.minutes
ends += 24.hours if ends < starts
(starts...ends).cover?(time)
end
I'm using 3 dots because I don't consider 9:00:00.000am after hours.
Then it's a different topic, but it's worth highlighting that cover? comes from Comparable (like time < now), while include? comes from Enumerable (like array inclusion), so I prefer to use cover? when possible.
Here is how I check if an event is tomorrow in Rails 3.x
(event > Time.now.tomorrow.beginning_of_day) && (event < Time.now.tomorrow.end_of_day)
if time is between one day:
(start_hour..end_hour).include? Time.zone.now.hour

How to find if range is contained in an array of ranges?

Example
business_hours['monday'] = [800..1200, 1300..1700]
business_hours['tuesday'] = [900..1100, 1300..1700]
...
I then have a bunch of events which occupy some of these intervals, for example
event = { start_at: somedatetime, end_at: somedatetime }
Iterating over events from a certain date to a certain date, I create another array
busy_hours['monday'] = [800..830, 1400..1415]
...
Now my challenges are
Creating an available_hours array that contains business_hours minus busy_hours
available_hours = business_hours - busy_hours
Given a certain duration say 30 minutes, find which time slots are available in available_hours. In the examples above, such a method would return
available_slots['monday'] = [830..900, 845..915, 900..930, and so on]
Not that it checks available_hours in increments of 15 minutes for slots of specified duration.
Thanks for the help!
I think this is a job for bit fields. Unfortunately this solution will rely on magic numbers, conversions helpers and a fair bit of binary logic, so it won't be pretty. But it will work and be very efficient.
This is how I'd approach the problem:
Atomize your days into reasonable time intervals. I'll follow your example and treat each 15 minute block of time as considered one time chunk (mostly because it keeps the example simple). Then represent your availability per hour as a hex digit.
Example:
0xF = 0x1111 => available for the whole hour.
0xC = 0x1100 => available for the first half of the hour.
String 24 of these together together to represent a day. Or fewer if you can be sure that no events will occur outside of the range. The example continues assuming 24 hours.
From this point on I've split long Hex numbers into words for legibility
Assuming the day goes from 00:00 to 23:59 business_hours['monday'] = 0x0000 0000 FFFF 0FFF F000 0000
To get busy_hours you store events in a similar format, and just & them all together.
Exmample:
event_a = 0x0000 0000 00F0 0000 0000 0000 # 10:00 - 11:00
event_b = 0x0000 0000 0000 07F8 0000 0000 # 13:15 - 15:15
busy_hours = event_a & event_b
From busy_hours and business_hours you can get available hours:
available_hours = business_hours & (busy_hours ^ 0xFFFF FFFF FFFF FFFF FFFF FFFF)
The xor(^) essentialy translates busy_hours into not_busy_hours. Anding (&) not_busy_hours with business_hours gives us the available times for the day.
This scheme also makes it simple to compare available hours for many people.
all_available_hours = person_a_available_hours & person_b_available_hours & person_c_available_hours
Then to find a time slot that fits into available hours. You need to do something like this:
Convert your length of time into a similar hex digit to the an hour where the ones represent all time chunks of that hour the time slot will cover. Next right shift the digit so there's no trailing 0's.
Examples are better than explanations:
0x1 => 15 minutes, 0x3 => half hour, 0x7 => 45 minutes, 0xF => full hour, ... 0xFF => 2 hours, etc.
Once you've done that you do this:
acceptable_times =[]
(0 .. 24 * 4 - (#of time chunks time slot)).each do |i|
acceptable_times.unshift(time_slot_in_hex) if available_hours & (time_slot_in_hex << i) == time_slot_in_hex << i
end
The high end of the range is a bit of a mess. So lets look a bit more at it. We don't want to shift too many times or else we'll could start getting false positives at the early end of the spectrum.
24 * 4 24 hours in the day, with each represented by 4 bits.
- (#of time chunks in time slot) Subtract 1 check for each 15 minutes in the time slot we're looking for. This value can be found by (Math.log(time_slot_in_hex)/Math.log(2)).floor + 1
Which starts at the end of the day, checking each time slot, moving earlier by a time chunk (15 minutes in this example) on each iteration. If the time slot is available it's added to the start of acceptable times. So when the process finishes acceptable_times is sorted in order of occurrence.
The cool thing is this implementation allows for time slots that incorporate so that your attendee can have a busy period in their day that bisects the time slot you're looking for with a break, where they might be otherwise busy.
It's up to you to write helper functions that translate between an array of ranges (ie: [800..1200, 1300..1700]) and the hex representation. The best way to do that is to encapsulate the behaviour in an object and use custom accessor methods. And then use the same objects to represent days, events, busy hours, etc. The only thing that's not built into this scheme is how to schedule events so that they can span the boundary of days.
To answer your question's title, find if a range of arrays contains a range:
ary = [800..1200, 1300..1700]
test = 800..830
p ary.any? {|rng| rng.include?(test.first) and rng.include?(test.last)}
# => true
test = 1245..1330
p ary.any? {|rng| rng.include?(test.first) and rng.include?(test.last)}
# => false
which could be written as
class Range
def include_range?(r)
self.include?(r.first) and self.include?(r.last)
end
end
Okay, I don't have time to write up a full solution, but the problem does not seem too difficult to me. I hacked together the following primitive methods you can use to help in constructing your solution (You may want to subclass Range rather than monkey patching, but this will give you the idea):
class Range
def contains(range)
first <= range.first || last >= range.last
end
def -(range)
out = []
unless range.first <= first && range.last >= last
out << Range.new(first, range.first) if range.first > first
out << Range.new(range.last, last) if range.last < last
end
out
end
end
You can iterate over business hours and find the one that contains the event like so:
event_range = event.start_time..event.end_time
matching_range = business_hours.find{|r| r.contains(event_range)}
You can construct the new array like this (pseudocode, not tested):
available_hours = business_hours.dup
available_hours.delete(matching_range)
available_hours += matching_range - event_range
That should be a pretty reusable approach. Of course you'll need something totally different for the next part of your question, but this is all I have time for :)

Resources