在休假期限内延长截止日期

时间:2018-06-13 17:27:40

标签: ruby-on-rails ruby

假设我有一些截止时间对象deadline = project_start + 34.days

现在让我们假设某种假期/推迟的业务逻辑必须让我延长这些假期的最后期限。

让我们假设我的假期是阵列或范围(或几个开始/结束日期,无论如何)并且该项目于12月15日开始

project_start = Time.parse('2017-12-15 0:00') # December 15th
deadline = project_start + 34.days            # January 18th

vacations = [
  Time.parse('2017-12-10 0:00')..Time.parse('2018-01-20 0:00'), # Starts 5 days before the project start, so only 5 out of 10 days extend the deadline
  Time.parse('2018-01-05 0:00')..Time.parse('2018-01-10 0:00'), # Starts during the project so whole vacation period must extend the deadline
  Time.parse('2018-01-20 0:00')..Time.parse('2018-01-25 0:00'), # STarts after the original deadline, but dued to previous vacations, this vacation fully extends the deadline
]

考虑到假期,我如何(轻松)计算延长的截止日期?

我尝试寻找现有的解决方案,但我发现大多数宝石涉及静态假期,但我对每个用户的内容感兴趣。

基本上我在考虑订购假期,并延长每个步骤的截止日期,但我需要考虑到在实际项目开始之前可能已经开始休假。要弄清楚正确的事情变得有点困难。我将发布第一次尝试

2 个答案:

答案 0 :(得分:0)

所以这是我设计的第一种方法,我正在编写规格,也许它已经很好但我有疑问

# Extends a deadline with extra time provided as "vacations"
#
# @param range: [Range<Time>]
# @param vacations: [Array<Range<Time>>]
#
# @return [Range]
def extend_time_range_with_vacations(range:, vacations:)
  extended_deadline = range.last.dup
  vacations.each do |vacation|
    cover = (range.first..extended_deadline) & vacation
    next unless cover.present?
    # Here I need to pick more than just the intersection
    #   For eg maybe the vacation is 2.weeks long
    #   but the range started only in the 2nd week of the vacation,
    #   hence the time extension is 1.week only)
    actual_cover = cover.first..vacation.last
    extended_deadline += (actual_cover.last - actual_cover.first)
  end
  extended_deadline
end

但感觉还有很多工作要做,也许更简单的事情存在?

编辑:代码已更改为使用this range intersection monkeypatch

编辑:规范

describe '#extend_time_range_with_vacations' do
  let(:start_d) { Time.parse('2018-04-20 8:42') }
  let(:end_d) { start_d + 42.days }
  let(:range) { start_d..end_d }
  let(:vacations) do
    [
      (start_d - 4.days)..(start_d + 4.days), # 4 days extension out of 8
      (start_d + 10.days)..(start_d + 14.days), # 4 days extension
      (end_d + 4.days)..(end_d + 8.days), # 4 days extensions unlocked because of previous vacations
    ]
  end

  it 'returns the expected extended deadline' do
    new_range = Utility.extend_time_range_with_vacations(range: range, vacations: vacations)
    expect(new_range).to be_within(1).of(end_d + (4 * 3).days)
  end
end

答案 1 :(得分:0)

我已经解决了以下问题。我们将获得完成项目所需的开始日期和工作天数。我们还有一系列假期。如果项目已经开始并且在给定日期尚未完成的情况下,如果该日期属于任何休假期,则该日期不会执行任何工作。每个休假期是连续几天的范围,可以在开始日期之前开始或结束,并且可以与其他休假期重叠。 1

目标是确定项目完成的日期。

问题的表现方式不同,但如果我的解释是准确的而不是单位(例如,我假设日期输入和日期输出是字符串而不是Time对象),它会不难修改我的答案以符合要求。

<强>代码

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

示例

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)] }

打印

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

<强>解释

Date#strftimeDate::strptimeDate#next(又名next_day)和Enumerable#reduce(又名inject)。 reduce采用&#34; set union&#34;通过应用方法Array#|来度假数组(连续日期)。

我将休假日期数组转换为快速查找集。 对于给出的示例,如果将vacation_dates的元素转换为非重叠范围的数组,然后将范围端点转换为日期字符串,则该数组将如下所示。

["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"]

第一个范围完全在start_date之前,但没有必要删除它。更一般地说,没有必要在start_date之前删除休假日期。

通过添加一些puts语句可以看到计算假期日期集的步骤。为了提高可读性,我删除了返回值和打印行范围内的Date个对象,用,...,替换它们的组。我还缩写了Date个对象。例如,我将#<Date: 2017-12-13 ((2458101j,0s,0n),+0s,2299161j)>缩短为#<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...>}>

打印(范围未显示的元素)

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如果保证度假范围不重叠,我的解决方案将是相同的。