lib/workpattern/week.rb in workpattern-0.4.0 vs lib/workpattern/week.rb in workpattern-0.5.0
- old
+ new
@@ -1,113 +1,206 @@
module Workpattern
-
+ # The representation of a week might not be obvious so I am writing about it
+ # here. It will also help me if I ever need to come back to this in the
+ # future.
+ #
+ # Each day is represented by a binary number where a 1 represents a working
+ # minute and a 0 represents a resting minute.
+ #
+ # @private
class Week
-
attr_accessor :values, :hours_per_day, :start, :finish, :week_total, :total
- def initialize(start,finish,type=1,hours_per_day=24)
+ def initialize(start, finish, type = 1, hours_per_day = 24)
@hours_per_day = hours_per_day
- @start=DateTime.new(start.year,start.month,start.day)
- @finish=DateTime.new(finish.year,finish.month,finish.day)
+ @start = Time.gm(start.year, start.month, start.day)
+ @finish = Time.gm(finish.year, finish.month, finish.day)
@values = Array.new(6)
- 0.upto(6) do |i|
+ 0.upto(6) do |i|
@values[i] = working_day * type
end
end
- def <=>(other_week)
- if self.start < other_week.start
- return -1
- elsif self.start == other_week.start
- return 0
- else
- return 1
- end
+ def <=>(other)
+ return -1 if start < other.start
+ return 0 if start == other.start
+ 1
end
-
+
def week_total
- span_in_days > 6 ? full_week_total_minutes : part_week_total_minutes
- end
+ elapsed_days > 6 ? full_week_total_minutes : part_week_total_minutes
+ end
def total
- total_days = span_in_days
- return week_total if total_days < 8
- sum = sum_of_minutes_in_day_range(self.start.wday, 6)
- total_days -= (7-self.start.wday)
- sum += sum_of_minutes_in_day_range(0,self.finish.wday)
- total_days-=(self.finish.wday+1)
- sum += week_total * total_days / 7
- return sum
+ elapsed_days < 8 ? week_total : range_total
end
- def workpattern(days,from_time,to_time,type)
- DAYNAMES[days].each do |day|
- type==1 ? work_on_day(day,from_time,to_time) : rest_on_day(day,from_time,to_time)
+ def workpattern(days, from_time, to_time, type)
+ DAYNAMES[days].each do |day|
+ if type == 1
+ work_on_day(day, from_time, to_time)
+ else
+ rest_on_day(day, from_time, to_time)
+ end
end
end
- def duplicate()
- duplicate_week=Week.new(self.start,self.finish)
- 0.upto(6).each do |i| duplicate_week.values[i] = @values[i] end
- return duplicate_week
+ def duplicate
+ duplicate_week = Week.new(start, finish)
+ 0.upto(6).each { |i| duplicate_week.values[i] = @values[i] }
+ duplicate_week
end
- def calc(start_date,duration, midnight=false)
- return start_date,duration,false if duration==0
- return add(start_date,duration) if duration > 0
- return subtract(self.start,duration, midnight) if (self.total==0) && (duration <0)
- return subtract(start_date,duration, midnight) if duration <0
+ def calc(start_date, duration, midnight = false)
+ return start_date, duration, false if duration == 0
+ return add(start_date, duration) if duration > 0
+ return subtract(start, duration, midnight) if total == 0 && duration < 0
+ subtract(start_date, duration, midnight)
end
- def working?(date)
- return true if bit_pos_time(date) & @values[date.wday] > 0
+ def working?(time)
+ return true if bit_pos(time.hour, time.min) & @values[time.wday] > 0
false
end
def resting?(date)
!working?(date)
end
- def diff(start_date,finish_date)
- start_date,finish_date=finish_date,start_date if ((start_date <=> finish_date))==1
-
- if (start_date.jd==finish_date.jd)
- duration, start_date=diff_in_same_day(start_date, finish_date)
- elsif (finish_date.jd<=self.finish.jd)
- duration, start_date=diff_in_same_weekpattern(start_date,finish_date)
- else
- duration, start_date=diff_beyond_weekpattern(start_date,finish_date)
- end
- return duration, start_date
+ def diff(start_d, finish_d)
+ start_d, finish_d = finish_d, start_d if ((start_d <=> finish_d)) == 1
+ return diff_in_same_day(start_d, finish_d) if jd(start_d) == jd(finish_d)
+ return diff_in_same_weekpattern(start_d, finish_d) if jd(finish_d) <= jd(finish)
+ diff_beyond_weekpattern(start_d, finish_d)
end
- private
+ private
- def span_in_days
- (self.finish-self.start).to_i + 1
+ def working_minutes_in(day)
+ day.to_s(2).count('1')
end
+ def elapsed_days
+ (finish - start).to_i / 86_400 + 1
+ end
+
def full_week_total_minutes
- sum_of_minutes_in_day_range 0, 6
+ minutes_in_day_range 0, 6
end
-
+
def part_week_total_minutes
+ start.wday <= finish.wday ? no_rollover_minutes : rollover_minutes
+ end
- if self.start.wday <= self.finish.wday
- total = sum_of_minutes_in_day_range(self.start.wday, self.finish.wday)
+ def no_rollover_minutes
+ minutes_in_day_range(start.wday, finish.wday)
+ end
+
+ def rollover_minutes
+ minutes_to_first_saturday + minutes_to_finish_day
+ end
+
+ def range_total
+ total_days = elapsed_days
+
+ sum = minutes_to_first_saturday
+ total_days -= (7 - start.wday)
+
+ sum += minutes_to_finish_day
+ total_days -= (finish.wday + 1)
+
+ sum += week_total * total_days / 7
+ sum
+ end
+
+ def minutes_to_first_saturday
+ minutes_in_day_range(start.wday, 6)
+ end
+
+ def minutes_to_finish_day
+ minutes_in_day_range(0, finish.wday)
+ end
+
+ def minutes_in_day_range(first, last)
+ @values[first..last].inject(0) { |a, e| a + working_minutes_in(e) }
+ end
+
+ def add(initial_date, duration)
+ running_date, duration = add_to_end_of_day(initial_date, duration)
+
+ running_date, duration = add_to_finish_day running_date, duration
+ running_date, duration = add_full_weeks running_date, duration
+ running_date, duration = add_remaining_days running_date, duration
+ [running_date, duration, false]
+ end
+
+ def add_to_end_of_day(initial_date, duration)
+ available_minutes_in_day = minutes_to_end_of_day(initial_date)
+
+ if available_minutes_in_day < duration
+ duration -= available_minutes_in_day
+ initial_date = start_of_next_day(initial_date)
+ elsif available_minutes_in_day == duration
+ duration -= available_minutes_in_day
+ initial_date = end_of_this_day(initial_date)
else
- total = sum_of_minutes_in_day_range(self.start.wday, 6)
- total += sum_of_minutes_in_day_range(0, self.finish.wday)
+ initial_date = consume_minutes(initial_date, duration)
+ duration = 0
end
- return total
+ [initial_date, duration]
end
- def sum_of_minutes_in_day_range(first,last)
- @values[first..last].inject(0) {|sum,item| sum + item.to_s(2).count('1')}
+ def add_to_finish_day(date, duration)
+ while (duration != 0) && (date.wday != next_day(finish).wday) && (jd(date) <= jd(finish))
+ date, duration = add_to_end_of_day(date, duration)
+ end
+
+ [date, duration]
end
+ def add_full_weeks(date, duration)
+ while (duration != 0) && (duration >= week_total) && ((jd(date) + (6 * 86_400)) <= jd(finish))
+ duration -= week_total
+ date += (7 * 86_400)
+ end
+
+ [date, duration]
+ end
+
+ def add_remaining_days(date, duration)
+ while (duration != 0) && (jd(date) <= jd(finish))
+ date, duration = add_to_end_of_day(date, duration)
+ end
+ [date, duration]
+ end
+
+ def add_to_finish_day(date, duration)
+ while ( duration != 0) && (date.wday != next_day(self.finish).wday) && (jd(date) <= jd(self.finish))
+ date, duration = add_to_end_of_day(date,duration)
+ end
+
+ return date, duration
+ end
+
+ def add_full_weeks(date, duration)
+
+ while (duration != 0) && (duration >= self.week_total) && ((jd(date) + (6*86400)) <= jd(self.finish))
+ duration -= self.week_total
+ date += (7*86400)
+ end
+
+ return date, duration
+ end
+
+ def add_remaining_days(date, duration)
+ while (duration != 0) && (jd(date) <= jd(self.finish))
+ date, duration = add_to_end_of_day(date,duration)
+ end
+ return date, duration
+ end
+
def work_on_day(day,from_time,to_time)
self.values[day] = self.values[day] | time_mask(from_time, to_time)
end
def rest_on_day(day,from_time,to_time)
@@ -115,267 +208,251 @@
mask = mask_of_1s ^ working_day & working_day
self.values[day] = self.values[day] & mask
end
def time_mask(from_time, to_time)
- bit_pos_above_time(to_time) - bit_pos_time(from_time)
+ bit_pos(to_time.hour, to_time.min + 1) - bit_pos(from_time.hour, from_time.min)
end
- def bit_pos_above_time(time)
- bit_pos(time.hour, time.min+1)
- end
-
def bit_pos(hour,minute)
2**( (hour * 60) + minute )
end
- def bit_pos_time(time)
- bit_pos(time.hour,time.min)
+ def work_on_day(day, from_time, to_time)
+ values[day] = values[day] | time_mask(from_time, to_time)
end
- def add(initial_date,duration)
+ def rest_on_day(day, from_time, to_time)
+ mask_of_ones = time_mask(from_time, to_time)
+ mask = mask_of_ones ^ working_day & working_day
+ values[day] = values[day] & mask
+ end
- initial_date, duration = add_to_end_of_day(initial_date,duration)
-
- while ( duration != 0) && (initial_date.wday != self.finish.next_day.wday) && (initial_date.jd <= self.finish.jd)
- initial_date, duration = add_to_end_of_day(initial_date,duration)
- end
-
- while (duration != 0) && (duration >= self.week_total) && ((initial_date.jd + 6) <= self.finish.jd)
- duration -= self.week_total
- initial_date += 7
- end
-
- while (duration != 0) && (initial_date.jd <= self.finish.jd)
- initial_date, duration = add_to_end_of_day(initial_date,duration)
- end
- return initial_date, duration, false
-
+ def time_mask(from_time, to_time)
+ bit_pos(to_time.hour, to_time.min + 1) - bit_pos(from_time.hour, from_time.min)
end
- def add_to_end_of_day(initial_date, duration)
- available_minutes_in_day = minutes_to_end_of_day(initial_date)
-
- if available_minutes_in_day < duration
- duration -= available_minutes_in_day
- initial_date = start_of_next_day(initial_date)
- elsif available_minutes_in_day == duration
- duration -= available_minutes_in_day
- initial_date = end_of_this_day(initial_date)
- else
- initial_date = consume_minutes(initial_date,duration)
- duration=0
- end
- return initial_date, duration
+ def bit_pos(hour, minute)
+ 2**((hour * 60) + minute)
end
- def minutes_to_end_of_day(date)
- pattern_to_end_of_day(date).to_s(2).count('1')
+ def minutes_to_end_of_day(date)
+ working_minutes_in pattern_to_end_of_day(date)
end
- def pattern_to_end_of_day(date)
+ def pattern_to_end_of_day(date)
mask = mask_to_end_of_day(date)
- (self.values[date.wday] & mask)
+ (values[date.wday] & mask)
end
- def mask_to_end_of_day(date)
- bit_pos(self.hours_per_day,0) - bit_pos(date.hour, date.min)
+ def mask_to_end_of_day(date)
+ bit_pos(hours_per_day, 0) - bit_pos(date.hour, date.min)
end
def working_day
- 2**(60*self.hours_per_day)-1
+ 2**(60 * hours_per_day) - 1
end
def start_of_next_day(date)
- date.next_day - (HOUR * date.hour) - (MINUTE * date.minute)
+ next_day(date) - (HOUR * date.hour) - (MINUTE * date.min)
end
def start_of_previous_day(date)
- start_of_next_day(date).prev_day.prev_day
+ prev_day(prev_day(start_of_next_day(date)))
end
def start_of_today(date)
- start_of_next_day(date.prev_day)
+ start_of_next_day(prev_day(date))
end
- def end_of_this_day(date)
+ def end_of_this_day(date)
position = pattern_to_end_of_day(date).to_s(2).size
- return adjust_date(date,position)
+ adjust_date(date, position)
end
- def adjust_date(date,adjustment)
+ def adjust_date(date, adjustment)
date - (HOUR * date.hour) - (MINUTE * date.min) + (MINUTE * adjustment)
end
- def diff_minutes_to_end_of_day(start_date)
- mask = ((2**(60*self.hours_per_day + 1)) - (2**(start_date.hour*60 + start_date.min))).to_i
- return (self.values[start.wday] & mask).to_s(2).count('1')
- end
-
def mask_to_start_of_day(date)
- bit_pos(date.hour, date.min) - bit_pos(0,0)
+ bit_pos(date.hour, date.min) - bit_pos(0, 0)
end
-
+
def pattern_to_start_of_day(date)
mask = mask_to_start_of_day(date)
- (self.values[date.wday] & mask)
+ (values[date.wday] & mask)
end
def minutes_to_start_of_day(date)
- pattern_to_start_of_day(date).to_s(2).count('1')
+ working_minutes_in pattern_to_start_of_day(date)
end
- def consume_minutes(date,duration)
+ def consume_minutes(date, duration)
+ minutes = pattern_to_end_of_day(date).to_s(2).reverse! if duration > 0
+ minutes = pattern_to_start_of_day(date).to_s(2) if duration < 0
- minutes=pattern_to_end_of_day(date).to_s(2).reverse! if duration > 0
- minutes=pattern_to_start_of_day(date).to_s(2) if duration < 0
-
- top=minutes.size
- bottom=1
+ top = minutes.size
+ bottom = 1
mark = top / 2
- while minutes[0,mark].count('1') != duration.abs
+ while minutes[0, mark].count('1') != duration.abs
last_mark = mark
- if minutes[0,mark].count('1') < duration.abs
+ if minutes[0, mark].count('1') < duration.abs
bottom = mark
- mark = (top-mark) / 2 + mark
+ mark = (top - mark) / 2 + mark
mark = top if last_mark == mark
else
top = mark
- mark = (mark-bottom) / 2 + bottom
- mark = bottom if last_mark = mark
+ mark = (mark - bottom) / 2 + bottom
+ mark = bottom if last_mark == mark
- end
+ end
end
mark = minutes_addition_adjustment(minutes, mark) if duration > 0
- mark = minutes_subtraction_adjustment(minutes,mark) if duration < 0
+ mark = minutes_subtraction_adjustment(minutes, mark) if duration < 0
return adjust_date(date, mark) if duration > 0
return start_of_today(date) + (MINUTE * mark) if duration < 0
-
end
-
- def minutes_subtraction_adjustment(minutes,mark)
- i = mark - 1
-
- while minutes[i]=='0'
- i-=1
- end
-
+
+ def minutes_subtraction_adjustment(minutes, mark)
+ i = mark - 1
+
+ while minutes[i] == '0'
+ i -= 1
+ end
+
minutes.size - (i + 1)
end
- def minutes_addition_adjustment(minutes,mark)
- minutes=minutes[0,mark]
+ def minutes_addition_adjustment(minutes, mark)
+ minutes = minutes[0, mark]
- while minutes[minutes.size-1]=='0'
+ while minutes[minutes.size - 1] == '0'
minutes.chop!
end
minutes.size
end
def subtract_to_start_of_day(initial_date, duration, midnight)
-
- initial_date,duration, midnight = handle_midnight(initial_date, duration) if midnight
+ initial_date, duration, midnight = handle_midnight(initial_date, duration) if midnight
available_minutes_in_day = minutes_to_start_of_day(initial_date)
if duration != 0
if available_minutes_in_day < duration.abs
duration += available_minutes_in_day
initial_date = start_of_previous_day(initial_date)
midnight = true
else
- initial_date = consume_minutes(initial_date,duration)
+ initial_date = consume_minutes(initial_date, duration)
duration = 0
midnight = false
end
end
- return initial_date, duration, midnight
+ [initial_date, duration, midnight]
end
-
- def handle_midnight(initial_date,duration)
+ def handle_midnight(initial_date, duration)
if working?(start_of_next_day(initial_date) - MINUTE)
duration += 1
end
-
+
initial_date -= (HOUR * initial_date.hour)
initial_date -= (MINUTE * initial_date.min)
- initial_date = initial_date.next_day - MINUTE
+ initial_date = next_day(initial_date) - MINUTE
- return initial_date, duration, false
+ [initial_date, duration, false]
end
-
def subtract(initial_date, duration, midnight)
- initial_date,duration, midnight = handle_midnight(initial_date, duration) if midnight
+ initial_date, duration, midnight = handle_midnight(initial_date, duration) if midnight
initial_date, duration, midnight = subtract_to_start_of_day(initial_date, duration, midnight)
- while ( duration != 0) && (initial_date.wday != self.start.prev_day.wday) && (initial_date.jd >= self.start.jd)
- initial_date, duration, midnight = subtract_to_start_of_day(initial_date,duration, midnight)
+ while (duration != 0) && (initial_date.wday != prev_day(start.wday)) && (jd(initial_date) >= jd(start))
+ initial_date, duration, midnight = subtract_to_start_of_day(initial_date, duration, midnight)
end
- while (duration != 0) && (duration >= self.week_total) && ((initial_date.jd - 6) >= self.start.jd)
- duration += self.week_total
+ while (duration != 0) && (duration >= week_total) && ((jd(initial_date) - (6 * 86_400)) >= jd(start))
+ duration += week_total
initial_date -= 7
end
- while (duration != 0) && (initial_date.jd >= self.start.jd)
- initial_date, duration, midnight = subtract_to_start_of_day(initial_date,duration, midnight)
+ while (duration != 0) && (jd(initial_date) >= jd(start))
+ initial_date, duration, midnight = subtract_to_start_of_day(initial_date,
+ duration,
+ midnight)
end
- return initial_date, duration, midnight
-
+ [initial_date, duration, midnight]
end
def diff_in_same_weekpattern(start_date, finish_date)
duration, start_date = diff_to_tomorrow(start_date)
- while true
- break if (start_date.wday == (self.finish.wday + 1))
- break if (start_date.jd == self.finish.jd)
- break if (start_date.jd == finish_date.jd)
+ loop do
+ break if start_date.wday == (finish.wday + 1)
+ break if jd(start_date) == jd(finish)
+ break if jd(start_date) == jd(finish_date)
duration += minutes_to_end_of_day(start_date)
start_date = start_of_next_day(start_date)
- end
+ end
- while true
- break if ((start_date + 7) > finish_date)
- break if ((start_date + 6).jd > self.finish.jd)
+ loop do
+ break if (start_date + (7 * 86_400)) > finish_date
+ break if jd(start_date + (6 * 86_400)) > jd(finish)
duration += week_total
- start_date += 7
+ start_date += (7 * 86_400)
end
- while true
- break if (start_date.jd >= self.finish.jd)
- break if (start_date.jd >= finish_date.jd)
+ loop do
+ break if jd(start_date) >= jd(finish)
+ break if jd(start_date) >= jd(finish_date)
duration += minutes_to_end_of_day(start_date)
start_date = start_of_next_day(start_date)
- end
-
- interim_duration, start_date = diff_in_same_day(start_date, finish_date) if (start_date < self.finish)
+ end
+
+ if start_date < finish
+ interim_duration, start_date = diff_in_same_day(start_date, finish_date)
+ end
duration += interim_duration unless interim_duration.nil?
- return duration, start_date
+ [duration, start_date]
end
-
- def diff_beyond_weekpattern(start_date,finish_date)
+
+ def diff_beyond_weekpattern(start_date, finish_date)
duration, start_date = diff_in_same_weekpattern(start_date, finish_date)
- return duration, start_date
+ [duration, start_date]
end
def diff_to_tomorrow(start_date)
- mask = bit_pos(self.hours_per_day, 0) - bit_pos(start_date.hour, start_date.min)
- return (self.values[start_date.wday] & mask).to_s(2).count('1'), start_of_next_day(start_date)
+ finish_bit_pos = bit_pos(hours_per_day, 0)
+ start_bit_pos = bit_pos(start_date.hour, start_date.min)
+ mask = finish_bit_pos - start_bit_pos
+ minutes = working_minutes_in(values[start_date.wday] & mask)
+ [minutes, start_of_next_day(start_date)]
end
def diff_in_same_day(start_date, finish_date)
- mask = bit_pos(finish_date.hour, finish_date.min) - bit_pos(start_date.hour, start_date.min)
- return (self.values[start_date.wday] & mask).to_s(2).count('1'), finish_date
+ finish_bit_pos = bit_pos(finish_date.hour, finish_date.min)
+ start_bit_pos = bit_pos(start_date.hour, start_date.min)
+ mask = finish_bit_pos - start_bit_pos
+ minutes = working_minutes_in(values[start_date.wday] & mask)
+ [minutes, finish_date]
end
+ def next_day(time)
+ time + 86_400
+ end
+
+ def prev_day(time)
+ time - 86_400
+ end
+
+ def jd(time)
+ Time.gm(time.year, time.month, time.day)
+ end
end
end