module Workpattern require 'set' # Represents the working and resting periods across a given number of whole years. Each Workpattern # has a unique name so it can be easily identified amongst all the other Workpattern objects. # # This and the Clock class are the only two that should be referenced by calling applications when # using this gem. # # @since 0.2.0 # class Workpattern # Holds collection of Workpattern objects @@workpatterns = Hash.new() # @!attribute [r] name # Name given to the Workpattern # @!attribute [r] base # Starting year # @!attribute [r] span # Number of years # @!attribute [r] from # First date in Workpattern # @!attribute [r] to # Last date in Workpattern # @!attribute [r] weeks # The Week objects that make up this workpattern # attr_reader :name, :base, :span, :from, :to, :weeks # Class for handling persistence in user's own way # def self.persistence_class=(klass) @@persistence = klass end def self.persistence? @@persistence ||= nil end # The new Workpattern object is created with all working minutes. # # @param [String] name Every workpattern has a unique name # @param [Integer] base Workpattern starts on the 1st January of this year. # @param [Integer] span Workpattern spans this number of years ending on 31st December. # @raise [NameError] if the given name already exists # def initialize(name=DEFAULT_NAME,base=DEFAULT_BASE_YEAR,span=DEFAULT_SPAN) raise(NameError, "Workpattern '#{name}' already exists and can't be created again") if @@workpatterns.key?(name) if span < 0 offset = span.abs - 1 else offset = 0 end @name = name @base = base @span = span @from = DateTime.new(base.abs - offset) @to = DateTime.new(@from.year + span.abs - 1,12,31,23,59) @weeks = SortedSet.new @weeks << Week.new(@from,@to,1) @@workpatterns[name]=self end # Deletes all Workpattern objects # def self.clear @@workpatterns.clear end # Returns an Array containing all the Workpattern objects # @return [Array] all Workpattern objects # def self.to_a @@workpatterns.to_a end # Returns the specific named Workpattern # @param [String] name of the required Workpattern # @raise [NameError] if a Workpattern of the supplied name does not exist # def self.get(name) return @@workpatterns[name] if @@workpatterns.key?(name) raise(NameError, "Workpattern '#{name}' doesn't exist so can't be retrieved") end # Deletes the specific named Workpattern # @param [String] name of the required Workpattern # @return [Boolean] true if the named Workpattern existed or false if it doesn't # def self.delete(name) if @@workpatterns.delete(name).nil? return false else return true end end # Applys a working or resting pattern to the Workpattern object. # # The #resting and #working methods are convenience methods that call # this with the appropriate :work_type already set. # # @param [Hash] opts the options used to apply a workpattern # @option opts [Date] :start The first date to apply the pattern. Defaults # to the start attribute. # @option opts [Date] :finish The last date to apply the pattern. Defaults # to the finish attribute. # @option opts [DAYNAMES] :days The specific day or days the pattern will apply to. # It defaults to :all # @option opts [(#hour, #min)] :start_time The first time in the selected days to apply the pattern. # Defaults to 00:00. # @option opts [(#hour, #min)] :finish_time The last time in the selected days to apply the pattern. # Defaults to 23:59. # @option opts [(WORK || REST)] :work_type Either working or resting. Defaults to working. # @see #working # @see #resting # def workpattern(opts={}) args={:start => @from, :finish => @to, :days => :all, :from_time => FIRST_TIME_IN_DAY, :to_time => LAST_TIME_IN_DAY, :work_type => WORK} args.merge! opts @@persistence.store( name: @name, workpattern: args) if self.class.persistence? args[:start] = dmy_date(args[:start]) args[:finish] = dmy_date(args[:finish]) from_time = hhmn_date(args[:from_time]) to_time = hhmn_date(args[:to_time]) upd_start=args[:start] upd_finish=args[:finish] while (upd_start <= upd_finish) current_wp=find_weekpattern(upd_start) if (current_wp.start == upd_start) if (current_wp.finish > upd_finish) clone_wp=clone_and_adjust_current_wp(current_wp, upd_finish+1,current_wp.finish,upd_start,upd_finish) clone_wp.workpattern(args[:days],from_time,to_time,args[:work_type]) @weeks<< clone_wp upd_start=upd_finish+1 else # (current_wp.finish == upd_finish) current_wp.workpattern(args[:days],from_time,to_time,args[:work_type]) upd_start=current_wp.finish + 1 end else clone_wp=clone_and_adjust_current_wp(current_wp, current_wp.start,upd_start-1,upd_start) if (clone_wp.finish <= upd_finish) clone_wp.workpattern(args[:days],from_time,to_time,args[:work_type]) @weeks<< clone_wp upd_start=clone_wp.finish+1 else after_wp=clone_and_adjust_current_wp(clone_wp, upd_start,upd_finish,upd_finish+1) @weeks<< after_wp clone_wp.workpattern(args[:days],from_time,to_time,args[:work_type]) @weeks<< clone_wp upd_start=clone_wp.finish+1 end end end end # Convenience method that calls #workpattern with the :work_type specified as resting. # # @see #workpattern # def resting(args={}) args[:work_type]=REST workpattern(args) end # Convenience method that calls #workpattern with the :work_type specified as working. # # @see #workpattern # def working(args={}) args[:work_type]=WORK workpattern(args) end # Calculates the resulting date when the duration in minutes is added to the start date. # The duration is always in whole minutes and subtracts from start when it is a # negative number. # # @param [DateTime] start date to add or subtract minutes # @param [Integer] duration in minutes to add or subtract to date # @return [DateTime] the date when duration is added to start # def calc(start,duration) return start if duration==0 midnight=false while (duration !=0) week=find_weekpattern(start) if (week.start==start) && (duration<0) && (!midnight) start=start.prev_day week=find_weekpattern(start) midnight=true end start,duration,midnight=week.calc(start,duration,midnight) end return start end # Returns true if the given minute is working and false if it is resting. # # @param [DateTime] start DateTime being tested # @return [Boolean] true if working and false if resting # def working?(start) return find_weekpattern(start).working?(start) end # Returns number of minutes between two dates # # @param [DateTime] start is the date to start from # @param [DateTime] finish is the date to end with # @return [Integer] number of minutes between the two dates # def diff(start,finish) start,finish=finish,start if finishWeek pattern for the supplied date. # # If the supplied date is outside the span of the Workpattern object # then it returns an all working Week object for the calculation. # # @param [DateTime] date whose containing Week pattern is required # @return [Week] Week object that includes the supplied date in it's range # def find_weekpattern(date) # find the pattern that fits the date # if date<@from result = Week.new(DateTime.jd(0),@from-MINUTE,1) elsif date>@to result = Week.new(@to+MINUTE,DateTime.new(9999),1) else date = DateTime.new(date.year,date.month,date.day) result=@weeks.find {|week| week.start <= date and week.finish >= date} end return result end # Strips off hours, minutes, seconds and so forth from a supplied Date or # DateTime # # @param [DateTime] date # @return [DateTime] with zero hours, minutes, seconds and so forth. # def dmy_date(date) return DateTime.new(date.year,date.month,date.day) end # Extract the time into a Clock object # # @param [DateTime] date # @return [Clock] def hhmn_date(date) return Clock.new(date.hour,date.min) end private # Handles cloning of Week Pattern including date adjustments # def clone_and_adjust_current_wp(current_wp, current_start,current_finish,clone_start,clone_finish=nil) clone_wp=current_wp.duplicate current_wp.adjust(current_start,current_finish) if (clone_finish.nil?) clone_wp.adjust(clone_start,clone_wp.finish) else clone_wp.adjust(clone_start,clone_finish) end return clone_wp end end end