lib/taskjuggler/ResourceScenario.rb in taskjuggler-0.1.1 vs lib/taskjuggler/ResourceScenario.rb in taskjuggler-0.2.0

- old
+ new

@@ -44,17 +44,26 @@ # The index of the last booked time Slot. @lastBookedSlot = nil # Same but for each assigned resource. @lastBookedSlots = {} + # Attributed are only really created when they are accessed the first + # time. So make sure some needed attributes really exist so we don't + # have to check for existance each time we access them. + %w( alloctdeffort criticalness directreports duties efficiency + effort limits managers rate reports shifts + vacations workinghours ).each do |attr| + @property[attr, @scenarioIdx] + end + @dCache = DataCache.instance end # This method must be called at the beginning of each scheduling run. It # initializes variables used during the scheduling process. def prepareScheduling - @property['effort', @scenarioIdx] = 0 + @effort = 0 initScoreboard if @property.leaf? setDirectReports end @@ -64,45 +73,45 @@ # statistically some tasks will not get their resources. A value between # 0 and 1 implies no guarantee, though. def calcCriticalness if @scoreboard.nil? # Resources that are not allocated are not critical at all. - @property['criticalness', @scenarioIdx] = 0.0 + @criticalness = 0.0 else freeSlots = 0 @scoreboard.each do |slot| freeSlots += 1 if slot.nil? end - @property['criticalness', @scenarioIdx] = freeSlots == 0 ? 1.0 : - a('alloctdeffort') / freeSlots + @criticalness = freeSlots == 0 ? 1.0 : + @alloctdeffort / freeSlots end end def setDirectReports # Only leaf resources have reporting lines. return unless @property.leaf? # The 'directreports' attribute is the reverse link for the 'managers' # attribute. In contrast to the 'managers' attribute, the # 'directreports' list has no duplicate entries. - a('managers').each do |manager| + @managers.each do |manager| unless manager['directreports', @scenarioIdx].include?(@property) manager['directreports', @scenarioIdx] << @property end end end def setReports - return unless a('directreports').empty? + return unless @directreports.empty? - a('managers').each do |r| + @managers.each do |r| r.setReports_i(@scenarioIdx, [ @property ]) end end def preScheduleCheck - a('managers').each do |manager| + @managers.each do |manager| unless manager.leaf? error('manager_is_group', "Resource #{@property.fullId} has group #{manager.fullId} " + "assigned as manager. Managers must be leaf resources.") end @@ -122,11 +131,11 @@ resource.finishScheduling(@scenarioIdx) end if (parent = @property.parent) # Add the assigned task to the parent duties list. - a('duties').each do |task| + @duties.each do |task| unless parent['duties', @scenarioIdx].include?(task) parent['duties', @scenarioIdx] << task end end end @@ -135,12 +144,12 @@ # Returns true if the resource is available at the time specified by # _sbIdx_. def available?(sbIdx) return false unless @scoreboard[sbIdx].nil? - limits = a('limits') - return false if limits && !limits.ok?(@scoreboard.idxToDate(sbIdx)) + limits = @limits + return false if limits && !limits.ok?(sbIdx) true end # Return true if the resource is booked for a tasks at the time specified by @@ -154,12 +163,12 @@ # method returns true if the slot was available. def book(sbIdx, task, force = false) return false if !force && !available?(sbIdx) # Make sure the task is in the list of duties. - unless a('duties').include?(task) - @property['duties', @scenarioIdx] << task + unless @duties.include?(task) + @duties << task end #puts "Booking resource #{@property.fullId} at " + # "#{@scoreboard.idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n" @scoreboard[sbIdx] = task @@ -168,11 +177,11 @@ t = @property while t t['effort', @scenarioIdx] += 1 t = t.parent end - a('limits').inc(@scoreboard.idxToDate(sbIdx)) if a('limits') + @limits.inc(sbIdx) if @limits # Scoreboard iterations are fairly expensive but they are very frequent # operations in later processing. To limit the interations to the # relevant intervals, we store the interval for all bookings and for # each individual task. @@ -240,11 +249,11 @@ query.sortable = query.numerical = cost = turnover(query.startIdx, query.endIdx, query.costAccount, query.scopeProperty) query.string = query.currencyFormat.format(cost) else - query.string = 'No cost account' + query.string = 'No \'balance\' defined!' end end # The effort allocated to the Resource in the specified interval. In case a # Task is given as scope property only the effort allocated to this Task is @@ -283,16 +292,16 @@ query.sortable = query.numerical = revenue = turnover(query.startIdx, query.endIdx, query.revenueAccount, query.scopeProperty) query.string = query.currencyFormat.format(revenue) else - query.string = 'No revenue account' + query.string = 'No \'balance\' defined!' end end # The work time of the Resource that was blocked by a vacation during the - # specified Interval. The result is in working days (effort). + # specified TimeInterval. The result is in working days (effort). def query_vacationdays(query) query.sortable = query.numerical = time = getVacationDays(query.startIdx, query.endIdx) query.string = query.scaleLoad(time) end @@ -300,11 +309,11 @@ # Returns the work of the resource (and its children) weighted by their # efficiency. def getEffectiveWork(startIdx, endIdx, task = nil) # There can't be any effective work if the start is after the end or the # todo list doesn't contain the specified task. - return 0.0 if startIdx >= endIdx || (task && !a('duties').include?(task)) + return 0.0 if startIdx >= endIdx || (task && !@duties.include?(task)) # The unique key we use to address the result in the cache. key = [ self, :ResourceScenarioEffectiveWork, startIdx, endIdx, task ].hash work = @dCache.load(key) @@ -322,11 +331,11 @@ else return 0.0 if @scoreboard.nil? work = @project.convertToDailyLoad( getAllocatedSlots(startIdx, endIdx, task) * - @project['scheduleGranularity']) * a('efficiency') + @project['scheduleGranularity']) * @efficiency end @dCache.store(work, key) end # Returns the allocated accumulated time of this resource and its children. @@ -382,11 +391,11 @@ work += resource.getEffectiveFreeWork(@scenarioIdx, startIdx, endIdx) end else work = @project.convertToDailyLoad( getFreeSlots(startIdx, endIdx) * - @project['scheduleGranularity']) * a('efficiency') + @project['scheduleGranularity']) * @efficiency end work end # Return the number of working days that are blocked by vacations. @@ -404,11 +413,11 @@ else initScoreboard if @scoreboard.nil? vacationDays = @project.convertToDailyLoad( getVacationSlots(startIdx, endIdx) * - @project['scheduleGranularity']) * a('efficiency') + @project['scheduleGranularity']) * @efficiency end vacationDays end def turnover(startIdx, endIdx, account, task = nil) @@ -416,31 +425,31 @@ if @property.container? @property.kids.each do |child| amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, task) end else - a('duties').each do |duty| + @duties.each do |duty| amount += duty.turnover(@scenarioIdx, startIdx, endIdx, account, @property) end end amount end - # Returns the cost for using this resource during the specified Interval - # _period_. If a Task _task_ is provided, only the work on this particular - # task is considered. + # Returns the cost for using this resource during the specified + # TimeInterval _period_. If a Task _task_ is provided, only the work on + # this particular task is considered. def cost(startIdx, endIdx, task = nil) - getAllocatedTime(startIdx, endIdx, task) * a('rate') + getAllocatedTime(startIdx, endIdx, task) * @rate end # Returns true if the resource or any of its children is allocated during - # the period specified with the Interval _iv_. If task is not nil + # the period specified with the TimeInterval _iv_. If task is not nil # only allocations to this tasks are respected. def allocated?(iv, task = nil) - return false if task && !a('duties').include?(task) + return false if task && !@duties.include?(task) startIdx = @project.dateToIdx(iv.start) endIdx = @project.dateToIdx(iv.end) startIdx, endIdx = fitIndicies(startIdx, endIdx, task) @@ -448,52 +457,62 @@ return allocatedSub(startIdx, endIdx, task) end # Iterate over the scoreboard and turn its content into a set of Bookings. - # _iv_ can be an Interval to limit the bookings within the provided - # period. - def getBookings(iv = nil) - return {} if @property.container? || @scoreboard.nil? || - @firstBookedSlot.nil? || @lastBookedSlot.nil? + # _iv_ can be a TimeInterval to limit the bookings within the provided + # period. if _hashByTask_ is true, the result is a Hash of Arrays with + # bookings hashed by Task. Otherwise it's just a plain Array with + # Bookings. + def getBookings(iv = nil, hashByTask = true) + bookings = hashByTask ? {} : [] + return bookings if @property.container? || @scoreboard.nil? || + @firstBookedSlot.nil? || @lastBookedSlot.nil? - bookings = {} - lastTask = nil - bookingStart = nil - # To speedup the collection we start with the first booked slot and end # with the last booked slot. startIdx = @firstBookedSlot endIdx = @lastBookedSlot + 1 - # In case the index markers are still uninitialized, we have no bookings. - return {} if startIdx.nil? || endIdx.nil? - - # If the user provided an Interval, we only return bookings within this - # Interval. + # If the user provided a TimeInterval, we only return bookings within + # this TimeInterval. if iv ivStartIdx = @project.dateToIdx(iv.start) ivEndIdx = @project.dateToIdx(iv.end) startIdx = ivStartIdx if ivStartIdx > startIdx endIdx = ivEndIdx if ivEndIdx < endIdx end + lastTask = nil + bookingStart = nil + startIdx.upto(endIdx) do |idx| task = @scoreboard[idx] # Now we watch for task changes. - if task != lastTask || (lastTask == nil && task.is_a?(Task)) || - (task.is_a?(Task) && idx == endIdx) - unless lastTask.nil? + if task != lastTask || + (task.is_a?(Task) && (lastTask.nil? || idx == endIdx)) + if lastTask + # We've found the end of a task booking series. # If we don't have a Booking for the task yet, we create one. - if bookings[lastTask].nil? - bookings[lastTask] = Booking.new(@property, lastTask, []) + if hashByTask + if bookings[lastTask].nil? + bookings[lastTask] = Booking.new(@property, lastTask, []) + end + # Append the new interval to the Booking. + bookings[lastTask].intervals << + TimeInterval.new(@scoreboard.idxToDate(bookingStart), + @scoreboard.idxToDate(idx)) + else + if bookings.empty? || bookings.last.task != lastTask + bookings << Booking.new(@property, lastTask, []) + end + # Append the new interval to the Booking. + bookings.last.intervals << + TimeInterval.new(@scoreboard.idxToDate(bookingStart), + @scoreboard.idxToDate(idx)) end - # Append the new interval to the Booking. - bookings[lastTask].intervals << - Interval.new(@scoreboard.idxToDate(bookingStart), - @scoreboard.idxToDate(idx)) end # Get ready for the next task booking interval if task.is_a?(Task) lastTask = task bookingStart = idx @@ -576,23 +595,20 @@ # Create scoreboard and mark all slots as unavailable @scoreboard = Scoreboard.new(@project['start'], @project['end'], @project['scheduleGranularity'], 2) # We'll need this frequently and can savely cache it here. - @shifts = a('shifts') - @workinghours = a('workinghours') + @shifts = @shifts + @workinghours = @workinghours # Change all work time slots to nil (available) again. - date = @scoreboard.idxToDate(0) - delta = @project['scheduleGranularity'] @project.scoreboardSize.times do |i| - @scoreboard[i] = nil if onShift?(date) - date += delta + @scoreboard[i] = nil if onShift?(i) end # Mark all resource specific vacation slots as such - a('vacations').each do |vacation| + @vacations.each do |vacation| startIdx = @scoreboard.dateToIdx(vacation.start) endIdx = @scoreboard.dateToIdx(vacation.end) startIdx.upto(endIdx - 1) do |i| # If the slot is nil, we don't set the time-off bit. @scoreboard[i] = (@scoreboard[i].nil? ? 0 : 2) | (1 << 2) @@ -611,11 +627,11 @@ end unless @shifts.nil? # Mark the vacations from all the shifts the resource is assigned to. @project.scoreboardSize.times do |i| - v = @shifts.getSbSlot(@scoreboard.idxToDate(i)) + v = @shifts.getSbSlot(i) # Check if the vacation replacement bit is set. In that case we copy # the whole interval over to the resource scoreboard overriding any # global vacations. if (v & (1 << 8)) != 0 @scoreboard[i] = (v & 0x3E == 0) ? nil : (v & 0x3D) @@ -652,25 +668,25 @@ # A manager must never show up in the list of his/her own reports. error('manager_loop', "Management loop detected. #{@property.fullId} has self " + "in list of reports") end - @property['reports', @scenarioIdx] += reports + @reports += reports # Resources can end up multiple times in the list if they have multiple # reporting chains. We only need them once in the list. - a('reports').uniq! + @reports.uniq! - a('managers').each do |r| - r.setReports_i(@scenarioIdx, a('reports')) + @managers.each do |r| + r.setReports_i(@scenarioIdx, @reports) end end - def onShift?(date) - if @shifts && @shifts.assigned?(date) - return @shifts.onShift?(date) + def onShift?(sbIdx) + if @shifts && @shifts.assigned?(sbIdx) + return @shifts.onShift?(sbIdx) else - @workinghours.onShift?(date) + @workinghours.onShift?(sbIdx) end end # Returns true if the resource or any of its children is allocated during # the period specified with _startIdx_ and _endIdx_. If task is not nil @@ -680,11 +696,11 @@ @property.kids.each do |resource| return true if resource.allocatedSub(@scenarioIdx, startIdx, endIdx, task) end else - return false unless @scoreboard && a('duties').include?(task) + return false unless @scoreboard && @duties.include?(task) startIdx, endIdx = fitIndicies(startIdx, endIdx, task) return false if startIdx >= endIdx startIdx.upto(endIdx - 1) do |idx| @@ -701,10 +717,10 @@ @property.kids.each do |resource| dailyRate += resource.rate(@scenarioIdx) end dailyRate else - a('rate') + @rate end end end