RoR, Can't iterate from DateTime/TimeWithZone - ruby-on-rails

I have a simple task where I want to take a starting date and an ending date and loop over the days/dates. This code is being used in my db:seed rake task. Currently, my code has gone through the following attempts.
(someModel.start_date.to_datetime..someModel.end_date.to_datetime).each {
|x| puts x
}
......
(someModel.start_date...someModel.end_date).each { |x| puts x }
In each case, I get an error like this.
can't iterate from ActiveSupport::TimeWithZone
or
can't iterate from DateTime
If anyone has any clue on how to iterate over a range of DateTimes I'd be greatly appreciative.

start = someModel.start_date.to_datetime
finish = someModel.end_date.to_datetime
while(start < finish) do
#bunch of awesome stuff
start += 1.day
end

You must make sure that you are dealing with a Date object (by calling to_date), then everything works as expected:
start_date.to_date.upto(end_date.to_date) {|date| puts date }
Or with a range:
(start_date.to_date..end_date.to_date).to_a

You can't iterate from DateTime. But you can iterate when start and end of
interval are instances of Date. Convert them if possible.
And then look at these Date methods:
step()
upto()
downto()
to use instead of each

If you want to iterate on the range you might consider a loop and step each day using something like someModel.start_date + 1.day.to_i

Here's what we're doing to iterate by minutes over two TimeWithZones:
# allow iteration over a set of TimeWithZones
def self.time_map(range, step: 30.minutes, &block)
result = []
time = range.first
max = range.last
test = range.exclude_end? ? ->(t) { t < max } : ->(t) { t <= max }
value = block_given? ? ->(t) { yield(t) } : ->(t) { t }
while test.call(time) do
result << value.call(time)
time += step
end
result
end

Related

Finding the months between two dates in rails

I can currently set a time range like so:
start_date: "2018-09-11"
end_date: "2018-11-19"
How can I do this for start to end of months? Examples:
time_range = ["2018-09-11".."2018-09-30"]
time_range = ["2018-10-01".."2018-10-31"]
time_range = ["2018-11-01".."2018-11-19"]
I'm not sure what's exactly your desired outcome but, given start date and end date as Date objects, you can perform
(start_date..end_date).to_a.group_by(&:month).values
and at the end what you get is a three element array, and each element contains an array with all the dates in that range for a month
I do not know if I understand very well what you asked, but I'll try to help you.
The Date class has several methods that will help you to work with dates.
Date < Object
Examples
my_date_range_array = [Date.today.beginning_of_year..Date.today.end_of_year]
my_date_time_range_array = [Time.now.beginning_of_year..Time.now.end_of_year]
my_date_range_array = [6.months.ago..Date.today]
YourModel.where date: Date.today.beginning_of_month..Date.today
YourModel.where date: 6.months.ago..Date.today
If you need every single date in the range, you can use something like this:
(Date.today.beginning_of_year..Date.today.end_of_year).map{ |date| date }
I hope that my answer helps you
This is a pure Ruby solution, but I believe (though I don't know Rails) it can be simplified slightly by replacing my methods first_day_of_month and first_day_of_month with Rails methods beginning_of_month and end_of_month, respectively. I designed the method for efficiency over simplicity.
require 'date'
DATE_FMT = "%Y-%m-%d"
def date_ranges(start_date_str, end_date_str)
start_date = Date.strptime(start_date_str, DATE_FMT)
end_date = Date.strptime(end_date_str, DATE_FMT)
return [start_date_str..end_date_str] if
[start_date.year, start_date.month] == [end_date.year, end_date.month]
d = start_date
ranges = [start_date_str..last_day_of_month(d)]
loop do
d = d >> 1
break if [d.year, d.month] == [end_date.year, end_date.month]
ranges << (first_day_of_month(d)..last_day_of_month(d))
end
ranges << (first_day_of_month(d)..end_date_str)
end
def first_day_of_month(d)
(d - d.day + 1).strftime(DATE_FMT)
end
def last_day_of_month(d)
((d >> 1)-d.day).strftime(DATE_FMT)
end
date_ranges("2018-09-11", "2019-02-11")
#=> ["2018-09-11".."2018-09-30", "2018-10-01".."2018-10-31",
# "2018-11-01".."2018-11-30", "2018-12-01".."2018-12-31",
# "2019-01-01".."2019-01-31", "2019-02-01".."2019-02-11"]
date_ranges("2018-09-08", "2018-09-23")
#=> ["2018-09-08".."2018-09-23"]
With the information provided by the OP, this is what I understand he is looking for.
Given a set range for example:
time_range = "2018-09-11".."2018-09-19"
new_range_min = time_range.min.to_date.beginning_of_month
new_range_max = time_range.max.to_date.end_of_month
new_range = new_range_min..new_range_max

How to check if time is in range using Rails

There is the following times:
now = "2014-01-24T15:58:07.169+04:00",
start = "2000-01-01T10:00:00Z",
end = "2000-01-01T16:00:00Z"
I need to check if now is between start and end. I use the following code:
Range.new(start, end).cover?(now)
Unfortunately, this code returns false for my data. What am I doing wrong? How can I fix it? Thanks.
Well, I would use between? method. Because it's faster than cover? and include? variants. Here's an example:
yesterday = Date.yesterday
today = Date.today
tomorrow = Date.tomorrow
today.between?(yesterday, tomorrow) #=> true
Here's a gist with performance tests Include?, Cover? or Between?
Update
According to your recent comment, you want to compare 'only time' without date. If I get you correctly, there's a way to do it - strftime. But before that, to make comparison correctly, you need to convert all your datetimes to a single timezone (for example, using utc). Here's an example:
start_time_with_date = Time.parse('2000-01-01T16:00:00Z').utc
end_time_with_date = Time.parse('2014-01-24T15:58:07.169+04:00').utc
start_time = start_time_with_date.strftime('%I:%M:%S') #=> '04:00:00'
end_time = end_time_with_date.strftime('%I:%M:%S') #=> '11:58:07'
current_time = Time.now.utc.strftime('%I:%M:%S') #=> '01:45:27' (my current time)
current_time.between?(start_time, end_time) #=> false
And yes. Sadly, it's a string comparison.
You can use Range#cover? with time objects.
start = Time.parse('2000-01-01T10:00:00Z')
end_time = Time.parse('2000-01-01T16:00:00Z')
now = Time.parse('2014-01-24T15:58:07.169+04:00')
(start..end_time).cover?(now)
You're currently using strings, Ruby cannot know you're speaking about time.
I see the only variant, to define additional method to Range:
class Range
def time_cover? now
(b,e,n) = [ self.begin.utc.strftime( "%H%M%S%N" ),
self.end.utc.strftime( "%H%M%S%N" ),
now.utc.strftime( "%H%M%S%N" ) ]
if b < e
b <= n && e >= n
else
e <= n && b >= n
end
end
end
now = Time.parse "2014-01-24T15:58:07.169+04:00"
s = Time.parse "2000-01-01T10:00:00Z"
e = Time.parse "2000-01-01T16:00:00Z"
Range.new(s, e).time_cover?(now)
# => true
your date time(now) is not in between start and end time

In ruby which is better, detect or index, to find an object in an array?

I have an array of objects.
I want to find an object in the array based on some property of the object.
I can do
array.detect {|x| x.name=="some name"}
or I could do
ind=array.index {|x| x.name=="some name"}
array[ind] unless ind.nil?
Is there any reason to choose one over the other?
If you aren't interested in finding the index value of the object you're searching for, I would suggest detect. It'll save you from having to do that nil check before accessing the array.
From a performance standpoint, I imagine it's relatively comparable, but that could help your decision too. That would require benchmarking as Niels B. mentioned in his comment.
If you want to find an element in a collection, it's important to use collections made for fast retrieval. Arrays are not made for that, nor are they particularly convenient unless you are making a stack or a queue.
Here's some code to show ways to improve the storage/retrieval speed over what you can get using find, detect or other normal array-based methods:
require 'fruity'
require 'digest'
class Foo
attr_reader :var1, :var2
def initialize(var1, var2)
#var1, #var2 = var1, var2
end
end
START_INT = 1
START_CHAR = 'a'
END_INT = 10
END_CHAR = 'z'
START_MD5 = Digest::MD5.hexdigest(START_INT.to_s + START_CHAR)
END_MD5 = Digest::MD5.hexdigest(END_INT.to_s + END_CHAR)
ary = []
hsh = {}
hsh2 = {}
START_INT.upto(END_INT) do |i|
(START_CHAR .. END_CHAR).each do |j|
foo = Foo.new(i, j)
ary << foo
hsh[[i, j]] = foo
hsh2[Digest::MD5.hexdigest(i.to_s + j)] = foo
end
end
compare do
array_find {
ary.find { |a| (a.var1 == START_INT) && (a.var2 == START_CHAR) }
ary.find { |a| (a.var1 == END_INT) && (a.var2 == END_CHAR) }
}
hash_access_with_array {
hsh[[START_INT, START_CHAR]]
hsh[[END_INT, END_CHAR]]
}
hash_access_with_digest {
hsh2[START_MD5]
hsh2[END_MD5]
}
end
Which results in:
Running each test 16384 times. Test will take about 17 seconds.
hash_access_with_digest is faster than hash_access_with_array by 10x ± 1.0
hash_access_with_array is faster than array_find by 16x ± 1.0
There are three different tests, and I'm looking for the first, and last elements in the array ary, and the corresponding objects in the hashes. The result of looking for the first and last elements in the array will be an average time for that search. For comparison I'm searching for the same objects in the hashes.
If we had some advance knowledge of which array index the object is in, retrieving the object from the array would be faster, but that's the problem, and making another container to keep track of that information would be slower than using the hash.
See for yourself!
require 'benchmark'
array = (1..1000000).to_a
Benchmark.bmbm do |x|
x.report("#index for 1") {
array.index(1)
}
x.report("#detect 1") {
array.detect { |i| i == 1 }
}
x.report("#index for 500k") {
array.index(500000)
}
x.report("#detect 500k") {
array.detect { |i| i == 500000 }
}
x.report("#index for 1m") {
array.index(1000000)
}
x.report("#detect 1m") {
array.detect { |i| i == 1000000 }
}
end
Put the code above in a file and execute it from the console with ruby <file>
Ignore the top block, that is rehearsal, the bottom block should look something like this:
user system total real
#index for 1 0.000005 0.000002 0.000007 ( 0.000004)
#detect 1 0.000007 0.000002 0.000009 ( 0.000006)
#index for 500k 0.003274 0.000049 0.003323 ( 0.003388)
#detect 500k 0.029870 0.000200 0.030070 ( 0.030872)
#index for 1m 0.005866 0.000009 0.005875 ( 0.005880)
#detect 1m 0.059819 0.000520 0.060339 ( 0.061340)
Running on my mac and Ruby 2.5.0, the numbers seem to suggest that #detect is an order of magnitude slower than #index.

Ruby: Average array of times

I have the following method in my Array class:
class Array
def avg
if partial_include?(":")
avg_times
else
blank? and 0.0 or (sum.to_f/size).round(2)
end
end
def avg_times
avg_minutes = self.map do |x|
hour, minute = x.split(':')
total_minutes = hour.to_i * 60 + minute.to_i
end.inject(:+)/size
"#{avg_minutes/60}:#{avg_minutes%60}"
end
def partial_include?(search_term)
self.each do |e|
return true if e[search_term]
end
return false
end
end
This works great with arrays of regular numbers, but there could instances where I have an array of times.
For example: [18:35, 19:07, 23:09]
Anyway to figure out the average of an array of time objects?
So you need do define a function that can calculate the average of times formatted as strings. Convert the data to minutes, avg the total minutes and then back to a time.
I would do it something like this:
a = ['18:35', '19:07', '23:09']
def avg_of_times(array_of_time)
size = array_of_time.size
avg_minutes = array_of_time.map do |x|
hour, minute = x.split(':')
total_minutes = hour.to_i * 60 + minute.to_i
end.inject(:+)/size
"#{avg_minutes/60}:#{avg_minutes%60}"
end
p avg_of_times(a) # = > "20:17"
Then when you call you function you check if any/all items in your array is formatted as a time. Maybe using regexp.
Average the Hours and Minutes Separately
Here's a simple method that we're using:
def calculate_average_of_times( times )
hours = times.collect{ |time| time.split( ":" ).first.to_i } # Large Arrays should only
minutes = times.collect{ |time| time.split( ":" ).second.to_i } # call .split 1 time.
average_hours = hours.sum / hours.size
average_minutes = ( minutes.sum / minutes.size ).to_s.rjust( 2, '0' ) # Pad with leading zero if necessary.
"#{ average_hours }:#{ average_minutes }"
end
And to show it working with your provided Array of 24-hour times, converted to Strings:
calculate_average_of_times( ["18:35", "19:07", "23:09"] )
#=> "20:17"
Thanks to #matt-privman for the help and inspiration on this.

Using scope to return results within multiple DateTime ranges in ActiveRecord

I've got a Session model that has a :created_at date and a :start_time date, both stored in the database as :time. I'm currently spitting out a bunch of results on an enormous table and allowing users to filter results by a single date and an optional range of time using scopes, like so:
class Session < ActiveRecord::Base
...
scope :filter_by_date, lambda { |date|
date = date.split(",")[0]
where(:created_at =>
DateTime.strptime(date, '%m/%d/%Y')..DateTime.strptime(date, '%m/%d/%Y').end_of_day
)
}
scope :filter_by_time, lambda { |date, time|
to = time[:to]
from = time[:from]
where(:start_time =>
DateTime.strptime("#{date} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{date} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
}
end
The controller looks more or less like this:
class SessionController < ApplicationController
def index
if params.include?(:date) ||
params.include?(:time) &&
( params[:time][:from][:digits].present? && params[:time][:to][:digits].present? )
i = Session.scoped
i = i.filter_by_date(params[:date]) unless params[:date].blank?
i = i.filter_by_time(params[:date], params[:time]) unless params[:time].blank? || params[:time][:from][:digits].blank? || params[:time][:to][:digits].blank?
#items = i
#items.sort_by! &params[:sort].to_sym if params[:sort].present?
else
#items = Session.find(:all, :order => :created_at)
end
end
end
I need to allow users to filter results using multiple dates. I'm receiving the params as a comma-separated list in string format, e.g. "07/12/2012,07/13/2012,07/17/2012", and need to be able to query the database for several different date ranges, and time ranges within those date ranges, and merge those results, so for example all of the sessions on 7/12, 7/13 and 7/17 between 6:30 pm and 7:30 pm.
I have been looking everywhere and have tried several different things but I can't figure out how to actually do this. Is this possible using scopes? If not what's the best way to do this?
My closest guess looks like this but it's not returning anything so I know it's wrong.
scope :filter_by_date, lambda { |date|
date = date.split(",")
date.each do |i|
where(:created_at =>
DateTime.strptime(i, '%m/%d/%Y')..DateTime.strptime(i, '%m/%d/%Y').end_of_day
)
end
}
scope :filter_by_time, lambda { |date, time|
date = date.split(",")
to = time[:to]
from = time[:from]
date.each do |i|
where(:start_time =>
DateTime.strptime("#{i} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{i} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
end
}
Another complication is that the start times are all stored as DateTime objects so they already include a fixed date, so if I want to return all sessions started between 6:30 pm and 7:30 pm on any date I need to figure something else out too. A third party is responsible for the data so I can't change how it's structured or stored, I just need to figure out how to do all these complex queries. Please help!
EDIT:
Here's the solution I've come up with by combining the advice of Kenichi and Chuck Vose below:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(created_at >= ? AND created_at <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
scope :filter_by_time, lambda { |times|
args = []
[times[:from], times[:to]].each do |time|
h, m, s = time[:digits].split(':')
h = (h.to_i + 12).to_s if time[:meridian] == 'pm'
h = '0' + h if h.length == 1
s = '00' if s.nil?
args.push "#{h}:#{m}:#{s}"
end
where("CAST(start_time AS TIME) >= ? AND
CAST(start_time AS TIME) <= ?", *args)
}
This solution allows me to return sessions from multiple non-consecutive dates OR return any sessions within a range of time without relying on dates at all, OR combine the two scopes to filter by non-consecutive dates and times within those dates. Yay!
An important point I overlooked is that the where statement must come last -- keeping it inside of an each loop returns nothing. Thanks to both of you for all your help! I feel smarter now.
something like:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
and
scope :filter_by_time, lambda { |dates, time|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
f = time[:from] # convert to '%H:%M:%S'
t = time[:to] # again, same
b = "#{y}-#{m}-#{d} #{f}"
e = "#{y}-#{m}-#{d} #{t}"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
So, the easy part of the question is what to do about datetimes. The nice thing about DateTimes is that they can be cast to times really easily with this:
CAST(datetime_col AS TIME)
So you can do things like:
i.where("CAST(start_time AS TIME) IN(?)", times.join(", "))
Now, the harder part, why aren't you getting any results. The first thing to try is to use i.to_sql to decide whether the scoped query looks reasonable. My guess is that when you print it out you'll find that all those where are chaining together with AND. So you're asking for objects with a date that is on 7/12, 7/13, and 7/21.
The last part here is that you've got a couple things that are concerning: sql injections and some overeager strptimes.
When you do a where you should never use #{} in the query. Even if you know where that input is coming from your coworkers may not. So make sure you're using ? like in the where I did above.
Secondly, strptime is extremely expensive in every language. You shouldn't know this, but it is. If at all possible avoid parsing dates, in this case you can probably just gsub / into - in that date and everything will be happy. MySQL expects dates in m/d/y form anyways. If you're still having trouble with it though and you really need a DateTime object you can just as easily do: Date.new(2001,2,3) without eating your cpu.

Resources