How do I get all Sundays between two dates in Ruby?

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


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.


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:

  • 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 passed

Conclusion

If you combine select and include?, you have the fastest and most reliable solution for this problem