active record query over datetime array - ruby-on-rails

Given I have an array of datetimes:
array = [Sat, 30 Jul 2011 00:00:00 CEST +02:00, Sat, 30 Jul 2011 00:15:00 CEST +02:00, Sat, 30 Jul 2011 00:30:00 CEST +02:00, Sat, 30 Jul 2011 00:45:00 CEST +02:00
I want my model class method to return the datetimes that dont match (aren't scheduled)
Sat, 30 Jul 2011 00:00:00 CEST +02:00
#appointment.rb (with colum `date` as DateTime)
def self.booked(array)
where("date NOT IN (?)", array)
end
Thx for advise!

It's because your array is malformed:
array = [Sat, 30 Jul 2011 00:00:00 CEST +02:00, Sat, 30 Jul 2011 00:15:00 CEST +02:00, Sat, 30 Jul 2011 00:30:00 CEST +02:00, Sat, 30 Jul 2011 00:45:00 CEST +02:00
notice it's separating the days as individual elements. Or is this your doing? If not, you should probably convert them all to actual DateTime objects (using #parse probably) and then put them in the array.
Also, you might want to make that method into a scope:
scope :booked, lambda { |datetimes| where("date NOT IN (?)", datetimes) }

I'm guessing that date is actually a date in the database, not a datetime or timestamp. So, you need to convert your Ruby Datetimes to just Date instances:
def self.booked(array)
where("date NOT IN (?)", array.map(&:to_date))
end
If you don't convert them to Dates by hand, AR won't know that it is supposed to convert them to dates for the database; then, depending on the underlying database, you could get empty results or possibly errors.

Your code look fine to me, but you need to provide an array, like this:
where(['date NOT IN(?)', array])
Here is an example with a Webinar class with a date column
>> array = Webinar.all[0..2].map(&:date)
=> [Wed, 04 May 2011 02:16:00 PDT -07:00, Tue, 05 Apr 2011 06:00:00 PDT -07:00, Thu, 30 Jun 2011 07:30:00 PDT -07:00]
>> count_without_array = (Webinar.count - array.size)
>> Webinar.where(['date NOT IN(?)', array]).count == count_without_array
=> true
Keep in mind that date must be exact match, if there is 1 second difference its not going to work.

Related

Why my DateTime format changed on Bamboo build

I have a User class that has_many Jobs. I map jobs with the following code, the start_at and end_at are datetime:
def ranges
user.jobs.map { |u| [u.start_at, u.end_at] }
end
I have a spec that compares two arrays:
my_array = [[start1, end1], [start2, end2]]
expect(ranges).to eq my_array
The test data are also datetime created from factory girl e.g.
create(:jobs, start_at:DateTime.parse('2017-03-26 00:00:00'), end_at: DateTime.parse('2017-03-27 00:00:00'))
Everything works fine, expect when Bamboo runs my spec, I get following error:
expect
[[2017-12-31 00:00:00.000000000 +0000, 2017-12-31 10:10:00.000000000 +0000], [2017-12-30 00:00:00.000000000 +0000, 2017-12-31 00:10:00.000000000 +0000], [2017-11-26 00:00:00.000000000 +0000, 2017-11-26 10:10:00.000000000 +0000], [2017-03-24 00:00:00.000000000 +0000, 2017-03-24 10:10:00.000000000 +0000], [2017-03-25 00:00:00.000000000 +0000, 2017-03-25 10:10:00.000000000 +0000], [2017-03-26 00:00:00.000000000 +0000, 2017-03-26 10:10:00.000000000 +0000]]
to match
[[Sun, 26 Mar 2017 00:00:00 UTC +00:00, Sun, 26 Mar 2017 10:10:00 UTC +00:00], [Sat, 25 Mar 2017 00:00:00 UTC +00:00, Sat, 25 Mar 2017 10:10:00 UTC +00:00], [Fri, 24 Mar 2017 00:00:00 UTC +00:00, Fri, 24 Mar 2017 10:10:00 UTC +00:00], [Sun, 26 Nov 2017 00:00:00 UTC +00:00, Sun, 26 Nov 2017 10:10:00 UTC +00:00], [Sat, 30 Dec 2017 00:00:00 UTC +00:00, Sun, 31 Dec 2017 00:10:00 UTC +00:00], [Sun, 31 Dec 2017 00:00:00 UTC +00:00, Sun, 31 Dec 2017 10:10:00 UTC +00:00]]
dose this mean I need to format all my datetime object byiso8601 all the time? what could cause this on Bamboo
The problem isn't your format. The problem is that the array elements are not in the same order. The dates in your expect array begin with the 2017-12-31 dates, while those in your match array begin with the 2017-03-26 dates.
RSpec's eq method returns true only if each element of the first array is identical to the element at the same index of the second array. But the match_array method returns true so long as the two arrays have the same elements, regardless of order.
Change your expectation line to:
expect(ranges).to match_array(my_array)
And you should be good to go.

Comparing Ruby/Rails TimeWithZone Objects

Apologies if this has already been answered, but I've looked over a few similar questions and haven't quite seen this issue addressed. I'm trying to compare two TimeWithZone objects one from a created_at from my model and the other is a time.ago object
The created_at datetime object:
entry.created_at
=> Mon, 27 Jul 2015 08:47:32 UTC +00:00
The time.ago object:
1.minute.ago
=> Mon, 27 Jul 2015 10:23:29 UTC +00:00
Comparing them returns incorrectly:
entry.created_at > 1.minute.ago
=> false
or
entry.created_at < 1.minute.ago
=> true
It seems like it's actually comparing these as strings, if that's true how could I convert them to a comparable format?
Mon, 27 Jul 2015 08:47:32 UTC +00:00 is earlier (therefore less) then Mon, 27 Jul 2015 10:23:29 UTC +00:00.
It compares it right.

How do get a random DateTime rounded to beginning of hour in Rails?

Basically I'd like to get a random datetime within the last year:
rand(1.year).ago #=> Sun, 22 Sep 2013 18:37:44 UTC +00:00 (example)
But how do I go about specifying or limiting this to times on the hour? For example:
Sun, 22 Sep 2013 18:00:00 UTC +00:00
Sat, 02 Nov 2013 10:00:00 UTC +00:00
Fri, 12 Apr 2013 21:00:00 UTC +00:00
I finally found what I was looking for. #Stoic's answer is very good but I found this available method (http://api.rubyonrails.org/classes/DateTime.html):
rand(1.year).ago.beginning_of_hour
Does exactly the same thing but looks neater and prevents you from having to write your own function.
Rounding datetime to the nearest hour in Rails would be
(DateTime.now + 30.minutes).beginning_of_hour
Not the answer to the actual question, but it does answer the title of the question (which is how i got here).
Try this:
def random_time_to_nearest_hour
time = rand(1.year).ago
time - time.sec - 60 * time.min
end
Examples:
[1] pry(main)> random_time_to_nearest_hour
=> Sun, 28 Apr 2013 16:00:00 UTC +00:00
[2] pry(main)> random_time_to_nearest_hour
=> Sat, 08 Jun 2013 15:00:00 UTC +00:00
[3] pry(main)> random_time_to_nearest_hour
=> Thu, 22 Aug 2013 23:00:00 UTC +00:00
[4] pry(main)> random_time_to_nearest_hour
=> Tue, 29 Jan 2013 14:00:00 UTC +00:00
[5] pry(main)> random_time_to_nearest_hour
=> Tue, 13 Aug 2013 06:00:00 UTC +00:00
[6] pry(main)> random_time_to_nearest_hour
=> Mon, 03 Jun 2013 08:00:00 UTC +00:00
[7] pry(main)>
Note that, this method will always floor down to the nearest hour, but since you are anyways generating a random time, it wont matter if this time is getting floor'ed down or getting round'ed. :)

In Rails, what's the nicest way to create a specific time (not now) in a particular time zone?

Here's one way, but can you think of a more idiomatic way?
>> Time.use_zone('Sydney'){ Time.zone.parse('2011-04-12 2pm') }
=> Tue, 12 Apr 2011 14:00:00 EST +10:00
I think you're looking for
Time.find_zone('Alaska').local(2011,1,1)
=> Sat, 01 Jan 2011 00:00:00 AKST -09:00
Time.find_zone('Amsterdam').local(2011,1,1)
=> Sat, 01 Jan 2011 00:00:00 CET +01:00
Time.find_zone('Sydney').local(2011,1,1)
=> Sat, 01 Jan 2011 00:00:00 EST +11:00
Time.find_zone('Wellington').local(2011,1,1)
=> Sat, 01 Jan 2011 00:00:00 NZDT +13:00
This also works with parse
Time.find_zone('Sydney').parse('2011-04-12 2pm')
=> Tue, 12 Apr 2011 14:00:00 EST +10:00
For parsing a date within a specific time zone, you can use ActiveSupport::TimeZone
> ActiveSupport::TimeZone["Sydney"].parse("2011-04-12 2pm")
=> Tue, 12 Apr 2011 14:00:00 EST 10:00
TimeZone API documentation is here:
http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-c-5B-5D
This is what I use:
Time.zone.local(2011, 4, 12, 14, 0)
As said above, to create a time in a specific timezone (e.g., 4/10/2014 1:30pm New York):
#event.start_time = Time.find_zone('Eastern Time (US & Canada)').local(2014,4,10,13,30)
=> Thu, 10 Apr 2014 13:30:00 EDT -04:00
#event.start_time.utc
=> 2014-04-10 17:30:00 UTC
When it is saved to your db, it will be converted to UTC (in Postgres at least if using a timestamp type in your migration), and on future access it will be displayed relative to the application timezone set in config/application.rb
To properly display the local time, we also store the timezone name (e.g., 'Eastern Time (US & Canada)' ) in our database. So, when we want to print the time in our views, we do...
#event.start_time.in_time_zone(#event.timezone)
=> Thu, 10 Apr 2014 13:30:00 EDT -04:00
To get the abbreviated timezone (e.g., EST)
#event.start_time.in_time_zone(#event.timezone).zone
=> "EDT"
How about using the *in_time_zone* helper..
Time.now.in_time_zone('Sydney')

How to combine overlapping time ranges (time ranges union)

I have an array with several time ranges inside:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00,
Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 09:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
I want to get the same array with the overlapping time ranges combined, so the output for this case will be:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
So it creates a new time range when to time ranges overlap, and so on. If they don´t overlap the will be keep separated. Another example:
Input:
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:00:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
Output (will be the same because they don´t overlap):
[Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:00:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
I was thinking in some recursive approach, but I need some guidance here...
Given a function that returns truthy if two ranges overlap:
def ranges_overlap?(a, b)
a.include?(b.begin) || b.include?(a.begin)
end
(this function courtesy of sepp2k and steenslag)
and a function that merges two overlapping ranges:
def merge_ranges(a, b)
[a.begin, b.begin].min..[a.end, b.end].max
end
then this function, given an array of ranges, returns a new array with any overlapping ranges merged:
def merge_overlapping_ranges(overlapping_ranges)
overlapping_ranges.sort_by(&:begin).inject([]) do |ranges, range|
if !ranges.empty? && ranges_overlap?(ranges.last, range)
ranges[0...-1] + [merge_ranges(ranges.last, range)]
else
ranges + [range]
end
end
end
Searching a little bit I have found a code that does the trick:
def self.merge_ranges(ranges)
ranges = ranges.sort_by {|r| r.first }
*outages = ranges.shift
ranges.each do |r|
lastr = outages[-1]
if lastr.last >= r.first - 1
outages[-1] = lastr.first..[r.last, lastr.last].max
else
outages.push(r)
end
end
outages
end
A sample (working with time ranges too!):
ranges = [1..5, 20..20, 4..11, 40..45, 39..50]
merge_ranges(ranges)
=> [1..11, 20..20, 39..50]
Found here: http://www.ruby-forum.com/topic/162010
You can do it by using multi_range gem.
Example 1:
ranges = [
Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 13:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 16:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 09:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 15:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
]
MultiRange.new(ranges).merge_overlaps.ranges
# => [2011-05-24 08:00:00 +0800..2011-05-24 13:00:00 +0800, 2011-05-24 15:30:00 +0800..2011-05-24 18:00:00 +0800]
Example 2:
ranges = [
Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00')..Time.parse('Tue, 24 May 2011 13:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 16:00:00 CEST +02:00')..Time.parse('Tue, 24 May 2011 18:00:00 CEST +02:00'),
]
MultiRange.new(ranges).merge_overlaps.ranges
# => [2011-05-24 08:00:00 +0800..2011-05-24 13:00:00 +0800, 2011-05-24 16:00:00 +0800..2011-05-24 18:00:00 +0800]
The facets gem has Range.combine method that may be of use: http://rdoc.info/github/rubyworks/facets/master/Range#combine-instance_method
Some kind of algorithm that might help:
Sort range array by start time (r1, r2, r3, r4, .. rn)
for each range pair [r1, r2], [r2, r3] .. [rn-1, rn]:
if r1_end > r2_start: # they overlap
add [r1_start, r2_end] to new range array
else: # they do not overlap
add [r1] and [r2] to new range array (no changes)
startover with the new range array until no more changes
The solution, offered by #wayne-conrad is a very good one. I implemented it for a problem, I stumbled upon. Then I implemented an iterative version and benchmarked the two. It appears, the iterative version is quicker. Note: I use ActiveSupport for Range#overlaps? and the time helpers, but it is trivial to implement a pure-Ruby version.
require 'active_support/all'
module RangesUnifier
extend self
# ranges is an array of ranges, e.g. [1..5, 2..6]
def iterative_call(ranges)
ranges.sort_by(&:begin).reduce([ranges.first]) do |merged_ranges, range|
if merged_ranges.last.overlaps?(range)
merged_ranges[0...-1] << merge_ranges(merged_ranges.last, range)
else
merged_ranges << range
end
end
end
def recursive_call(ranges)
return ranges if ranges.size == 1
if ranges[0].overlaps?(ranges[1])
recursive_call [merge_ranges(ranges[0], ranges[1]), *ranges[2..-1]]
else
[ranges[0], *recursive_call(ranges[1..-1])]
end
end
def merge_ranges(a, b)
[a.begin, b.begin].min..[a.end, b.end].max
end
end
five_hours_ago = 5.hours.ago
four_hours_ago = 4.hours.ago
three_hours_ago = 3.hours.ago
two_hours_ago = 2.hours.ago
one_hour_ago = 1.hour.ago
one_hour_from_now = 1.hour.from_now
two_hours_from_now = 2.hours.from_now
three_hours_from_now = 3.hours.from_now
four_hours_from_now = 4.hours.from_now
five_hours_from_now = 5.hours.from_now
input = [
five_hours_ago..four_hours_ago,
three_hours_ago..two_hours_from_now,
one_hour_ago..one_hour_from_now,
one_hour_from_now..three_hours_from_now,
four_hours_from_now..five_hours_from_now
]
RangesUnifier.iterative_call(input)
#=> [
# 2017-08-21 12:50:50 +0300..2017-08-21 13:50:50 +0300,
# 2017-08-21 14:50:50 +0300..2017-08-21 20:50:50 +0300,
# 2017-08-21 21:50:50 +0300..2017-08-21 22:50:50 +0300
# ]
RangesUnifier.recursive_call(input)
#=> [
# 2017-08-21 12:50:50 +0300..2017-08-21 13:50:50 +0300,
# 2017-08-21 14:50:50 +0300..2017-08-21 20:50:50 +0300,
# 2017-08-21 21:50:50 +0300..2017-08-21 22:50:50 +0300
# ]
n = 100_000
Benchmark.bm do |x|
x.report('iterative') { n.times { RangesUnifier.iterative_call(input) } }
x.report('recursive') { n.times { RangesUnifier.recursive_call(input) } }
end
# =>
# user system total real
# iterative 0.970000 0.000000 0.970000 ( 0.979549)
# recursive 0.540000 0.010000 0.550000 ( 0.546755)
The gem range_operators does a wonderful job by adding missing features to the Ruby Range class. It is way smaller than adding the whole facets gem.
I your case the solution would be the rangify method, which is added to the Array class and would do exactly what you are looking for.
I made a minor update to the answer from Wayne Conrad to handle edge cases involved with open-ended arrays (created with ... operator instead of .. operator).
I changed the name to merge_continuous_ranges since while ranges like 0...1 and 1..2 do not overlap, their combined ranges are continuous, so it makes sense to combine them:
def merge_continuous_ranges(ranges)
ranges.sort_by(&:begin).inject([]) do |result, range|
if !result.empty? && ranges_continuous?(result.last, range)
result[0...-1] + [merge_ranges(result.last, range)]
else
result + [range]
end
end
end
def ranges_continuous?(a, b)
a.include?(b.begin) || b.include?(a.begin) || a.end == b.begin || b.end == a.begin
end
def merge_ranges(a, b)
range_begin = [a.begin, b.begin].min
range_end = [a.end, b.end].max
exclude_end = case a.end <=> b.end
when -1
b.exclude_end?
when 0
a.exclude_end? && b.exclude_end?
when 1
a.exclude_end?
end
exclude_end ? range_begin...range_end : range_begin..range_end
end
A solution in one method and bug-free for what I can tell:
def merge_ranges(ranges)
ranges = ranges.sort_by(&:first)
merged = [ranges[0]]
ranges.each do |current|
previous = merged[-1]
if current.first <= previous.last
merged[-1] = previous.first..[previous.last, current.last].max
else
merged.push(current)
end
end
merged
end
Usage:
ranges = [
Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 13:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 16:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 08:00:00 CEST +02:00..Tue')..Time.parse('24 May 2011 09:00:00 CEST +02:00'),
Time.parse('Tue, 24 May 2011 15:30:00 CEST +02:00..Tue')..Time.parse('24 May 2011 18:00:00 CEST +02:00'),
]
merge_ranges(ranges)
#=> [2011-05-24 08:00:00 +0200..2011-05-24 13:00:00 +0200, 2011-05-24 15:30:00 +0200..2011-05-24 18:00:00 +0200]
Disclaimer: it's a port of https://stackoverflow.com/a/43600953/807442
Dont you just want to find the smallest first value and the largest last value from the set of arrays?
ranges = [Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 13:00:00 CEST +02:00,
Tue, 24 May 2011 16:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00,
Tue, 24 May 2011 08:00:00 CEST +02:00..Tue, 24 May 2011 09:00:00 CEST +02:00,
Tue, 24 May 2011 15:30:00 CEST +02:00..Tue, 24 May 2011 18:00:00 CEST +02:00]
union = [ranges.collect(&:first).sort.first, ranges.collect(&:last).sort.last]
The Marked answer works well except for few use cases. One of such use case is
[Tue, 21 June 13:30:00 GMT +0:00..Tue, 21 June 15:30:00 GMT +00:00,
Tue, 21 June 14:30:00 GMT +0:00..Tue, 21 June 15:30:00 GMT +00:00]
The condition in ranges_overlap does not handle this use case. So I wrote this
def ranges_overlap?(a, b)
a.include?(b.begin) || b.include?(a.begin) || a.include?(b.end) || b.include?(a.end)|| (a.begin < b.begin && a.end >= b.end) || (a.begin >= b.begin && a.end < b.end)
end
This is handling all the edge cases for me so far.

Resources