Was curious about speed, so here's what I did.
Here are two approaches to solve the problem:
- Use a range and filter
- Find first day and add
1.week to that day until stop
For the ranged solutions, there are different ways to use the range:
- Take all the dates and group them by weekday
- Run the select function on the range
- Convert range to array and then run select
How you select dates is also important:
- Run
include? on the days requested
- Find intersection between two arrays and check if empty
I 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:
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 passed
Conclusion
If you combine select and include?, you have the fastest and most reliable solution for this problem