lib/WorkingHours.rb in taskjuggler-0.0.7 vs lib/WorkingHours.rb in taskjuggler-0.0.8
- old
+ new
@@ -9,159 +9,74 @@
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
require 'Interval'
+require 'Scoreboard'
class TaskJuggler
- # This cache class is used to speedup accesses to the frequently used
- # WorkingHours::onShift? function. It saves the result the first time the
- # function is called for a particular date and working hour set and returns
- # it on subsequent calls again. Each partucular set of working hours needs
- # its separate cache. The OnShiftCache object is shared amongst all
- # WorkingHours objects so that WorkingHours objects with identical working
- # hours can share the cache.
- class OnShiftCache
-
- # Create the OnShiftCache object. There should be only one for the
- # application.
- def initialize
- @caches = []
- @workingHoursTable = []
- # The cache is an array with entries for each date. To minimize the
- # necessary storage space, we need to guess the smallest used date
- # (which gets index 0 then) and the smallest distance between dates.
- @minDate = nil
- # We assume a timing resolution of 1 hour (the TaskJuggler default)
- # first.
- @minDateDelta = 60 * 60
- end
-
- # Register the WorkingHours object with the caches. The function will
- # return the actual cache used for this particular set of working hours.
- # The WorkingHours object may not change its working hours after this
- # call. The returned cache reference is used as a handle for subsequent
- # OnShiftCache::set and OnShiftCache::get calls.
- def register(wh)
- # Search the list of already registered caches for an identical set of
- # WorkingHours. In case one is found, return the reference to this
- # cache.
- @workingHoursTable.length.times do |i|
- if @workingHoursTable[i] == wh
- return @caches[i]
- end
- end
- # If this is a new set of WorkingHours we create a new cache for it.
- @workingHoursTable << WorkingHours.new(wh)
- @caches << []
- @caches.last
- end
-
- # Set the +value+ for a given +cache+ and +date+.
- def set(cache, date, value)
- cache[dateToIndex(date)] = value
- end
-
- # Get the value for a given +cache+ and +date+.
- def get(cache, date)
- cache[dateToIndex(date)]
- end
-
- private
-
- # When the @minDate or @minDateDelta values need to be changed, we have to
- # clear all the caches again.
- def resetCaches
- @caches.each { |c| c.clear }
- end
-
- # Convert a TjTime +date+ to an index in the cache Array. To optimize the
- # size of the cache, we have to guess the smallest used date and the
- # regular distance between the date values. If we have to correct these
- # guessed values, we have to clear the caches.
- def dateToIndex(date)
- if @minDate.nil? || date < @minDate
- @minDate = date
- resetCaches
- end
- startDate = date - @minDate
- div, mod = startDate.divmod(@minDateDelta)
- if mod != 0
- resetCaches
- # We have to guess the timingresolution of the project here. Possible
- # values are 5, 10, 15, 20, 30 or 60 minutes.
- case @minDateDelta / 60
- when 60
- @minDateDelta = 30
- when 30
- @minDateDelta = 20
- when 20
- @minDateDelta = 15
- when 15
- @minDateDelta = 10
- when 10
- @minDateDelta = 5
- else
- raise "Illegal timing resolution!"
- end
- @minDateDelta *= 60
- div = startDate / @minDateDelta
- end
-
- div
- end
-
- end
-
# Class to store the working hours for each day of the week. The working hours
# are stored as Arrays of Fixnum intervals for each day of the week. A day off
# is modelled as empty Array for that week day. The start end end times of
# each working period are stored as seconds after midnight.
class WorkingHours
- attr_reader :days
- attr_accessor :timezone
+ attr_reader :days, :startDate, :endDate, :slotDuration, :timezone,
+ :scoreboard
- # All WorkingHours objects share the same cache to speedup the onShift?
- # method.
- @@onShiftCache = OnShiftCache.new
-
# Create a new WorkingHours object. The method accepts a reference to an
# existing WorkingHours object in +wh+. When it's present, the new object
- # will be a deep copy of the given object.
- def initialize(wh = nil)
+ # will be a deep copy of the given object. The Scoreboard object is _not_
+ # deep copied. It will be copied on write.
+ def initialize(arg1 = nil, startDate = nil, endDate = nil)
# One entry for every day of the week. Sunday === 0.
@days = Array.new(7, [])
- @cache = nil
+ @scoreboard = nil
- if wh.nil?
- # Create a new object with default working hours.
- @timezone = nil
- # Set the default working hours. Monday to Friday 9am - 12pm, 1pm - 6pm.
- # Saturday and Sunday are days off.
- 1.upto(5) do |day|
- @days[day] = [ [ 9 * 60 * 60, 12 * 60 * 60 ],
- [ 13 * 60 * 60, 18 * 60 * 60 ] ]
- end
- else
- # Copy the values from the given object.
+ if arg1.is_a?(WorkingHours)
+ # Create a copy of the passed WorkingHours object.
+ wh = arg1
@timezone = wh.timezone
7.times do |day|
hours = []
wh.days[day].each do |hrs|
hours << hrs.dup
end
setWorkingHours(day, hours)
end
+ @startDate = wh.startDate
+ @endDate = wh.endDate
+ @slotDuration = wh.slotDuration
+ @scoreboard = wh.scoreboard
+ else
+ slotDuration = arg1
+ if arg1.nil? || startDate.nil? || endDate.nil?
+ raise "You must supply values for slotDuration, start and end dates"
+ end
+ @startDate = startDate
+ @endDate = endDate
+ @slotDuration = slotDuration
+
+ # Create a new object with default working hours.
+ @timezone = nil
+ # Set the default working hours. Monday to Friday 9am - 12pm, 1pm - 6pm.
+ # Saturday and Sunday are days off.
+ 1.upto(5) do |day|
+ @days[day] = [ [ 9 * 60 * 60, 12 * 60 * 60 ],
+ [ 13 * 60 * 60, 18 * 60 * 60 ] ]
+ end
end
end
# Return true of the given WorkingHours object +wh+ is identical to this
# object.
def ==(wh)
- return false if wh.nil? || @timezone != wh.timezone
+ return false if wh.nil? || @timezone != wh.timezone ||
+ @startDate != wh.startDate ||
+ @endDate != wh.endDate ||
+ @slotDuration != wh.slotDuration
7.times do |d|
return false if @days[d].length != wh.days[d].length
# Check all working hour intervals
@days[d].length.times do |i|
@@ -176,14 +91,12 @@
# Sunday, 1 for Monday and so on. +intervals+ must be an Array that
# contains an Array with 2 Fixnums for each working period. Each value
# specifies the time of day as minutes after midnight. The first value is
# the start time of the interval, the second the end time.
def setWorkingHours(dayOfWeek, intervals)
- if @cache
- raise 'You cannot change the working hours after onShift? has been ' +
- 'called.'
- end
+ # Changing the working hours requires the score board to be regenerated.
+ @scoreboard = nil
# Legal values range from 0 Sunday to 6 Saturday.
if dayOfWeek < 0 || dayOfWeek > 6
raise "dayOfWeek out of range: #{dayOfWeek}"
end
@@ -197,77 +110,44 @@
end
end
@days[dayOfWeek] = intervals
end
+ # Set the time zone _zone_ for the working hours. This will reset the
+ # @scoreboard.
+ def timezone=(zone)
+ @scoreboard = nil
+ @timezone = zone
+ end
+
# Return the working hour intervals for a given day of the week.
# +dayOfWeek+ must 0 for Sunday, 1 for Monday and so on. The result is an
# Array that contains Arrays of 2 Fixnums.
def getWorkingHours(dayOfWeek)
@days[dayOfWeek]
end
# Return true if _date_ is within the defined working hours.
def onShift?(date)
- @cache = @@onShiftCache.register(self) unless @cache
+ initScoreboard unless @scoreboard
- # If we have the result cached already, return it.
- unless (os = @@onShiftCache.get(@cache, date)).nil?
- return os
- end
+ @scoreboard.get(date)
+ end
- # The date is in UTC. The weekday needs to be calculated according to the
- # timezone of the project.
- projectDate = toLocaltime(date)
- dow = projectDate.wday
+ # Return true only if all slots in the _interval_ are offhour slots.
+ def timeOff?(interval)
+ initScoreboard unless @scoreboard
- # The working hours need to be put into the proper time zone.
- localDate = toLocaltime(date, @timezone)
- secondsOfDay = localDate.secondsOfDay
+ startIdx = @scoreboard.dateToIdx(interval.start, true)
+ endIdx = @scoreboard.dateToIdx(interval.end, true)
- @days[dow].each do |iv|
- # Check the working hours of that day if they overlap with +date+.
- if iv[0] <= secondsOfDay && secondsOfDay < iv[1]
- # Store the result in the cache.
- @@onShiftCache.set(@cache, date, true)
- return true
- end
+ startIdx.upto(endIdx - 1) do |i|
+ return false if @scoreboard[i]
end
-
- # Store the result in the cache.
- @@onShiftCache.set(@cache, date, false)
- false
- end
-
- # This function does not belong here! It should be handled via the
- # ShiftAssignment.
- def timeOff?(interval)
- t = interval.start.midnight
- while t < interval.end
- dow = t.wday
- unless @days[dow].empty?
- dayStart = t < interval.start ? interval.start.secondsOfDay :
- t.secondsOfDay
- dayEnd = t.sameTimeNextDay > interval.end ? interval.end.secondsOfDay :
- 60 * 60 * 24;
- @days[dow].each do |iv|
- return false if (dayStart <= iv[0] && iv[0] < dayEnd) ||
- (iv[0] <= dayStart && dayStart < iv[1])
- end
- end
- t = t.sameTimeNextDay
- end
true
end
- # Probably should be put into ShiftAssignment as well.
- def dayOff?(date)
- projectDate = toLocaltime(date)
- dow = projectDate.wday
- @days[dow].empty?
- end
-
# Returns the time interval settings for each day in a human readable form.
def to_s
dayNames = %w( Sun Mon Tue Wed Thu Fri Sat )
str = ''
7.times do |day|
@@ -293,29 +173,54 @@
str
end
private
- # Convert a UTC date into the corresponding date in the local time zone.
- # This is either the current system setting or the time zone specified by
- # _tz_.
- def toLocaltime(date, tz = nil)
+ def initScoreboard
+ # The scoreboard is an Array of True/False values. It spans a certain
+ # time period with one entry per time slot.
+ @scoreboard = Scoreboard.new(@startDate, @endDate, @slotDuration, false)
+
oldTimezone = nil
# Set environment variable TZ to appropriate time zone
if @timezone
- oldTimezone = ENV['tz']
- ENV['tz'] = @timezone
+ oldTimezone = ENV['TZ']
+ ENV['TZ'] = @timezone
end
- localDate = date.dup
- localDate.localtime
+ date = @startDate
+ @scoreboard.collect! do |slot|
+ localDate = date.dup
+ localDate.localtime
- # Restore environment
- if oldTimezone
- ENV['tz'] = oldTimezone
+ # The date is in UTC. The weekday needs to be calculated according to
+ # the local timezone.
+ weekday = localDate.wday
+ secondsOfDay = localDate.secondsOfDay
+
+ result = false
+ @days[weekday].each do |iv|
+ # Check the working hours of that day if they overlap with +date+.
+ if iv[0] <= secondsOfDay && secondsOfDay < iv[1]
+ # The time slot is a working slot.
+ result = true
+ break
+ end
+ end
+ # Calculate date of next scoreboard slot
+ date += @slotDuration
+
+ result
end
- localDate
+ # Restore environment
+ if @timezone
+ if oldTimezone
+ ENV['TZ'] = oldTimezone
+ else
+ ENV.delete('TZ')
+ end
+ end
end
end
end