lib/workpattern/week.rb in workpattern-0.3.6 vs lib/workpattern/week.rb in workpattern-0.4.0

- old
+ new

@@ -1,385 +1,381 @@ module Workpattern - - # @author Barrie Callender - # @!attribute values - # @return [Array] each day of the week - # @!attribute days - # @return [Integer] number of days in the week - # @!attribute start - # @return [DateTime] first date in the range - # @!attribute finish - # @return [DateTime] last date in the range - # @!attribute week_total - # @return [Integer] total number of minutes in a week - # @!attribute total - # @return [Integer] total number of minutes in the range - # - # Represents working and resting periods for each day in a week for a specified date range. - # - # @since 0.2.0 - # + class Week - attr_accessor :values, :days, :start, :finish, :week_total, :total + attr_accessor :values, :hours_per_day, :start, :finish, :week_total, :total - # The new <tt>Week</tt> object can be created as either working or resting. - # - # @param [DateTime] start first date in the range - # @param [DateTime] finish last date in the range - # @param [Integer] type working (1) or resting (0) - # @return [Week] newly initialised Week object - # - def initialize(start,finish,type=1) - hours_in_days_in_week=[24,24,24,24,24,24,24] - @days=hours_in_days_in_week.size - @values=Array.new(7) {|index| Day.new(type)} + 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) - - set_attributes + @values = Array.new(6) + 0.upto(6) do |i| + @values[i] = working_day * type + end end - - # Duplicates the current <tt>Week</tt> object - # - # @return [Week] a duplicated instance of the current <tt>Week</tt> object - # - def duplicate() - duplicate_week=Week.new(self.start,self.finish) - duplicate_values=Array.new(self.values.size) - self.values.each_index {|index| - duplicate_values[index]=self.values[index].duplicate - } - duplicate_week.values=duplicate_values - duplicate_week.days=self.days - duplicate_week.start=self.start - duplicate_week.finish=self.finish - duplicate_week.week_total=self.week_total - duplicate_week.total=self.total - duplicate_week.refresh - return duplicate_week - end - - # Recalculates the attributes that define a <tt>Week</tt> object. - # This was made public for <tt>#duplicate</tt> to work - # - def refresh - set_attributes - end - - # Changes the date range. - # This method calls <tt>#refresh</tt> to update the attributes. - # - # @param [DateTime] start is the new starting date for the <tt>Week</tt> - # @param [DateTime] finish is the new finish date for the <tt>Week</tt> - # - def adjust(start_date,finish_date) - self.start=DateTime.new(start_date.year,start_date.month,start_date.day) - self.finish=DateTime.new(finish_date.year,finish_date.month,finish_date.day) - refresh - end - - # Sets a range of minutes in a week to be working or resting. The parameters supplied - # to this method determine exactly what should be changed - # - # @param [Hash(DAYNAMES)] days identifies the days to be included in the range - # @param [DateTime] from_time where the time portion is used to specify the first minute to be set - # @param [DateTime] to_time where the time portion is used to specify the last minute to be set - # @param [Integer] type where a 1 sets it to working and a 0 to resting - # - def workpattern(days,from_time,to_time,type) - DAYNAMES[days].each {|day| self.values[day].workpattern(from_time,to_time,type)} - refresh - end - - # Calculates a new date by adding or subtracting a duration in minutes. - # - # @param [DateTime] start original date - # @param [Integer] duration minutes to add or subtract - # @param [Boolean] midnight flag used for subtraction that indicates the start date is midnight - # - 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 - end - - # Comparison Returns an integer (-1, 0, or +1) if week is less than, equal to, or greater than other_week - # - # @param [Week] other_week object to compare to - # @return [Integer] -1,0 or +1 if week is less than, equal to or greater than other_week + def <=>(other_week) if self.start < other_week.start return -1 elsif self.start == other_week.start return 0 else return 1 end end - - # Returns true if the supplied DateTime is working and false if resting - # - # @param [DateTime] start DateTime to be tested - # @return [Boolean] true if the minute is working otherwise false if it is a resting minute - # - def working?(start_date) - self.values[start_date.wday].working?(start_date) - end - - # Returns the difference in minutes between two DateTime values. - # - # @param [DateTime] start starting DateTime - # @param [DateTime] finish ending DateTime - # @return [Integer, DateTime] number of minutes and start date for rest of calculation. - # - def diff(start_date,finish_date) - start_date,finish_date=finish_date,start_date if ((start_date <=> finish_date))==1 - # calculate to end of day - # - if (start_date.jd==finish_date.jd) # same day - duration, start_date=self.values[start_date.wday].diff(start_date,finish_date) - elsif (finish_date.jd<=self.finish.jd) #within this week - duration, start_date=diff_detail(start_date,finish_date,finish_date) - else # after this week - duration, start_date=diff_detail(start_date,finish_date,self.finish) - end - return duration, start_date - end - + def week_total - span_in_days > 6 ? self.values.inject(0) {|sum,x| sum + x.total } : day_indexes.inject(0) {|sum,x| sum + self.values[x].total} + span_in_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 = total_hours(self.start.wday,6) + sum = sum_of_minutes_in_day_range(self.start.wday, 6) total_days -= (7-self.start.wday) - sum += total_hours(0,self.finish.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 end - private + 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) + end + end - def day_indexes - self.start.wday > self.finish.wday ? self.start.wday.upto(6).to_a.concat(0.upto(self.finish.wday).to_a) : self.start.wday.upto(self.finish.wday).to_a + 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 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 + end + + def working?(date) + return true if bit_pos_time(date) & @values[date.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 + + end + + private + def span_in_days (self.finish-self.start).to_i + 1 end - # Recalculates all the attributes for a Week object - # - def set_attributes - + def full_week_total_minutes + sum_of_minutes_in_day_range 0, 6 end - # Calculates the total number of minutes between two dates - # - # @param [DateTime] start is the first date in the range - # @param [DateTime] finish is the last date in the range - # @return [Integer] total number of minutes between supplied dates - # - def total_hours(start,finish) - total=0 - start.upto(finish) {|day| - total+=self.values[day].total - } + def part_week_total_minutes + + if self.start.wday <= self.finish.wday + total = sum_of_minutes_in_day_range(self.start.wday, self.finish.wday) + else + total = sum_of_minutes_in_day_range(self.start.wday, 6) + total += sum_of_minutes_in_day_range(0, self.finish.wday) + end return total end - - # Adds a duration in minutes to a date. - # - # The Boolean returned is always false. - # - # @param [DateTime] start original date - # @param [Integer] duration minutes to add - # @return [DateTime, Integer, Boolean] the calculated date, remaining minutes and flag used for subtraction - # - def add(start,duration) - # aim to calculate to the end of the day - start,duration = self.values[start.wday].calc(start,duration) - return start,duration,false if (duration==0) || (start.jd > self.finish.jd) - # aim to calculate to the end of the next week day that is the same as @finish - while((duration!=0) && (start.wday!=self.finish.next_day.wday) && (start.jd <= self.finish.jd)) - if (duration>self.values[start.wday].total) - duration = duration - self.values[start.wday].total - start=start.next_day - elsif (duration==self.values[start.wday].total) - start=after_last_work(start) - duration = 0 - else - start,duration = self.values[start.wday].calc(start,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')} + 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) + mask_of_1s = time_mask(from_time, to_time) + 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) + 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) + end + + def add(initial_date,duration) + + 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 - return start,duration,false if (duration==0) || (start.jd > self.finish.jd) - - # while duration accomodates full weeks - while ((duration!=0) && (duration>=self.week_total) && ((start.jd+6) <= self.finish.jd)) - duration=duration - self.week_total - start=start+7 + 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 + end - return start,duration,false if (duration==0) || (start.jd > self.finish.jd) + def minutes_to_end_of_day(date) + pattern_to_end_of_day(date).to_s(2).count('1') + end - # while duration accomodates full days - while ((duration!=0) && (start.jd<= self.finish.jd)) - if (duration>self.values[start.wday].total) - duration = duration - self.values[start.wday].total - start=start.next_day - else - start,duration = self.values[start.wday].calc(start,duration) - end - end - return start, duration, false - + def pattern_to_end_of_day(date) + mask = mask_to_end_of_day(date) + (self.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) + end + + def working_day + 2**(60*self.hours_per_day)-1 + end + + def start_of_next_day(date) + date.next_day - (HOUR * date.hour) - (MINUTE * date.minute) + end + + def start_of_previous_day(date) + start_of_next_day(date).prev_day.prev_day + end + + def start_of_today(date) + start_of_next_day(date.prev_day) + end + + def end_of_this_day(date) + position = pattern_to_end_of_day(date).to_s(2).size + return adjust_date(date,position) + end + + 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) + end - # Subtracts a duration in minutes from a date - # - # @param [DateTime] start original date - # @param [Integer] duration minutes to subtract - always a negative - # @param [Boolean] midnight flag indicates the start date is midnight when true - # @return [DateTime, Integer, Boolean] the calculated date, remaining number of minutes and - # true if the time is midnight on the date - # - def subtract(start,duration,midnight=false) - - # Handle subtraction from start of day - if midnight - start,duration=minute_b4_midnight(start,duration) - midnight=false - end + def pattern_to_start_of_day(date) + mask = mask_to_start_of_day(date) + (self.values[date.wday] & mask) + end - # aim to calculate to the start of the day - start,duration, midnight = self.values[start.wday].calc(start,duration) + def minutes_to_start_of_day(date) + pattern_to_start_of_day(date).to_s(2).count('1') + end - if midnight && (start.jd >= self.start.jd) - start,duration=minute_b4_midnight(start,duration) - return subtract(start,duration, false) - elsif midnight - return start,duration,midnight - elsif (duration==0) || (start.jd ==self.start.jd) - return start,duration, midnight - end + def consume_minutes(date,duration) - # aim to calculate to the start of the previous week day that is the same as @start - while((duration!=0) && (start.wday!=self.start.wday) && (start.jd >= self.start.jd)) + 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 - if (duration.abs>=self.values[start.wday].total) - duration = duration + self.values[start.wday].total - start=start.prev_day + top=minutes.size + bottom=1 + mark = top / 2 + + while minutes[0,mark].count('1') != duration.abs + last_mark = mark + if minutes[0,mark].count('1') < duration.abs + + bottom = mark + mark = (top-mark) / 2 + mark + mark = top if last_mark == mark + else - start,duration=minute_b4_midnight(start,duration) - start,duration = self.values[start.wday].calc(start,duration) - end + + top = mark + mark = (mark-bottom) / 2 + bottom + mark = bottom if last_mark = mark + + end end - return start,duration if (duration==0) || (start.jd ==self.start.jd) + mark = minutes_addition_adjustment(minutes, mark) if duration > 0 + mark = minutes_subtraction_adjustment(minutes,mark) if duration < 0 - #while duration accomodates full weeks - while ((duration!=0) && (duration.abs>=self.week_total) && ((start.jd-6) >= self.start.jd)) - duration=duration + self.week_total - start=start-7 + 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 + + minutes.size - (i + 1) + end + + def minutes_addition_adjustment(minutes,mark) + minutes=minutes[0,mark] + + while minutes[minutes.size-1]=='0' + minutes.chop! end - return start,duration if (duration==0) || (start.jd ==self.start.jd) + minutes.size + end - #while duration accomodates full days - while ((duration!=0) && (start.jd>= self.start.jd)) - if (duration.abs>=self.values[start.wday].total) - duration = duration + self.values[start.wday].total - start=start.prev_day + def subtract_to_start_of_day(initial_date, duration, 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 - start,duration=minute_b4_midnight(start,duration) - start,duration = self.values[start.wday].calc(start,duration) + initial_date = consume_minutes(initial_date,duration) + duration = 0 + midnight = false end - end - - return start, duration , midnight - + end + return initial_date, duration, midnight end - - # Supports calculating from midnight by updating the given duration depending on whether the - # last minute in the day is resting or working. It then sets the time to this minute. - # - # @param [DateTime] start is the date whose midnight is to be used as the start date - # @param [Integer] duration is the number of minutes to subtract - # @return [DateTime, Integer] the date with a time of 23:59 and remaining duration - # adjusted according to whether 23:59 is resting or not - # - def minute_b4_midnight(start,duration) - start -= start.hour * HOUR - start -= start.min * MINUTE - duration += self.values[start.wday].minutes(23,59,23,59) - start = start.next_day - MINUTE - return start,duration - end - - # Calculates the date and time after the last working minute of the current date - # - # @param [DateTime] start is the current date - # @return [DateTime] the new date - # - def after_last_work(start_date) - if self.values[start_date.wday].last_hour.nil? - return start_date.next_day - else - start_date = start_date + HOUR * (self.values[start_date.wday].last_hour - start_date.hour) - start_date = start_date + MINUTE * (self.values[start_date.wday].last_min - start_date.min + 1) - return start_date - end - end - - # Calculates the difference between two dates that exist in this Week object. - # - # @param [DateTime] start first date - # @param [DateTime] finish last date - # @param [DateTime] finish_on the range to be used in this Week object. - # @return [DateTime, Integer] new date for rest of calculation and total number of minutes calculated thus far. - # - def diff_detail(start_date,finish_date,finish_on_date) + + + def handle_midnight(initial_date,duration) + if working?(start_of_next_day(initial_date) - MINUTE) + duration += 1 + end - duration, start_date=diff_in_day(start_date, finish_date) - return duration,start_date if start_date > finish_on_date - - #rest of week to finish day - while (start_date.wday<finish_date.wday) do - duration+=day_total(start_date) - start_date=start_date.next_day + initial_date -= (HOUR * initial_date.hour) + initial_date -= (MINUTE * initial_date.min) + initial_date = initial_date.next_day - MINUTE + + return 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 = 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) end - #weeks - while (start_date.jd+7<finish_on_date.jd) do - duration+=self.week_total - start_date+=7 + while (duration != 0) && (duration >= self.week_total) && ((initial_date.jd - 6) >= self.start.jd) + duration += self.week_total + initial_date -= 7 end - #days - while (start_date.jd < finish_on_date.jd) do - duration+=day_total(start_date) - start_date=start_date.next_day + while (duration != 0) && (initial_date.jd >= self.start.jd) + initial_date, duration, midnight = subtract_to_start_of_day(initial_date,duration, midnight) end - #day - day_duration, start_date=diff_in_day(start_date, finish_date) - duration+=day_duration - return duration, start_date + return 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) + duration += minutes_to_end_of_day(start_date) + start_date = start_of_next_day(start_date) + end + + while true + break if ((start_date + 7) > finish_date) + break if ((start_date + 6).jd > self.finish.jd) + duration += week_total + start_date += 7 + end + + while true + break if (start_date.jd >= self.finish.jd) + break if (start_date.jd >= finish_date.jd) + 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) + duration += interim_duration unless interim_duration.nil? + return duration, start_date + end - def diff_in_day(start_date,finish_date) - return self.values[start_date.wday].diff(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 end - def day_total(start_date) - return self.values[start_date.wday].total + 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) + 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 end end end