Permalink
Browse files

Make shifts work for task assignments.

Various other bug fixes and speed improvements.


git-svn-id: https://www.taskjuggler.org/svn/ruby-tj@62 b116ed28-de24-0410-b202-8e75063b5d06
  • Loading branch information...
1 parent 8f5a4a6 commit 12d1fe4b534a893ab4a3e5807a0a0ba4655464ca cs committed Aug 26, 2007
View
30 benchmarks/allocate.tjp
@@ -0,0 +1,30 @@
+project test "Allocation Performance Test" "1.0" 2007-08-25 +3m
+
+macro r_and_t [
+ resource r${1} "Resource ${1}"
+ task t${1} "Task ${1}" {
+ period ${projectstart} - ${projectend}
+ allocate r${1}
+ }
+]
+
+macro r_and_t_10 [
+ ${r_and_t "${1}0"}
+ ${r_and_t "${1}1"}
+ ${r_and_t "${1}2"}
+ ${r_and_t "${1}3"}
+ ${r_and_t "${1}4"}
+ ${r_and_t "${1}5"}
+ ${r_and_t "${1}6"}
+ ${r_and_t "${1}7"}
+ ${r_and_t "${1}8"}
+ ${r_and_t "${1}9"}
+]
+
+${r_and_t_10 "0"}
+${r_and_t_10 "1"}
+${r_and_t_10 "2"}
+${r_and_t_10 "3"}
+${r_and_t_10 "4"}
+${r_and_t_10 "5"}
+
View
2 lib/AttributeBase.rb
@@ -22,7 +22,7 @@
# Attributes that are of an inheritable type will be copied from a parent
# property.
class AttributeBase
- attr_reader :property, :type, :provided, :inherited
+ attr_reader :property, :type, :provided, :inherited, :value
def initialize(type, property)
@type = type
View
12 lib/ExportReport.rb
@@ -57,12 +57,13 @@ def generateProjectProperty
@file << "project #{@project['id']} \"#{@project['name']}\" " +
"\"#{@project['version']}\" #{@project['start']} - " +
"#{@project['end']} {\n"
- generateCustomAttributeDeclarations('resource', @project.resources)
- generateCustomAttributeDeclarations('task', @project.tasks)
+ generateCustomAttributeDeclarations('resource', @project.resources,
+ @resourceAttrs)
+ generateCustomAttributeDeclarations('task', @project.tasks, @taskAttrs)
@file << "}\n\n"
end
- def generateCustomAttributeDeclarations(tag, propertySet)
+ def generateCustomAttributeDeclarations(tag, propertySet, attributes)
# First we search the attribute definitions for any user defined
# attributes and count them.
customAttributes = 0
@@ -76,7 +77,8 @@ def generateCustomAttributeDeclarations(tag, propertySet)
# taskAttributes list.
@file << ' extend ' + tag + "{\n"
propertySet.eachAttributeDefinition do |ad|
- next unless ad.userDefined && @taskAttrs.include?(ad.id)
+ next unless ad.userDefined &&
+ (attributes.include?(ad.id) || attributes.include?('all'))
@file << " #{ad.objClass.tjpId} #{ad.id} \"#{ad.name}\"\n"
end
@@ -119,7 +121,7 @@ def generateResource(resource, indent)
# resource list as well.
resource.children.each do |subresource|
if @resourceList.include?(subresource)
- generateresource(subresource, indent + 2)
+ generateResource(subresource, indent + 2)
end
end
View
6 lib/MacroParser.rb
@@ -51,21 +51,21 @@ def rule_macroCall
# macro call is replaced by an empty string.
unless @scanner.macroDefined?(@val[2])
if @val[1].nil?
- error('undef_macro', "Macro #{name} is undefined", sourceFileInfo)
+ error('undef_macro', "Macro #{@val[2]} is undefined")
end
return nil
end
@scanner.expandMacro([ @val[2] ] + @val[3])
})
- newPattern(%w( _( $ID _) ), Proc.new {
+ newPattern(%w( _( $ID _) ), lambda {
ENV[@val[0]]
})
end
def rule_relax
newRule('relax')
optional
- newPattern(%w( _? ), Proc.new {
+ newPattern(%w( _? ), lambda {
true
})
end
View
3 lib/Project.rb
@@ -86,6 +86,7 @@ def initialize(id, name, version, messageHandler)
attrs = [
# ID Name Type Inher. Scen. Default
[ 'index', 'No', FixnumAttribute, false, false, -1 ],
+ [ 'replace', 'Replace', BooleanAttribute, true, true, false ],
[ 'timezone', 'Time Zone', StringAttribute, true, true, nil ],
[ 'tree', 'Tree Index', StringAttribute, false, false, "" ],
[ 'vacations', 'Vacations', IntervalListAttribute, true, true, [] ],
@@ -149,6 +150,8 @@ def initialize(id, name, version, messageHandler)
[ 'priority', 'Priority', FixnumAttribute, true, true, 500 ],
[ 'responsible', 'Responsible', ResourceListAttribute, true, true, [] ],
[ 'scheduled', 'Scheduled', BooleanAttribute, true, true, false ],
+ [ 'shifts', 'Shifts', ShiftAssignmentsAttribute, true, true,
+ nil ],
[ 'start', 'Start', DateAttribute, true, true, nil ],
[ 'startpreds', 'Start Preds.', TaskListAttribute, false, true, [] ],
[ 'startsuccs', 'Start Succs.', TaskListAttribute, false, true, [] ],
View
4 lib/PropertyTreeNode.rb
@@ -42,7 +42,7 @@ def initialize(propertySet, id, name, parent)
end
def inheritAttributes
- # These attributes are inherited from the global context
+ # These attributes are being inherited from the global context.
whitelist = %w( priority projectid rate vacation workinghours )
# Inherit non-scenario-specific values
@@ -305,7 +305,7 @@ def to_s
def newAttribute(attributeType)
attribute = attributeType.objClass.new(attributeType, self)
# If the attribute requires a pointer to the project, we'll hand it over.
- if attribute.respond_to?('setProject')
+ if !attribute.value.nil? && attribute.respond_to?('setProject')
attribute.setProject(@project)
end
View
2 lib/Resource.rb
@@ -21,7 +21,7 @@ def initialize(project, id, name, parent)
@data = Array.new(@project.scenarioCount, nil)
0.upto(@project.scenarioCount) do |i|
- @data[i] = ResourceScenario.new(self, i)
+ @data[i] = ResourceScenario.new(self, i, @scenarioAttributes[i])
end
end
View
82 lib/ResourceScenario.rb
@@ -14,9 +14,16 @@
class ResourceScenario < ScenarioData
- def initialize(resource, scenarioIdx)
+ def initialize(resource, scenarioIdx, attributes)
super
+
+ # The scoreboard entries are either nil, a number or a task reference. nil
+ # means the slot is unassigned. The task reference means assigned to this
+ # task. The numbers have the following values:
+ # 1: Off hour
+ # 2: Vacation
@scoreboard = nil
+
@firstBookedSlot = nil
@lastBookedSlot = nil
end
@@ -45,14 +52,14 @@ def available?(sbIdx)
end
def booked?(sbIdx)
- !(@scoreboard[sbIdx].nil? || @scoreboard[sbIdx].class == Fixnum)
+ @scoreboard[sbIdx].is_a?(Task)
end
def book(sbIdx, task, force = false)
return false if !force && !available?(sbIdx)
#puts "Booking resource #{@property.fullId} at " +
- # "#{idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n"
+ # "#{@scoreboard.idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n"
@scoreboard[sbIdx] = task
# Track the total allocated slots for this resource and all parent
# resources.
@@ -76,23 +83,25 @@ def book(sbIdx, task, force = false)
def bookBooking(sbIdx, booking)
unless @scoreboard[sbIdx].nil?
- if @scoreboard[sbIdx].is_a?(Task)
+ if booked?(sbIdx)
error('booking_conflict',
"Resource #{@property.fullId} has multiple conflicting " +
- "bookings for #{idxToDate(sbIdx)}. The conflicting " +
+ "bookings for #{@scoreboard.idxToDate(sbIdx)}. The conflicting " +
"tasks are #{@scoreboard[sbIdx].fullId} and " +
"#{booking.task.fullId}.", true, booking.sourceFileInfo)
end
if @scoreboard[sbIdx] > booking.overtime
if @scoreboard[sbIdx] == 1 && booking.sloppy == 0
error('booking_no_duty',
"Resource #{@property.fullId} has no duty at " +
- "#{idxToDate(sbIdx)}.", true, booking.sourceFileInfo)
+ "#{@scoreboard.idxToDate(sbIdx)}.", true,
+ booking.sourceFileInfo)
end
if @scoreboard[sbIdx] == 2 && booking.sloppy <= 1
error('booking_on_vacation',
"Resource #{@property.fullId} is on vacation at " +
- "#{idxToDate(sbIdx)}.", true, booking.sourceFileInfo)
+ "#{@scoreboard.idxToDate(sbIdx)}.", true,
+ booking.sourceFileInfo)
end
return false
end
@@ -166,8 +175,8 @@ def getEffectiveFreeLoad(startIdx, endIdx)
# the period specified with the Interval _iv_. If task is not nil
# only allocations to this tasks are respected.
def allocated?(iv, task = nil)
- startIdx = @project.dateToIdx(iv.start, true)
- endIdx = @project.dateToIdx(iv.end, true)
+ startIdx = @scoreboard.dateToIdx(iv.start, true)
+ endIdx = @scoreboard.dateToIdx(iv.end, true)
startIdx = @firstBookedSlot if @firstBookedSlot &&
startIdx < @firstBookedSlot
@@ -206,8 +215,9 @@ def getBookings
# Make sure the index is correct even for the last task block.
idx += 1 if idx == endIdx
# Append the new interval to the Booking.
- bookings[lastTask].intervals << Interval.new(idxToDate(bookingStart),
- idxToDate(idx))
+ 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)
@@ -225,50 +235,68 @@ def getBookings
def initScoreboard
# Create scoreboard and mark all slots as unavailable
- @scoreboard = Array.new(@project.scoreboardSize, 1)
- # We need this frequently and can savely cache it here.
+ @scoreboard = Scoreboard.new(@project['start'], @project['end'],
+ @project['scheduleGranularity'], 1)
+
+ # We'll need this frequently and can savely cache it here.
@shifts = a('shifts')
# Change all work time slots to nil (available) again.
- 0.upto(@project.scoreboardSize) do |i|
- @scoreboard[i] = nil if onShift?(idxToDate(i))
+ 0.upto(@project.scoreboardSize - 1) do |i|
+ @scoreboard[i] = nil if onShift?(@scoreboard.idxToDate(i))
end
# Mark all resource specific vacation slots as such (2)
a('vacations').each do |vacation|
- startIdx = @project.dateToIdx(vacation.start)
- endIdx = @project.dateToIdx(vacation.end) - 1
+ startIdx = @scoreboard.dateToIdx(vacation.start)
+ endIdx = @scoreboard.dateToIdx(vacation.end) - 1
startIdx.upto(endIdx) do |i|
@scoreboard[i] = 2
end
end
- # Mark the vacations from all the shifts the resource is assigned to.
- 0.upto(@project.scoreboardSize) do |i|
- @scoreboard[i] = 2 if @shifts.onVacation?(idxToDate(i))
- end
# Mark all global vacation slots as such (2)
@project['vacations'].each do |vacation|
- startIdx = @project.dateToIdx(vacation.start, true)
- endIdx = @project.dateToIdx(vacation.end, true) - 1
+ startIdx = @scoreboard.dateToIdx(vacation.start, true)
+ endIdx = @scoreboard.dateToIdx(vacation.end, true) - 1
startIdx.upto(endIdx) do |i|
@scoreboard[i] = 2
end
end
- end
- def idxToDate(sbIdx)
- @project.idxToDate(sbIdx)
+ unless @shifts.nil?
+ # Mark the vacations from all the shifts the resource is assigned to.
+ 0.upto(@project.scoreboardSize - 1) do |i|
+ v = @shifts.getSbSlot(@scoreboard.idxToDate(i))
+ # Check if the vacation replacement bit is set. In that case we copy the
+ # while interval over to the resource scoreboard overriding any global
+ # vacations.
+ if (v & (1 << 8)) > 0
+ # The ShiftAssignments scoreboard and the ResourceScenario scoreboard
+ # unfortunately can't use the same values for a certain meaning. So,
+ # we have to use a map to translate the values.
+ map = [ nil, nil, 1, 2 ]
+ @scoreboard[i] = map[v & 0xFF]
+ elsif (v & 0xFF) == 3
+ # 3 in ShiftAssignments means 2 in ResourceScenario (on vacation)
+ @scoreboard[i] = 2
+ end
+ end
+ end
end
+ #def idxToDate(sbIdx)
+ # @scoreboard.idxToDate(sbIdx)
+ #end
+
def onShift?(date)
# The more redable but slower form would be:
# if @shifts.assigned?(date)
# return @shifts.onShift?(date)
# else
# a('workinghours').onShift?(date)
# end
- if (v = @shifts.getSbSlot(date)) > 0
+ if !@shifts.nil? && (v = (@shifts.getSbSlot(date) & 0xFF)) > 0
v == 1
else
a('workinghours').onShift?(date)
View
7 lib/ScenarioData.rb
@@ -15,14 +15,17 @@
class ScenarioData
- def initialize(property, idx)
+ attr_reader :property
+
+ def initialize(property, idx, attributes)
@property = property
@project = property.project
@scenarioIdx = idx
+ @attributes = attributes
end
def a(attributeName)
- @property[attributeName, @scenarioIdx]
+ @attributes[attributeName].value
end
def error(id, text, abort = true, sourceFileInfo = nil)
View
9 lib/Scoreboard.rb
@@ -41,7 +41,7 @@ def idxToDate(idx, forceIntoProject = false)
if forceIntoProject
return @startDate if kdx < 0
return @endDate if @size - 1 if idx >= @size
- elsif endif idx < 0 || idx >= @size
+ elsif idx < 0 || idx >= @size
raise "Index #{idx} is out of scoreboard range (#{size - 1})"
end
@startDate + idx * @resolution
@@ -60,6 +60,13 @@ def dateToIdx(date, forceIntoProject = false)
((date - @startDate) / @resolution).to_i
end
+ # Iterate over all scoreboard entries.
+ def each
+ @sb.each do |entry|
+ yield entry
+ end
+ end
+
# Get the value at index _idx_.
def [](idx)
@sb[idx]
View
2 lib/Shift.rb
@@ -23,7 +23,7 @@ def initialize(project, id, name, parent)
@data = Array.new(@project.scenarioCount, nil)
0.upto(@project.scenarioCount) do |i|
- @data[i] = ShiftScenario.new(self, i)
+ @data[i] = ShiftScenario.new(self, i, @scenarioAttributes[i])
end
end
View
73 lib/ShiftAssignments.rb
@@ -40,6 +40,12 @@ def overlaps?(iv)
@interval.overlaps?(iv)
end
+ # Returns true if the shift is active and requests to replace global
+ # vacation settings.
+ def replace?(date)
+ @interval.start <= date && date < @interval.end && @shiftScenario.replace?
+ end
+
# Check if date is withing the assignment period.
def assigned?(date)
@interval.start <= date && date < @interval.end
@@ -55,6 +61,10 @@ def onVacation?(date)
@shiftScenario.onVacation?(date)
end
+ # Primarily used for debugging
+ def to_s
+ "#{@shiftScenario.property.id} #{interval}"
+ end
end
# This class manages a list of ShiftAssignment elements. The intervals of the
@@ -105,6 +115,7 @@ def initialize(sa = nil)
# Some operations require access to the whole project.
def setProject(project)
@project = project
+ @scoreboard = newScoreboard
end
# Add a new assignment to the list. In case there was no overlap the
@@ -129,15 +140,18 @@ def getSbSlot(date)
@assignments.each do |sa|
next unless sa.assigned?(date)
+ # Set the 8th bit if the shift replaces global vacations.
+ replace = sa.replace?(date) ? (1 << 8) : 0
+
if sa.onVacation?(date)
# On vacation
- return @scoreboard[idx] = 3
+ return @scoreboard[idx] = 3 | replace
elsif sa.onShift?(date)
# On shift
- return @scoreboard[idx] = 1
+ return @scoreboard[idx] = 1 | replace
else
# Off hour
- return @scoreboard[idx] = 2
+ return @scoreboard[idx] = 2 | replace
end
end
# No assignment for slot
@@ -153,51 +167,67 @@ def assigned?(date)
# Returns true if any of the defined shift periods contains the
# _date_ and the shift has working hours defined for that _date_.
def onShift?(date)
- getSbSlot(date) == 1
+ (getSbSlot(date) & 0xFF) == 1
end
# Returns true if any of the defined shift periods contains the _date_ and
# the shift has a vacation defined or all off hours defined for that _date_.
def timeOff?(date)
- getSbSlot(date) >= 2
+ (getSbSlot(date) & 0xFF) >= 2
end
# Returns true if any of the defined shift periods contains the _date_ and
# if the shift has a vacation defined for the _date_.
def onVacation?(date)
- getSbSlot(date) == 3
+ (getSbSlot(date) & 0xFF) == 3
end
# Returns true of two ShiftAssignments object have the same assignment
# pattern.
def ==(shiftAssignments)
- return false if @assignments.size != shiftAssignments.assignments.size
+ return false if @assignments.size != shiftAssignments.assignments.size ||
+ @project != shiftAssignments.project
0.upto(@assignments.size - 1) do |i|
return false if @assignments[i] != shiftAssignments.assignments[i]
end
true
end
+ # This function is primarily used for debugging purposes.
+ def to_s
+ return '' if @assignments.empty?
+
+ out = "shifts "
+ first = true
+ @assignments.each do |sa|
+ if first
+ first = false
+ else
+ out += ', '
+ end
+ out += sa.to_s
+ end
+ out
+ end
+
private
# This function either returns a new Scoreboard or a reference to an
# existing one in case we already have one for the same assigment patterns.
def newScoreboard
@@scoreboards.each do |sbRecord|
- assignmentObjectIDs = sbRecord[0]
- scoreboard = sbRecord[1]
- assignmentObjectIDs.each do |id|
- # We have to store the object_id, not the reference. If we'd store a
- # reference, the GC will never destroy it.
- sa = _id2ref(id)
- if sa == self
- # Register the ShiftAssignments object as a user of an existing
- # scoreboard.
- assignmentObjectIDs << object_id
- # Return a reference to the existing scoreboard.
- return scoreboard
- end
+ # We only have to look at the first ShiftAssignment for a comparison.
+ # The others should match as well.
+ id = sbRecord[0][0]
+ # We have to store the object_id, not the reference. If we'd store a
+ # reference, the GC will never destroy it.
+ if self == _id2ref(id)
+ # Register the ShiftAssignments object as a user of an existing
+ # scoreboard.
+ sbRecord[0] << object_id
+ # Return a reference to the existing scoreboard.
+ return sbRecord[1]
end
end
# We have not found a matching scoreboard, so we have to create a new one.
@@ -235,9 +265,10 @@ def ShiftAssignments.deleteScoreboard(objId)
end
end
end
+ undefine_finalizer(self)
end
- # Returns true if the intverval overlaps with any of the assignment periods.
+ # Returns true if the interval overlaps with any of the assignment periods.
def overlaps?(iv)
@assignments.each do |sa|
return true if sa.overlaps?(iv)
View
6 lib/ShiftAssignmentsAttribute.rb
@@ -16,12 +16,6 @@ class ShiftAssignmentsAttribute < AttributeBase
def initialize(property, type)
super(property, type)
-
- @value = ShiftAssignments.new
- end
-
- def initValue(arg)
- ShiftAssignments.new
end
def setProject(project)
View
6 lib/ShiftScenario.rb
@@ -15,7 +15,7 @@
# This class handles the scenario specific features of a Shift object.
class ShiftScenario < ScenarioData
- def initialize(resource, scenarioIdx)
+ def initialize(resource, scenarioIdx, attributes)
super
end
@@ -24,6 +24,10 @@ def onShift?(date)
a('workinghours').onShift?(date)
end
+ def replace?
+ a('replace')
+ end
+
# Returns true if the shift has a vacation defined for the _date_.
def onVacation?(date)
a('vacations').each do |vacationIv|
View
2 lib/Task.rb
@@ -21,7 +21,7 @@ def initialize(project, id, name, parent)
@data = Array.new(@project.scenarioCount, nil)
0.upto(@project.scenarioCount) do |i|
- @data[i] = TaskScenario.new(self, i)
+ @data[i] = TaskScenario.new(self, i, @scenarioAttributes[i])
end
end
View
24 lib/TaskScenario.rb
@@ -16,7 +16,7 @@ class TaskScenario < ScenarioData
attr_reader :isRunAway
- def initialize(task, scenarioIdx)
+ def initialize(task, scenarioIdx, attributes)
super
end
@@ -113,7 +113,7 @@ def postScheduleCheck
@errors += 1 unless task.postScheduleCheck(@scenarioIdx)
end
- # There is no point the check the parent if the child(s) have errors.
+ # There is no point to check the parent if the child(s) have errors.
return false if @errors > 0
# Same for runaway tasks. They have already been reported.
@@ -459,6 +459,10 @@ def calcPathCriticalness
calcDirCriticalness(true)
end
+ # Return the date of the next slot this task wants to have scheduled. This
+ # must either be the first slot ever or it must be directly adjecent to the
+ # previous slot. If this task has not yet been scheduled at all, @lastSlot
+ # is still nil. Otherwise it contains the date of the last scheduled slot.
def nextSlot(slotDuration)
return nil if a('scheduled')
@@ -469,21 +473,22 @@ def nextSlot(slotDuration)
end
end
+ # Check if the task is ready to be scheduled. For this it needs to have at
+ # least one specified end date and a duration criteria or the other end
+ # date.
def readyForScheduling?
return false if a('scheduled')
if a('forward')
if !a('start').nil? &&
(a('effort') != 0 || a('length') != 0 || a('duration') != 0 ||
- a('milestone')) &&
- a('end').nil?
+ a('milestone') || !a('end').nil?)
return true
end
else
if !a('end').nil? &&
(a('effort') != 0 || a('length') != 0 || a('duration') != 0 ||
- a('milestone')) &&
- a('start').nil?
+ a('milestone') || !a('start').nil?)
return true
end
end
@@ -562,7 +567,7 @@ def schedule(slot, slotDuration)
# Depending on the scheduling direction we can mark the task as
# scheduled once we have reached the other end.
if (a('forward') && slot + slotDuration >= a('end')) ||
- (!a('forward') && slot < a('start'))
+ (!a('forward') && slot <= a('start'))
@property['scheduled', @scenarioIdx] = true
return true
end
@@ -756,7 +761,10 @@ def bookResources(date, slotDuration)
return
end
- # TODO: Handle shifts
+ # If the task has shifts to limit the allocations, we check that we are
+ # within a shift interval. If not, abort the booking.
+ return if !a('shifts').nil? && !a('shifts').onShift?(date)
+
sbIdx = @project.dateToIdx(date)
# We first have to make sure that if there are mandatory resources
View
4 lib/TextScanner.rb
@@ -264,8 +264,10 @@ def returnChar(c)
def skipComment
# Read all characters until line or file end is found
+ @ignoreMacros = true
while (c = nextChar(true)) && c != ?\n
end
+ @ignoreMacros = false
returnChar(c)
end
@@ -276,10 +278,12 @@ def skipMultiLineComment
return
end
+ @ignoreMacros = true
begin
while (c = nextChar(false)) != ?*
end
end until (c = nextChar(false)) == ?/
+ @ignoreMacros = false
end
def readBlanks(c)
View
76 lib/TjpSyntaxRules.rb
@@ -519,6 +519,24 @@ def rule_include
)
end
+ def rule_intervalOrDate
+ newRule('intervalOrDate')
+ newPattern(%w( $DATE !intervalOptionalEnd ), lambda {
+ if @val[1]
+ mode = @val[1][0]
+ endSpec = @val[1][1]
+ if mode == 0
+ Interval.new(@val[0], endSpec)
+ else
+ Interval.new(@val[0], @val[0] + endSpec)
+ end
+ else
+ Interval.new(@val[0], @val[0].sameTimeNextDay)
+ end
+ })
+ arg(0, 'start date', 'The start date of the interval')
+ end
+
def rule_interval
newRule('interval')
newPattern(%w( $DATE !intervalEnd ), lambda {
@@ -560,8 +578,21 @@ def rule_intervalEnd
})
end
+ def rule_intervalOptionalEnd
+ newRule('intervalOptionalEnd')
+ optional
+ newPattern([ '_ - ', '$DATE' ], lambda {
+ [ 0, @val[1] ]
+ })
+ arg(1, 'end date', 'The end date of the interval')
+
+ newPattern(%w( _+ !intervalDuration ), lambda {
+ [ 1, @val[1] ]
+ })
+ end
+
def rule_intervals
- newListRule('intervals', '!interval')
+ newListRule('intervals', '!intervalOrDate')
end
def rule_listOfDays
@@ -1526,6 +1557,13 @@ def rule_resourceScenarioAttributes
)
newPattern(%w( _shifts !shiftAssignments ))
+ doc('resource.shifts', <<'EOT'
+Limits the working time of a resource to a defined shift during the specified
+interval. Multiple shifts can be defined, but shift intervals may not overlap.
+Outside of the defined shift intervals the resource uses its normal working
+hours and vacations.
+EOT
+ )
newPattern(%w( _vacation !vacationName !intervals ), lambda {
@property['vacations', @scenarioIdx] =
@@ -1643,11 +1681,20 @@ def rule_shift
def rule_shiftAssignment
newRule('shiftAssignment')
- newPattern(%w( !shiftId !interval ), lambda {
- if !@property['shifts', @scenarioIdx].
- addAssignment(ShiftAssignment.new(@val[0].scenario(@scenarioIdx),
- @val[1]))
- error('shift_assignment_overlap', 'Shifts may not overlap each other.')
+ newPattern(%w( !shiftId !intervals ), lambda {
+ # Make sure we have a ShiftAssignment for the property.
+ if @property['shifts', @scenarioIdx].nil?
+ @property['shifts', @scenarioIdx] = ShiftAssignments.new
+ @property['shifts', @scenarioIdx].setProject(@project)
+ end
+
+ @val[1].each do |interval|
+ if !@property['shifts', @scenarioIdx].
+ addAssignment(ShiftAssignment.new(@val[0].scenario(@scenarioIdx),
+ interval))
+ error('shift_assignment_overlap',
+ 'Shifts may not overlap each other.')
+ end
end
# Set same value again to set the 'provided' state for the attribute.
@property['shifts', @scenarioIdx] = @property['shifts', @scenarioIdx]
@@ -1700,6 +1747,14 @@ def rule_shiftId
def rule_shiftScenarioAttributes
newRule('shiftScenarioAttributes')
+ newPattern(%w( _replace ), lambda {
+ @property['replace', @scenarioIdx] = true
+ })
+ doc('replace', <<'EOT'
+Use this attribute if the vacation definition for the shift should replace the vacation settings of a resource. This is only effective for shifts that are assigned to resources directly. It is not effective for shifts that are assigned to tasks or allocations.
+EOT
+ )
+
newPattern(%w( _timezone $STRING ), lambda {
@property['timezone', @scenarioIdx] = @val[1]
})
@@ -2314,6 +2369,15 @@ def rule_taskScenarioAttributes
EOT
)
+ newPattern(%w( _shifts !shiftAssignments ))
+ doc('task.shifts', <<'EOT'
+Limits the working time for this task to a defined shift during the specified
+interval. Multiple shifts can be defined, but shift intervals may not overlap.
+If one or more shifts have been assigned to a task, no work is done outside of
+the assigned intervals and the workinghours defined by the shifts.
+EOT
+ )
+
newPattern(%w( _start !valDate), lambda {
@property['start', @scenarioIdx] = @val[1]
@property['forward', @scenarioIdx] = true
View
74 test/TestSuite/Scheduler/Correct/Shift.tjp
@@ -3,57 +3,87 @@ project test "Test" "1.0" 2007-08-22 +2m
macro Fixend [ minend ${1} maxend ${1} ]
shift odd_days "Odd Days" {
- workinghours mon, wed, fri 10:00 - 16:00
- workinghours tue, thu, sat, sun off
+ workinghours mon, wed, fri 10:00 - 16:00
+ workinghours tue, thu, sat, sun off
}
shift even_days "Even Days" {
- workinghours mon, wed, fri off
- workinghours tue, thu, sat, sun 10:00 - 16:00
+ workinghours mon, wed, fri off
+ workinghours tue, thu, sat, sun 10:00 - 16:00
+ shift even_no_we "Even Days, no weekend" {
+ workinghours sat, sun off
+ }
}
shift morning "Morning" {
- workinghours mon - sun 8:00 - 12:00
+ workinghours mon - sun 8:00 - 12:00
+}
+
+shift thu_vac "Vacation on Thursday" {
+ vacation 2007-09-06
+ replace
}
resource team "Team" {
- vacation 2007-08-22 +3d
+ vacation 2007-08-22 +3d
shifts morning 2007-08-23 +5d
resource mdf "MDF Worker" {
- shifts odd_days 2007-09-01 +2w
+ shifts odd_days 2007-09-01 +2w
}
resource ttss "TTSS Worker" {
- shifts even_days 2007-09-01 +2w
+ shifts even_days 2007-09-01 +2w
+ }
+
+ resource tt "TT Worker" {
+ shifts even_no_we 2007-09-01 +2w
+ }
+
+ resource wed_vac "Vacation on Wednesday" {
+ vacation 2007-09-05
+ shifts thu_vac 2007-09-01 +7d
}
}
resource default "Default Worker"
task prj "Project" {
- start 2007-08-22
+ start 2007-08-22
- task t1 "MDF Task" {
- effort 4w
- allocate mdf
+ task mdf "MDF Task" {
+ effort 4w
+ allocate mdf
${Fixend "2007-09-28-18:00"}
}
- task t2 "TTSS Task" {
- effort 5w
- allocate ttss
- ${Fixend "2007-10-04-14:00"}
+ task ttss "TTSS Task" {
+ effort 5w
+ allocate ttss
+ ${Fixend "2007-10-04-14:00"}
}
- task t3 "Default Task" {
- effort 7w
- allocate default
- ${Fixend "2007-10-09-18:00"}
- }
+ task tt "TT Task" {
+ effort 4w
+ allocate tt
+ ${Fixend "2007-10-02-14:00"}
+ }
+
+ task default "Default Task" {
+ effort 7w
+ allocate default
+ ${Fixend "2007-10-09-18:00"}
+ }
+
+ task vac_test "Vacation on Thursday" {
+ start 2007-09-01
+ effort 4d
+ allocate wed_vac
+ ${Fixend "2007-09-07-18:00"}
+ }
}
htmltaskreport "Shift.html" {
- columns name, start, end, daily
+ columns name, start, end, daily
}

0 comments on commit 12d1fe4

Please sign in to comment.