I have addressed the following problem. We are given a start date and the number of days of work required to complete a project. We are also given an array of vacation periods. If the project has begun and by a given date has not been completed, no work is performed on that date if that date falls within any vacation period. Each vacation period is a range of consecutive days which may begin or end before the start date and which may overlap other vacation periods.1.
The objective is to determine the date on which the project will be completed.
The question is cast differently, but if my interpretation is accurate other than for the units (e.g., I've assumed date inputs and the date output are strings rather than Time
objects`), it would not be difficult to modify my answer to be consistent with requirements.
Code
require 'date'
require 'set'
DATE_FMT = "%Y-%m-%d"
def end_date(start_date, duration, vacations)
start_date = Date.strptime(start_date, DATE_FMT)
vacation_dates = construct_vacations_dates_set(vacations)
d = start_date
while duration > 0
duration -= 1 unless vacation_dates.include?(d)
d = d.next
end
d.strftime(DATE_FMT)
end
def construct_vacations_dates_set(vacations)
vacations.map do |a|
s, e = a.map { |s| Date.strptime(s, DATE_FMT) }
(s..e).to_a
end.
reduce(:|).
to_set
end
Example
start_date = '2017-12-15'
vacations = [['2017-11-20', '2017-12-03'],
['2017-12-13', '2017-12-19'],
['2017-12-23', '2018-01-02'],
['2017-12-29', '2018-01-08'],
['2018-01-14', '2018-02-28'],
['2018-05-18', '2018-06-01']]
(1..20).each { |n| puts "for duration %2d days: %s" %
[n, end_date(start_date, n, vacations)] }
prints
for duration 1 days: 2017-12-21
for duration 2 days: 2017-12-22
for duration 3 days: 2017-12-23
for duration 4 days: 2018-01-10
for duration 5 days: 2018-01-11
for duration 6 days: 2018-01-12
for duration 7 days: 2018-01-13
for duration 8 days: 2018-01-14
for duration 9 days: 2018-03-02
for duration 10 days: 2018-03-03
for duration 11 days: 2018-03-04
for duration 12 days: 2018-03-05
for duration 13 days: 2018-03-06
for duration 14 days: 2018-03-07
for duration 15 days: 2018-03-08
for duration 16 days: 2018-03-09
for duration 17 days: 2018-03-10
for duration 18 days: 2018-03-11
for duration 19 days: 2018-03-12
for duration 20 days: 2018-03-13
Explanation
See Date#strftime,
Date::strptime, Date#next (aka next_day
) and Enumerable#reduce (aka inject
). reduce
takes the "set union" of the vacation arrays (of consecutive dates) by applying the method Array#|.
I converted the array of vacation dates to a set for fast lookups.
For the example given, if the elements of vacation_dates
were converted to an array of non-overlapping ranges, and then the range endpoint were converted to date strings, that array would be as follows.
["2017-11-20".."2017-12-03",
"2017-12-13".."2017-12-19",
"2017-12-23".."2018-01-08",
"2018-01-14".."2018-02-28",
"2018-05-18".."2018-06-01"]
The first range is wholly before start_date
, but there is no need to remove it. More generally, there is no need to remove vacation dates before start_date
.
The steps to compute the set of vacation dates can be seen by adding some puts
statements. To improve readability I have removed Date
objects that fall within ranges in the return value and printed lines, replacing groups of them with ,...,
. I also abbreviated Date
objects. For example, I shortened #<Date: 2017-12-13 ((2458101j,0s,0n),+0s,2299161j)>
to #<Date: 2017-12-13...>
.
def construct_vacations_dates_set(vacations)
vacations.map do |a|
puts "a = #{a}"
s, e = a.map { |s| Date.strptime(s, DATE_FMT) }
puts " s = #{s}, e = #{e}"
(s..e).to_a.
tap { |o| puts " (s..e).to_a = #{o}" }
end.
reduce(:|).
tap { |o| puts "after reduce(:|) = #{o}" }.
to_set
end
construct_vacations_dates_set([['2017-12-13', '2017-12-19'],
['2017-12-23', '2018-01-02'], ['2017-12-29', '2018-01-08']])
#=> #<Set: {#<Date: 2017-12-13...>,..., #<Date: 2017-12-19...>,
# #<Date: 2017-12-23...>,..., #<Date: 2018-01-08...>}>
prints (with elements in ranges not shown)
a = ["2017-12-13", "2017-12-19"]
s = 2017-12-13, e = 2017-12-19
(s..e).to_a = [#<Date: 2017-12-13...>,.., #<Date: 2017-12-19..>]
a = ["2017-12-23", "2018-01-02"]
s = 2017-12-23, e = 2018-01-02
(s..e).to_a = [#<Date: 2017-12-23...>,..., #<Date: 2018-01-02...>]
a = ["2017-12-29", "2018-01-08"]
s = 2017-12-29, e = 2018-01-08
(s..e).to_a = [#<Date: 2017-12-29...>,..., #<Date: 2018-01-08...>]
after reduce(:|) = [#<Date: 2017-12-13...>,..., #<Date: 2017-12-19...>,
#<Date: 2017-12-23...>,..., #<Date: 2018-01-08...>]
1 My solution would be the same if it were guaranteed that vacation ranges do not overlap.