I'm working on a form where the user enters a date range and selects from a list of checkboxes a day/days of the week i.e Sunday, Monday, Tuesday, Wednesday, Thursday, Friday and saturday.
Once the form is submitted I need a way to grab a list of dates between the two dates entered based upon the days that were chosen i.e All Mondays and Thursdays between the two dates given. I've looked through the docs but can't pin point how to do this efficiently i.e the ruby way.
fun one! :D
start_date = Date.today # your start
end_date = Date.today + 1.year # your end
my_days = [1,2,3] # day of the week in 0-6. Sunday is day-of-week 0; Saturday is day-of-week 6.
result = (start_date..end_date).to_a.select {|k| my_days.include?(k.wday)}
using the data above you'll get an array of all Mon/Tue/Weds between now and next year.
Another approach is to group your date range by wday
and pick off your day of the week:
datesByWeekday = (start_date..end_date).group_by(&:wday)
datesByWeekday[0] # All Sundays
for example, to get all Saturdays in March 2019:
> (Date.new(2019,03,01)..Date.new(2019,04,01)).group_by(&:wday)[0]
=> [Sun, 03 Mar 2019, Sun, 10 Mar 2019, Sun, 17 Mar 2019, Sun, 24 Mar 2019, Sun, 31 Mar 2019]
https://apidock.com/ruby/Date/wday
Was curious about speed, so here's what I did.
Here are two approaches to solve the problem:
1.week
to that day until stopFor the ranged solutions, there are different ways to use the range:
How you select dates is also important:
include?
on the days requestedI made a file to test all these methods. I called it test.rb
. I placed it at the root of a rails application. I ran it by typing these commands:
rails c
load 'test.rb'
Here's the testing file:
@days = {
'Sunday' => 0, 'Monday' => 1, 'Tuesday' => 2, 'Wednesday' => 3,
'Thursday' => 4, 'Friday' => 5, 'Saturday' => 6,
}
@start = Date.today
@stop = Date.today + 1.year
# use simple arithmetic to count number of weeks and then get all days by adding a week
def division(args)
my_days = args.map { |key| @days[key] }
total_days = (@stop - @start).to_i
start_day = @start.wday
my_days.map do |wday|
total_weeks = total_days / 7
remaining_days = total_days % 7
total_weeks += 1 if is_there_wday? wday, remaining_days, @stop
days_to_add = wday - start_day
days_to_add = days_to_add + 7 if days_to_add.negative?
next_day = @start + days_to_add
days = []
days << next_day
(total_weeks - 1).times do
next_day = next_day + 1.week
days << next_day
end
days
end.flatten.sort
end
def is_there_wday?(wday, remaining_days, stop)
new_start = stop - remaining_days
(new_start..stop).map(&:wday).include? wday
end
# take all the dates and group them by weekday
def group_by(args)
my_days = args.map { |key| @days[key] }
grouped = (@start..@stop).group_by(&:wday)
my_days.map { |wday| grouped[wday] }.flatten.sort
end
# run the select function on the range
def select_include(args)
my_days = args.map { |key| @days[key] }
(@start..@stop).select { |x| my_days.include? x.wday }
end
# run the select function on the range
def select_intersect(args)
my_days = args.map { |key| @days[key] }
(@start..@stop).select { |x| (my_days & [x.wday]).any? }
end
# take all the dates, convert to array, and then select
def to_a_include(args)
my_days = args.map { |key| @days[key] }
(@start..@stop).to_a.select { |k| my_days.include? k.wday }
end
# take all dates, convert to array, and check if interection is empty
def to_a_intersect(args)
my_days = args.map { |key| @days[key] }
(@start..@stop).to_a.select { |k| (my_days & [k.wday]).any? }
end
many = 10_000
Benchmark.bmbm do |b|
[[], ['Sunday'], ['Sunday', 'Saturday'], ['Sunday', 'Wednesday', 'Saturday']].each do |days|
str = days.map { |x| @days[x] }
b.report("#{str} division") { many.times { division days }}
b.report("#{str} group_by") { many.times { group_by days }}
b.report("#{str} select_include") { many.times { select_include days }}
b.report("#{str} select_&") { many.times { select_intersect days }}
b.report("#{str} to_a_include") { many.times { to_a_include days }}
b.report("#{str} to_a_&") { many.times { to_a_intersect days }}
end
end
Sorted results
[] division 0.017671
[] select_include 2.459335
[] group_by 2.743273
[] to_a_include 2.880896
[] to_a_& 4.723146
[] select_& 5.235843
[0] to_a_include 2.539350
[0] select_include 2.543794
[0] group_by 2.953319
[0] division 4.494644
[0] to_a_& 4.670691
[0] select_& 4.897872
[0, 6] to_a_include 2.549803
[0, 6] select_include 2.553911
[0, 6] group_by 4.085657
[0, 6] to_a_& 4.776068
[0, 6] select_& 5.016739
[0, 6] division 10.203996
[0, 3, 6] select_include 2.615217
[0, 3, 6] to_a_include 2.618676
[0, 3, 6] group_by 4.605810
[0, 3, 6] to_a_& 5.032614
[0, 3, 6] select_& 5.169711
[0, 3, 6] division 14.679557
Trends
range.select
is slightly faster than range.to_a.select
include?
is faster than intersect.any?
group_by
is faster than intersect.any?
but slower than include?
division
is fast when nothing is given, but slows down significantly the more params that are passedConclusion
If you combine select
and include?
, you have the fastest and most reliable solution for this problem
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With