Time-of-day range in Ruby? - ruby

I want to know if a time belongs to an schedule or another.
In my case is for calculate if the time is in night schedule or normal schedule.
I have arrived to this solution:
NIGHT = ["21:00", "06:00"]
def night?( date )
date_str = date.strftime( "%H:%M" )
date_str > NIGHT[0] || date_str < NIGHT[1]
end
But I think is not very elegant and also only works for this concrete case and not every time range.
(I've found several similar question is SO but all of them make reference to Date ranges no Time ranges)
Updated
Solution has to work for random time ranges not only for this concrete one. Let's say:
"05:00"-"10:00"
"23:00"-"01:00"
"01:00"-"01:10"

This is actually more or less how I would do it, except maybe a bit more concise:
def night?( date )
!("06:00"..."21:00").include?(date.strftime("%H:%M"))
end
or, if your schedule boundaries can remain on the hour:
def night?(date)
!((6...21).include? date.hour)
end
Note the ... - that means, basically, "day time is hour 6 to hour 21 but not including hour 21".
edit: here is a generic (and sadly much less pithy) solution:
class TimeRange
private
def coerce(time)
time.is_a? String and return time
return time.strftime("%H:%M")
end
public
def initialize(start,finish)
#start = coerce(start)
#finish = coerce(finish)
end
def include?(time)
time = coerce(time)
#start < #finish and return (#start..#finish).include?(time)
return !(#finish..#start).include?(time)
end
end
You can use it almost like a normal Range:
irb(main):013:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,02,30))
=> true
irb(main):014:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,01,30))
=> false
irb(main):015:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,01,30))
=> true
irb(main):016:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,02,30))
=> false
Note, the above class is ignorant about time zones.

In Rails 3.2 it has added Time.all_day and similars as a way of generating date ranges. I think you must see how it works. It may be useful.

Related

(Ruby) Padding single digits when it comes to time

I have 2 methods in a Timer class I'm creating. One method is where hours, minutes, and seconds are calculated from any given amount of seconds and the other method will pad any single digits with a "0". Every things I've look up so far isn't work for me. Below is my code:
class Timer
attr_accessor :seconds=(time), :time_string
def initialize(seconds = 00)
#seconds = seconds
end
def time_string
hours = padded((#seconds/3600)
minutes = padded(#seconds/60 - hours * 60)
seconds = padded(#seconds%60)
puts '#{hours):#{minutes}:#{seconds}'
end
def padded(x)
if x.length == 1
"0"+x
end
end
end
so if I put in 7683, the output I want to get is "02:08:03". but when I execute it, I get the following error message:
(eval):6: (eval):6:in `-': String can't be coerced into Fixnum (TypeError)
from (eval):6:in `time'
from (eval):19
I'm confused where this is erroring out.
To answer your question as to why your code is not working, you have got couple of conversion issues between FixNum and String throughout your code, you can fix it as follows:
def time_string(seconds)
hours = seconds/3600
minutes = seconds/60 - (hours * 60)
seconds = seconds%60
puts padded(hours)+':'+padded(minutes)+':'+padded(seconds)
end
You use the hours variable in the second statement, but because its already converted to string, it crashes, therefore its better to do all the calculations first, and only later use the padded method which returns the padded digits in string format. The padded method must also be modified to be consistent:
def padded(x)
if x.to_s.length == 1
return "0"+x.to_s
else
return x.to_s
end
end
Just keep in mind that the combination of the two methods will work only for numbers up to 86399, which will return 23:59:59. Any number passed to time_string bigger than that will pass the 24 hour mark and will return something like: 26:00:00
There is a brilliant method for padding, why not use it?
3.to_s.rjust(10,"*") #=> "*********3"
4.to_s.rjust(2,"0") #=> "04"
44.to_s.rjust(2,"0") #=> "44"
If you want a better solution than writing your own class, use at
Time.at(7683).strftime("%H:%M:%S") #=> "02:08:03"
There's no need to reinvent the wheel.
t = 7683 # seconds
Time.at(t).strftime("%H:%M:%S")
Time.at(seconds) converts your seconds into a time object, which then you can format with strftime. From the strftime documentation you can see you can get the parameters you want non padded, white padded or zero padded.
I tend to use something like this
"%02d" % 2 #=> 02
"%02d" % 13 #=> 13
It's part of the Kernel module: http://ruby-doc.org/core-2.1.3/Kernel.html#M001433

Ruby, Recursion Method

I have a method that is basically a loop and it calls itself at the end each time. What is the best way for the method to not call itself when the date reaches a certain point? Each iteration through adds 1 day and basically processes stats for that day. It looks like the below:
def loop(start_day)
date = start_day
#do a bunch of stuff
date = date +1.day
if date > Time.now
puts "loop should be over"
end
loop(date)
end
Each iteration through adds 1 day
That's not true for the code you've posted. In your code you add 1 day to the start date once and then you keep processing the same date over and over again because you recurse on the old date (start_date), not the incremented date (date).
What is the best way for the method to not call itself when the date reaches a certain point?
Just put the recursive call inside an if or, in this case, inside of the else of the if that you already have.
Since you set date to start_date immediately, it seems there's no point in having both. Here's the more canonical form of doing recursion:
def loop(date)
return if date > Time.now
#do a bunch of stuff
loop(date + 1.day)
end
Update: If it's not obvious to you that recursion isn't necessary here, in real life, it would make more sense to do something like this:
def process_from(date)
while date <= Time.now
# Process date
date += 1.day
end
end
What about this?
def loop(start_day)
return "loop should be over" if start_day >= Time.now
#...
loop(modified_date)
end
or...
def loop(start_day)
date = start_day.dup
time = Time.now
date += 1.day while date <= time
'loop should be over'
end
It seems like you want to iterate over all days from starting date to today. Then maybe this is even more simple:
def map_date(date)
(date.to_date..Date.today).map do |d|
d.strftime("Today is %A")
end
end
You must have base case (stop case) for recursive function;
example:
def loop(date)
if date == certain_point
return [something_for_stop]
end
loop(date - 1)
end

Ruby: intersection between two ranges

In ruby, given two date ranges, I want the range that represents the intersection of the two date ranges, or nil if no intersection. For example:
(Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
=> Mon, 10 Jan 2011..Sat, 15 Jan 2011
Edit: Should have said that I want it to work for DateTime as well, so interval can be down to mins and secs:
(DateTime.new(2011,1,1,22,45)..Date.new(2011,2,15)) & (Date.new(2011,1,1)..Date.new(2011,2,15))
=> Sat, 01 Jan 2011 22:45:00 +0000..Tue, 15 Feb 2011
require 'date'
class Range
def intersection(other)
return nil if (self.max < other.begin or other.max < self.begin)
[self.begin, other.begin].max..[self.max, other.max].min
end
alias_method :&, :intersection
end
p (Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
#<Date: 2011-01-10 ((2455572j,0s,0n),+0s,2299161j)>..#<Date: 2011-01-15 ((2455577j,0s,0n),+0s,2299161j)>
You can try this to get a range representing intersection
range1 = Date.new(2011,12,1)..Date.new(2011,12,10)
range2 = Date.new(2011,12,4)..Date.new(2011,12,12)
inters = range1.to_a & range2.to_a
intersected_range = inters.min..inters.max
Converting your example:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
inters = self.to_a & other.to_a
inters.empty? ? nil : inters.min..inters.max
end
alias_method :&, :intersection
end
I found this: http://www.postal-code.com/binarycode/2009/06/06/better-range-intersection-in-ruby/ which is a pretty good start, but does not work for dates. I've tweaked a bit into this:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil
new_max = self.cover?(other.max) ? other.max : other.cover?(max) ? max : nil
new_min && new_max ? new_min..new_max : nil
end
alias_method :&, :intersection
end
I've omitted the tests, but they are basically the tests from the post above changed for dates. This works for ruby 1.9.2.
Anyone got a better solution?
I baked this solution for ascending ranges, also taking care of the exclude end situations:
intersect_ranges = ->(r1, r2) do
new_end = [r1.end, r2.end].min
new_begin = [r1.begin, r2.begin].max
exclude_end = (r2.exclude_end? && new_end == r2.end) || (r1.exclude_end? && new_end == r1.end)
valid = (new_begin <= new_end && !exclude_end)
valid ||= (new_begin < new_end && exclude_end))
valid ? Range.new(new_begin, new_end, exclude_end) : nil
end
I'm also a bit worried by you guys adding it to the Range class itself, since the behavior of intersecting ranges is not uniformly defined.
(How about intersecting 1...4 and 4...1? Why nil when there is no intersection; we could also say this is an empty range: 1...1 )
You can use overlaps? with Range starting with Rails v3
# For dates, make sure you have the correct format
first_range = first_start.to_date..first_end.to_date
second_range = second_start.to_date..second_end.to_date
intersection = first_range.overlaps?(second_range) # => Boolean
# Example with numbers
(1..7).overlaps?(3..5) # => true
More details in the docs
Try something like this
require 'date'
sample = Date.parse('2011-01-01')
sample1 = Date.parse('2011-01-15')
sample2 = Date.parse('2010-12-19')
sample3 = Date.parse('2011-01-11')
puts (sample..sample1).to_a & (sample2..sample3).to_a
What this will give you is a array of intersection dates!!
I have times as [[start, end], ...] and I want to remove the some time ranges from a each initial time range, here is what I did:
def exclude_intersecting_time_ranges(initial_times, other_times)
initial_times.map { |initial_time|
other_times.each do |other_time|
next unless initial_time
# Other started after initial ended
next if other_time.first >= initial_time.last
# Other ended before initial started
next if other_time.last <= initial_time.first
# if other time started before and ended after after, no hour is counted
if other_time.first <= initial_time.first && other_time.last >= initial_time.last
initial_time = nil
# if other time range is inside initial time range, split in two time ranges
elsif initial_time.first < other_time.first && initial_time.last > other_time.last
initial_times.push([other_time.last, initial_time.last])
initial_time = [initial_time.first, other_time.first]
# if start time of other time range is before initial time range
elsif other_time.first <= initial_time.first
initial_time = [other_time.last, initial_time.last]
# if end time of other time range if after initial time range
elsif other_time.last >= initial_time.last
initial_time = [initial_time.first, other_time.first]
end
end
initial_time
}.compact
end
Since this question is related to How to combine overlapping time ranges (time ranges union), I also wanted to post my finding of the gem range_operators here, because if helped me in the same situation.
I'd transfer them into an array, since arrays know the intersection-operation:
(Date.new(2011,1,1)..Date.new(2011,1,15)).to_a & (Date.new(2011,1,10)..Date.new(2011,2,15)).to_a
Of course this returns an Array. So if you want an Enumerator (Range doesn't seem to be possible since these are not consecutive values anymore) just throw to_enum at the end.

How do I get elapsed time in milliseconds in Ruby?

If I have a Time object got from :
Time.now
and later I instantiate another object with that same line, how can I see how many milliseconds have passed? The second object may be created that same minute, over the next minutes or even hours.
As stated already, you can operate on Time objects as if they were numeric (or floating point) values. These operations result in second resolution which can easily be converted.
For example:
def time_diff_milli(start, finish)
(finish - start) * 1000.0
end
t1 = Time.now
# arbitrary elapsed time
t2 = Time.now
msecs = time_diff_milli t1, t2
You will need to decide whether to truncate that or not.
You can add a little syntax sugar to the above solution with the following:
class Time
def to_ms
(self.to_f * 1000.0).to_i
end
end
start_time = Time.now
sleep(3)
end_time = Time.now
elapsed_time = end_time.to_ms - start_time.to_ms # => 3004
I think the answer is incorrectly chosen, that method gives seconds, not milliseconds.
t = Time.now.t­o_f
=> 1382471965.146
Here I suppose the floating value are the milliseconds
DateTime.now.strftime("%Q")
Example usage:
>> DateTime.now.strftime("%Q")
=> "1541433332357"
>> DateTime.now.strftime("%Q").to_i
=> 1541433332357
To get time in milliseconds, it's better to add .round(3), so it will be more accurate in some cases:
puts Time.now.to_f # => 1453402722.577573
(Time.now.to_f.round(3)*1000).to_i # => 1453402722578
ezpz's answer is almost perfect, but I hope I can add a little more.
Geo asked about time in milliseconds; this sounds like an integer quantity, and I wouldn't take the detour through floating-point land. Thus my approach would be:
t8 = Time.now
# => Sun Nov 01 15:18:04 +0100 2009
t9 = Time.now
# => Sun Nov 01 15:18:18 +0100 2009
dif = t9 - t8
# => 13.940166
(1000 * dif).to_i
# => 13940
Multiplying by an integer 1000 preserves the fractional number perfectly and may be a little faster too.
If you're dealing with dates and times, you may need to use the DateTime class. This works similarly but the conversion factor is 24 * 3600 * 1000 = 86400000 .
I've found DateTime's strptime and strftime functions invaluable in parsing and formatting date/time strings (e.g. to/from logs). What comes in handy to know is:
The formatting characters for these functions (%H, %M, %S, ...) are almost the same as for the C functions found on any Unix/Linux system; and
There are a few more: In particular, %L does milliseconds!
The answer is something like:
t_start = Time.now
# time-consuming operation
t_end = Time.now
milliseconds = (t_start - t_end) * 1000.0
However, the Time.now approach risks to be inaccurate. I found this post by Luca Guidi:
https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
system clock is constantly floating and it doesn't move only forwards. If your calculation of elapsed time is based on it, you're very likely to run into calculation errors or even outages.
So, it is recommended to use Process.clock_gettime instead. Something like:
def measure_time
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed_time = end_time - start_time
elapsed_time.round(3)
end
Example:
elapsed = measure_time do
# your time-consuming task here:
sleep 2.2321
end
=> 2.232
%L gives milliseconds in ruby
require 'time'
puts Time.now.strftime("%Y-%m-%dT%H:%M:%S.%L")
or
puts Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
will give you current timestamp in milliseconds.
Time.now.to_f can help you but it returns seconds.
In general, when working with benchmarks I:
put in variable the current time;
insert the block to test;
put in a variable the current time, subtracting the preceding current-time value;
It's a very simple process, so I'm not sure you were really asking this...
Try subtracting the first Time.now from the second. Like so:
a = Time.now
sleep(3)
puts Time.now - a # about 3.0
This gives you a floating-point number of the seconds between the two times (and with that, the milliseconds).
If you want something precise, unaffected by other part of your app (Timecop) or other programs (like NTP), use Process#clock_gettime with Process::CLOCK_MONOTONIC to directly get the processor time.
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# other code
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
Also, if you are trying to benchmark some code tho, there is the Benchmark module for that!
require "benchmark"
time = Benchmark.realtime do
# code to measure
end

Iterate over Ruby Time object with delta

Is there a way to iterate over a Time range in Ruby, and set the delta?
Here is an idea of what I would like to do:
for hour in (start_time..end_time, hour)
hour #=> Time object set to hour
end
You can iterate over the Time objects, but it returns every second between the two. What I really need is a way to set the offset or delta (such as minute, hour, etc.)
Is this built in to Ruby, or is there a decent plugin available?
Prior to 1.9, you could use Range#step:
(start_time..end_time).step(3600) do |hour|
# ...
end
However, this strategy is quite slow since it would call Time#succ 3600 times. Instead,
as pointed out by dolzenko in his answer, a more efficient solution is to use a simple loop:
hour = start_time
while hour < end_time
# ...
hour += 3600
end
If you're using Rails you can replace 3600 with 1.hour, which is significantly more readable.
If your start_time and end_time are actually instances of Time class then the solution with using the Range#step would be extremely inefficient since it would iterate over every second in this range with Time#succ. If you convert your times to integers the simple addition will be used but this way you will end up with something like:
(start_time.to_i..end_time.to_i).step(3600) do |hour|
hour = Time.at(hour)
# ...
end
But this also can be done with simpler and more efficient (i.e. without all the type conversions) loop:
hour = start_time
begin
# ...
end while (hour += 3600) < end_time
Range#step method is very slow in this case. Use begin..end while, as dolzenko posted here.
You can define a new method:
def time_iterate(start_time, end_time, step, &block)
begin
yield(start_time)
end while (start_time += step) <= end_time
end
then,
start_time = Time.parse("2010/1/1")
end_time = Time.parse("2010/1/31")
time_iterate(start_time, end_time, 1.hour) do |t|
puts t
end
if in rails.

Resources