Class: Roxbury::BusinessCalendar

Inherits:
Object
  • Object
show all
Defined in:
lib/roxbury/business_calendar.rb

Constant Summary collapse

DAYS_OF_THE_WEEK =
%w[Mon Tue Wed Thu Fri Sat Sun]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(working_hours: {}, holidays: []) ⇒ BusinessCalendar

Returns a new instance of BusinessCalendar.



7
8
9
10
11
12
13
14
15
# File 'lib/roxbury/business_calendar.rb', line 7

def initialize working_hours: {}, holidays: []
  @working_hours = DAYS_OF_THE_WEEK.inject({}) do |wh, dow|
    wh.merge dow => WorkingHours.parse(working_hours[dow])
  end
  if @working_hours.values.all?(&:non_working?)
    raise ArgumentError, 'You must specify at least one working day in working_hours.'
  end
  @holidays = Set.new(holidays)
end

Instance Attribute Details

#working_hoursObject (readonly)

Returns the value of attribute working_hours.



5
6
7
# File 'lib/roxbury/business_calendar.rb', line 5

def working_hours
  @working_hours
end

Instance Method Details

#add_working_days(to, number_of_days) ⇒ Date, Time

Returns The result of adding the number_of_days to the given date. If a Date is given returns a Date, otherwise if a Time is given returns a Time.

Parameters:

  • to (Date, Time)
  • number_of_days (Integer, Float)

Returns:

  • (Date, Time)

    The result of adding the number_of_days to the given date. If a Date is given returns a Date, otherwise if a Time is given returns a Time.

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/roxbury/business_calendar.rb', line 60

def add_working_days to, number_of_days
  raise ArgumentError, 'number_of_days must not be negative' if number_of_days < 0

  if to.is_a?(Time)
    # this implementation would work for Date instances as well, but is around 10 times slower than the other in my machine
    add_working_hours(to, number_of_days * max_working_hours_in_a_day)
  else
    remaining_days = number_of_days
    rolling_date = to
    loop do
      remaining_days -= working_hours_percentage(rolling_date)
      break if remaining_days < 0
      rolling_date = roll_forward rolling_date.next
    end
    rolling_date
  end
end

#add_working_hours(to, number_of_hours) ⇒ Object

Raises:

  • (ArgumentError)


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/roxbury/business_calendar.rb', line 33

def add_working_hours to, number_of_hours
  raise ArgumentError, 'number_of_hours must not be negative' if number_of_hours < 0
  to = cast_time(to, :start)
  rolling_timestamp = roll_forward(to)
  remaining_hours = number_of_hours

  loop do
    bday = business_day(rolling_timestamp)
    break if bday.include?(rolling_timestamp + remaining_hours.hours)
    remaining_hours -= bday.number_of_working_hours(from: rolling_timestamp)
    break if remaining_hours < 0
    rolling_timestamp = next_working_day(rolling_timestamp)
  end

  rolling_timestamp + remaining_hours.hours
end

#holiday?(date_or_time) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/roxbury/business_calendar.rb', line 136

def holiday? date_or_time
  @holidays.include?(date_or_time.to_date)
end

#next_working_day(date) ⇒ Object

If a Date is given, returns then next business day. Otherwise if a Time is given, snaps the date to the beginning of the next business day.



108
109
110
111
112
113
114
115
116
117
# File 'lib/roxbury/business_calendar.rb', line 108

def next_working_day date
  case date
  when Time
    roll_forward date.tomorrow.beginning_of_day
  when Date
    roll_forward date.tomorrow
  else
    raise ArgumentError, 'only Date or Time instances are allowed'
  end
end

#prev_working_day(date) ⇒ Object

If a Date is given, returns then prev business day. Otherwise if a Time is given, snaps the date to the beginning of the prev business day.



121
122
123
124
125
126
127
128
129
130
# File 'lib/roxbury/business_calendar.rb', line 121

def prev_working_day date
  case date
  when Time
    roll_backward date.yesterday.end_of_day
  when Date
    roll_backward date.yesterday
  else
    raise ArgumentError, 'only Date or Time instances are allowed'
  end
end

#roll_backward(date) ⇒ Object

Snaps the date to the end of the previous business day, unless it is already within the working hours of a business day.

Parameters:

  • date (Date, Time)


95
96
97
98
99
100
101
102
103
104
# File 'lib/roxbury/business_calendar.rb', line 95

def roll_backward date
  bday = business_day(date)
  if bday.include?(date)
    date
  elsif bday.ends_before?(date)
    bday.at_end
  else
    roll_backward(date.is_a?(Date) ? date.yesterday : date.yesterday.end_of_day)
  end
end

#roll_forward(date) ⇒ Object

Snaps the date to the beginning of the next business day, unless it is already within the working hours of a business day.

Parameters:

  • date (Date, Time)


81
82
83
84
85
86
87
88
89
90
# File 'lib/roxbury/business_calendar.rb', line 81

def roll_forward date
  bday = business_day(date)
  if bday.include?(date)
    date
  elsif bday.starts_after?(date)
    bday.at_beginning
  else
    roll_forward(date.is_a?(Date) ? date.tomorrow : date.tomorrow.beginning_of_day)
  end
end

#working_days_between(from, to) ⇒ Float

Returns the number of working days between the given dates.

Parameters:

  • from (Date, Time)

    if it’s a date, it’s handled as the beginning of the day

  • to (Date, Time)

    if it’s a date, it’s handled as the end of the day

Returns:

  • (Float)

    the number of working days between the given dates.



53
54
55
# File 'lib/roxbury/business_calendar.rb', line 53

def working_days_between from, to
  working_hours_between(from, to) / max_working_hours_in_a_day.to_f
end

#working_hours_between(from, to) ⇒ Float

Returns the number of working hours between the given dates.

Parameters:

  • from (Date, Time)

    if it’s a date, it’s handled as the beginning of the day

  • to (Date, Time)

    if it’s a date, it’s handled as the end of the day

Returns:

  • (Float)

    the number of working hours between the given dates



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/roxbury/business_calendar.rb', line 20

def working_hours_between from, to
  from, to, sign = invert_if_needed cast_time(from, :start), cast_time(to, :end)

  working_hours_per_day = (from.to_date..to.to_date).map do |date|
    filters = {}
    filters[:from] = from if date == from.to_date
    filters[:to] = to if date == to.to_date
    business_day(date).number_of_working_hours filters
  end

  working_hours_per_day.sum.round(2) * sign
end

#working_hours_percentage(date) ⇒ Object



132
133
134
# File 'lib/roxbury/business_calendar.rb', line 132

def working_hours_percentage date
  business_day(date).number_of_working_hours * 1.0 / max_working_hours_in_a_day
end