Quantcast

True time-span implementation.

Johnny C. Lam [10-31-13 - 19:06]
True time-span implementation.

A time-span is a union of continuous interval subsets of the real number
line (0, infinity).  Internally represented by an array of numbers
representing alternately the left and right endpoints of an interval.
Point sets and empty sets are both considered empty.

Pre-allocate tables when creating parse tree nodes in OvaleCompile to be
reused for time-span evaluation within OvaleBestAction.

Convert OvaleBestAction to use time-spans for correctness when dealing
with arbitrary unions and intersections of time intervals when evaluating
nodes.

This fixes ticket 306 - "and" "or" bugged by @Wiljo.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1117 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
OvaleBestAction.lua
OvaleCompile.lua
OvaleCondition.lua
OvaleFrame.lua
OvaleTimeSpan.lua
compiler.pl
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index 75c75a7..60bf989 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -18,6 +18,7 @@ local OvaleCondition = Ovale.OvaleCondition
 local OvaleData = Ovale.OvaleData
 local OvaleEquipement = Ovale.OvaleEquipement
 local OvalePaperDoll = Ovale.OvalePaperDoll
+local OvalePool = Ovale.OvalePool
 local OvalePower = Ovale.OvalePower
 local OvaleSpellBook = Ovale.OvaleSpellBook
 local OvaleStance = Ovale.OvaleStance
@@ -33,9 +34,12 @@ local select = select
 local strfind = string.find
 local tonumber = tonumber
 local tostring = tostring
+local wipe = table.wipe
 local Complement = OvaleTimeSpan.Complement
+local CopyTimeSpan = OvaleTimeSpan.CopyTo
 local HasTime = OvaleTimeSpan.HasTime
 local Intersect = OvaleTimeSpan.Intersect
+local IntersectInterval = OvaleTimeSpan.IntersectInterval
 local Measure = OvaleTimeSpan.Measure
 local Union = OvaleTimeSpan.Union
 local API_GetActionCooldown = GetActionCooldown
@@ -56,6 +60,8 @@ local OVALE_DEFAULT_PRIORITY = 3

 -- Age of the current computation.
 local self_serial = 0
+-- Pool of time-span tables.
+local self_pool = OvalePool("OvaleBestAction_pool")
 --</private-static-properties>

 --<private-static-methods>
@@ -72,22 +78,24 @@ end

 local function ComputeAction(element)
 	local self = OvaleBestAction
+	local action = element.params[1]
 	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
 		actionUsable, actionShortcut, actionIsCurrent, actionEnable, spellId = self:GetActionInfo(element)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()

 	if not actionTexture then
-		Ovale:Logf("Action %s not found", element.params[1])
-		return nil
-	end
-	if not (actionEnable and actionEnable > 0) then
-		Ovale:Logf("Action %s not enabled", element.params[1])
-		return nil
-	end
-	if element.params.usable == 1 and not actionUsable then
-		Ovale:Logf("Action %s not usable", element.params[1])
-		return nil
+		Ovale:Logf("Action %s not found", action)
+		return timeSpan
+	elseif not (actionEnable and actionEnable > 0) then
+		Ovale:Logf("Action %s not enabled", action)
+		return timeSpan
+	elseif element.params.usable == 1 and not actionUsable then
+		Ovale:Logf("Action %s not usable", action)
+		return timeSpan
 	end

+	-- Set the cast time of the action.
 	if spellId then
 		local si = spellId and OvaleData.spellInfo[spellId]
 		if si and si.casttime then
@@ -101,8 +109,8 @@ local function ComputeAction(element)
 			end
 		end
 		if si and si.toggle and actionIsCurrent then
-			Ovale:Logf("Action %s (toggle) is the current action", element.params[1])
-			return nil
+			Ovale:Logf("Action %s (toggle) is the current action", action)
+			return timeSpan
 		end
 	else
 		element.castTime = 0
@@ -118,6 +126,7 @@ local function ComputeAction(element)

 	Ovale:Logf("start=%f attenteFinCast=%s [%d]", start, OvaleState.attenteFinCast, element.nodeId)

+	-- If the action is available before the end of the current spellcast, then wait until we can first cast the action.
 	if start < OvaleState.attenteFinCast then
 		local si = OvaleState.currentSpellId and OvaleData.spellInfo[OvaleState.currentSpellId]
 		if not (si and si.canStopChannelling) then
@@ -148,38 +157,44 @@ local function ComputeAction(element)
 			Ovale:Logf("%s start=%f, numTicks=%d, tick=%f, tickTime=%f", spellId, start, numTicks, tick, tickTime)
 		end
 	end
-	Ovale:Logf("Action %s can start at %f", element.params[1], start)
+	Ovale:Logf("Action %s can start at %f", action, start)

 	local priority = element.params.priority or OVALE_DEFAULT_PRIORITY
-	return start, math.huge, priority, element
+	timeSpan[1], timeSpan[2] = start, math.huge
+	return timeSpan, priority, element
 end

 local function ComputeAnd(element)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
-	local startA, endA = self:ComputeBool(element.a)
+	local timeSpanA = self:ComputeBool(element.a)
+	local timeSpan = element.timeSpan
+
 	-- Short-circuit evaluation of left argument to AND.
-	if Measure(startA, endA) == 0 then
-		Ovale:Logf("%s return timespan with measure 0 [%d]", element.type, element.nodeId)
-		return nil
+	if Measure(timeSpanA) == 0 then
+		timeSpan:Reset(timeSpanA)
+	else
+		local timeSpanB = self:ComputeBool(element.b)
+		-- Take intersection of A and B.
+		timeSpan:Reset()
+		Intersect(timeSpanA, timeSpanB, timeSpan)
 	end
-	local startB, endB = self:ComputeBool(element.b)
-	-- Take intersection of (startA, endA) and (startB, endB).
-	startA, endA = Intersect(startA, endA, startB, endB)
-	Ovale:Logf("%s returns %s, %s [%d]", element.type, startA, endA, element.nodeId)
-	return startA, endA
+	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+	return timeSpan
 end

 local function ComputeArithmetic(element)
 	local self = OvaleBestAction
-	local startA, endA, _, elementA = self:Compute(element.a)
-	local startB, endB, _, elementB = self:Compute(element.b)
+	local timeSpanA, _, elementA = self:Compute(element.a)
+	local timeSpanB, _, elementB = self:Compute(element.b)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()

-	-- Take intersection of (startA, endA) and (startB, endB)
-	startA, endA = Intersect(startA, endA, startB, endB)
-	if Measure(startA, endA) == 0 then
-		Ovale:Logf("%s return timespan with measure 0 [%d]", element.type, element.nodeId)
-		return nil
+	-- Take intersection of A and B.
+	Intersect(timeSpanA, timeSpanB, timeSpan)
+	if Measure(timeSpan) == 0 then
+		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+		return timeSpan, OVALE_DEFAULT_PRIORITY, PutValue(element, 0, 0, 0)
 	end

 	--[[
@@ -230,17 +245,17 @@ local function ComputeArithmetic(element)
 		n = c - z
 	elseif element.operator == "*" then
 		--[[
-			     A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
-			     B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
 			A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z)
-			          = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere.
+					  = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere.
 		--]]
 			l = A*B
 			m = atTime
 			n = A*z + B*c
 	elseif element.operator == "/" then
 		--[[
-			     A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
 				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
 			A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|.
 		--]]
@@ -253,7 +268,10 @@ local function ComputeArithmetic(element)
 		else
 			bound = abs(B/z)
 		end
-		startA, endA = Intersect(startA, endA, atTime - bound, atTime + bound)
+		local scratch = OvaleTimeSpan(self_pool:Get())
+		scratch:Reset(timeSpan)
+		IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan)
+		self_pool:Release(scratch)
 	elseif element.operator == "%" then
 		if c == 0 and z == 0 then
 			l = A % B
@@ -261,22 +279,27 @@ local function ComputeArithmetic(element)
 			n = 0
 		else
 			Ovale:Error("Parameters of % must be constants")
-			return nil
+			l = 0
+			m = 0
+			n = 0
+			timeSpan:Reset()
 		end
 	end
 	Ovale:Logf("result = %f+(t-%f)*%f [%d]", l, m, n, element.nodeId)
-	return startA, endA, OVALE_DEFAULT_PRIORITY, PutValue(element, l, m, n)
+	return timeSpan, OVALE_DEFAULT_PRIORITY, PutValue(element, l, m, n)
 end

 local function ComputeCompare(element)
 	local self = OvaleBestAction
-	local startA, endA, _, elementA = self:Compute(element.a)
-	local startB, endB, _, elementB = self:Compute(element.b)
+	local timeSpanA, _, elementA = self:Compute(element.a)
+	local timeSpanB, _, elementB = self:Compute(element.b)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()

-	-- Take intersection of (startA, endA) and (startB, endB)
-	startA, endA = Intersect(startA, endA, startB, endB)
-	if Measure(startA, endA) == 0 then
-		return nil
+	-- Take intersection of A and B.
+	Intersect(timeSpanA, timeSpanB, timeSpan)
+	if Measure(timeSpan) == 0 then
+		return timeSpan
 	end

 	--[[
@@ -296,11 +319,11 @@ local function ComputeCompare(element)
 	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, operator, x, y, z, element.nodeId)

 	--[[
-		         A(t) = B(t)
+				 A(t) = B(t)
 		a + (t - b)*c = x + (t - y)*z
 		a + t*c - b*c = x + t*z - y*z
-		    t*c - t*z = (x - y*z) - (a - b*c)
-		    t*(c - z) = B(0) - A(0)
+			t*c - t*z = (x - y*z) - (a - b*c)
+			t*(c - z) = B(0) - A(0)
 	--]]
 	local A = a - b*c
 	local B = x - y*z
@@ -310,37 +333,45 @@ local function ComputeCompare(element)
 				or (operator == "<=" and A <= B)
 				or (operator == ">" and A > B)
 				or (operator == ">=" and A >= B)) then
-			startA, endA = nil, nil
+			timeSpan:Reset()
 		end
 	else
+		local scratch = OvaleTimeSpan(self_pool:Get())
+		scratch:Reset(timeSpan)
 		local t = (B - A)/(c - z)
 		if (c > z and operator == "<")
 				or (c > z and operator == "<=")
 				or (c < z and operator == ">")
 				or (c < z and operator == ">=") then
-			startA, endA = Intersect(startA, endA, 0, t)
+			IntersectInterval(scratch, 0, t, timeSpan)
 		end
 		if (c < z and operator == "<")
 				or (c < z and operator == "<=")
 				or (c > z and operator == ">")
 				or (c > z and operator == ">=") then
-			startA, endA = Intersect(startA, endA, t, math.huge)
+			IntersectInterval(scratch, t, math.huge, timeSpan)
 		end
+		self_pool:Release(scratch)
 	end
-	Ovale:Logf("compare %s returns %s, %s [%d]", operator, startA, endA, element.nodeId)
-	return startA, endA
+	Ovale:Logf("compare %s returns %s [%d]", operator, tostring(timeSpan), element.nodeId)
+	return timeSpan
 end

 local function ComputeCustomFunction(element)
 	Ovale:Logf("custom function %s", element.name)
 	local self = OvaleBestAction
 	if not element.serial or element.serial < self_serial then
-		element.startA, element.endA, element.priorityA, element.elementA = self:Compute(element.a)
+		-- Cache new values in element.
+		element.timeSpanA, element.priorityA, element.elementA = self:Compute(element.a)
 		element.serial = self_serial
 	else
 		Ovale:Logf("Using cached values for %s", element.name)
 	end
-	local startA, endA, priorityA, elementA = element.startA, element.endA, element.priorityA, element.elementA
+
+	local timeSpanA, priorityA, elementA = element.timeSpanA, element.priorityA, element.elementA
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
 	if element.params.asValue and element.params.asValue == 1 then
 		--[[
 			Allow for the return value of a custom function to be "typecast" to a constant value.
@@ -357,7 +388,7 @@ local function ComputeCustomFunction(element)
 		==]]
 		local atTime = OvaleState.currentTime
 		local value = 0
-		if HasTime(startA, endA, atTime) then
+		if HasTime(timeSpanA, atTime) then
 			if not elementA then	-- boolean
 				value = 1
 			elseif elementA.type == "value" then
@@ -366,21 +397,26 @@ local function ComputeCustomFunction(element)
 				value = 1
 			end
 		end
-		return 0, math.huge, priorityA, PutValue(element, value, 0, 0)
+		timeSpan[1], timeSpan[2] = 0, math.huge
+		return timeSpan, priorityA, PutValue(element, value, 0, 0)
 	else
-		return startA, endA, priorityA, elementA
+		CopyTimeSpan(timeSpanA, timeSpan)
+		return timeSpan, priorityA, elementA
 	end
 end

 local function ComputeFunction(element)
 	local self = OvaleBestAction
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
 	local condition = OvaleCondition.conditions[element.func]
 	if not condition then
 		Ovale:Errorf("Condition %s not found", element.func)
-		return nil
+		return timeSpan
 	end
-	local start, ending, value, origin, rate = condition(element.params)

+	local start, ending, value, origin, rate = condition(element.params)
 	if Ovale.trace then
 		local conditionCall = element.func .. "("
 		for k, v in pairs(element.params) do
@@ -390,16 +426,21 @@ local function ComputeFunction(element)
 		Ovale:FormatPrint("Condition %s returned %s, %s, %s, %s, %s", conditionCall, start, ending, value, origin, rate)
 	end

+	if start and ending then
+		timeSpan[1], timeSpan[2] = start, ending
+	end
 	if value then
-		return start, ending, OVALE_DEFAULT_PRIORITY, PutValue(element, value, origin, rate)
+		return timeSpan, OVALE_DEFAULT_PRIORITY, PutValue(element, value, origin, rate)
 	else
-		return start, ending
+		return timeSpan
 	end
 end

 local function ComputeGroup(element)
 	local self = OvaleBestAction
-	local bestStart, bestEnding, bestPriority, bestElement, bestCastTime
+	local bestTimeSpan, bestPriority, bestElement, bestCastTime
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()

 	Ovale:Logf("%s [%d]", element.type, element.nodeId)

@@ -407,158 +448,181 @@ local function ComputeGroup(element)
 		return self:Compute(element.nodes[1])
 	end

+	local best = OvaleTimeSpan(self_pool:Get())
+	local current = OvaleTimeSpan(self_pool:Get())
+
 	for k, v in ipairs(element.nodes) do
-		local start, ending, priority, newElement = self:Compute(v)
+		local currentTimeSpan, currentPriority, currentElement = self:Compute(v)
 		-- We only care about actions that are available at time t > OvaleState.currentTime.
-		start, ending = Intersect(start, ending, OvaleState.currentTime, math.huge)
-		if Measure(start, ending) > 0 then
-			local castTime
-			if newElement then
-				castTime = newElement.castTime
+		current:Reset()
+		IntersectInterval(currentTimeSpan, OvaleState.currentTime, math.huge, current)
+		if Measure(current) > 0 then
+			Ovale:Logf("    group checking %s [%d]", tostring(current), element.nodeId)
+			local currentCastTime
+			if currentElement then
+				currentCastTime = currentElement.castTime
 			end
-			if not castTime or castTime < OvaleState.gcd then
-				castTime = OvaleState.gcd
+			if not currentCastTime or currentCastTime < OvaleState.gcd then
+				currentCastTime = OvaleState.gcd
 			end

 			local replace = false
-			if not bestStart then
+			if Measure(best) == 0 then
+				Ovale:Logf("    group first best %s [%d]", tostring(current), element.nodeId)
 				replace = true
-			else
-				if priority and not bestPriority then
-					Ovale:Errorf("Internal error: bestPriority=nil and priority=%d", priority)
-					return nil
-				elseif priority and priority > bestPriority then
-					-- If the new spell has a higher priority than the best one found, then choose the
-					-- higher priority spell if its cast is pushed back too far by the lower priority one.
-					if newElement and newElement.params and newElement.params.wait then
-						if start - bestStart < newElement.params.wait then
-							replace = true
-						end
-					elseif start - bestStart < bestCastTime * 0.75 then
-						replace = true
-					end
-				elseif priority and priority < bestPriority then
-					-- If the new spell has a lower priority than the best one found, then choose the
-					-- lower priority spell only if it doesn't push back the cast of the higher priority
-					-- one by too much.
-					if bestElement and bestElement.params and bestElement.params.wait then
-						if bestStart - start > bestElement.params.wait then
-							replace = true
-						end
-					elseif bestStart - start > castTime * 0.75 then
-						replace = true
-					end
-				else
-					-- If the spells have the same priority, then pick the one with an earlier cast time.
-					if bestElement and bestElement.params and bestElement.params.wait then
-						if bestStart - start > bestElement.params.wait then
-							replace = true
-						end
-					elseif bestStart > start then
-						replace = true
-					end
+			elseif not currentPriority or not bestPriority or currentPriority == bestPriority then
+				-- If the spells have the same priority, then pick the one with an earlier cast time.
+				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or 0
+				if best[1] - current[1] > threshold then
+					Ovale:Logf("    group new best %s [%d]", tostring(current), element.nodeId)
+					replace = true
+				end
+			elseif currentPriority > bestPriority then
+				-- If the current spell has a higher priority than the best one found, then choose the
+				-- higher priority spell if its cast is pushed back too far by the lower priority one.
+				local threshold = (currentElement and currentElement.params) and currentElement.params.wait or (bestCastTime * 0.75)
+				if current[1] - best[1] < threshold then
+					Ovale:Logf("    group new best (lower prio) %s [%d]", tostring(current), element.nodeId)
+					replace = true
+				end
+			elseif currentPriority < bestPriority then
+				-- If the current spell has a lower priority than the best one found, then choose the
+				-- lower priority spell only if it doesn't push back the cast of the higher priority
+				-- one by too much.
+				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or (currentCastTime * 0.75)
+				if best[1] - current[1] > threshold then
+					Ovale:Logf("    group new best (higher prio) %s [%d]", tostring(current), element.nodeId)
+					replace = true
 				end
 			end
 			if replace then
-				bestStart = start
-				bestPriority = priority
-				bestElement = newElement
-				bestEnding = ending
-				bestCastTime = castTime
+				best:Reset(current)
+				bestTimeSpan = currentTimeSpan
+				bestPriority = currentPriority
+				bestElement = currentElement
+				bestCastTime = currentCastTime
 			end
+			-- If the node is a "wait" node, then skip the remaining nodes.
+			if currentElement and currentElement.wait then break end
 		end
-		-- If the node is a "wait" node, then skip the remaining nodes.
-		if newElement and newElement.wait then break end
 	end

-	if not bestStart then
-		Ovale:Logf("group return nil [%d]", element.nodeId)
-		return nil
-	end
+	self_pool:Release(best)
+	self_pool:Release(current)

-	if bestElement then
-		local id = bestElement.value
-		if bestElement.params then
-			id = bestElement.params[1]
-		end
-		Ovale:Logf("group best action %s remains %f, %f [%d]", id, bestStart, bestEnding, element.nodeId)
+	if not bestTimeSpan then
+		Ovale:Logf("group return %s [%d]", tostring(timeSpan), element.nodeId)
+		return timeSpan
 	else
-		Ovale:Logf("group no best action returns %f, %f [%d]", bestStart, bestEnding, element.nodeId)
+		CopyTimeSpan(bestTimeSpan, timeSpan)
+		if bestElement then
+			local id = bestElement.value
+			if bestElement.params then
+				id = bestElement.params[1]
+			end
+			Ovale:Logf("group best action %s remains %s [%d]", id, tostring(timeSpan), element.nodeId)
+		else
+			Ovale:Logf("group no best action returns %s [%d]", tostring(timeSpan), element.nodeId)
+		end
+		return timeSpan, bestPriority, bestElement
 	end
-	return bestStart, bestEnding, bestPriority, bestElement
 end

 local function ComputeIf(element)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction

-	local startA, endA = self:ComputeBool(element.a)
-	local atTime = OvaleState.currentTime
-	-- "unless A B" is equivalent to "if (not A) B", so take the complement of A.
-	if element.type == "unless" then
-		startA, endA = Complement(startA, endA, atTime)
+	local timeSpanA = self:ComputeBool(element.a)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
+	local conditionTimeSpan = OvaleTimeSpan(self_pool:Get())
+	if element.type == "if" then
+		conditionTimeSpan:Reset(timeSpanA)
+	elseif element.type == "unless" then
+		-- "unless A B" is equivalent to "if (not A) B", so take the complement of A.
+		Complement(timeSpanA, conditionTimeSpan)
 	end
-	-- Short-circuit evaluation of left argument to AND.
-	if Measure(startA, endA) == 0 then
-		Ovale:Logf("%s return timespan with measure 0 [%d]", element.type, element.nodeId)
-		return nil
+	-- Short-circuit evaluation of left argument to IF.
+	if Measure(conditionTimeSpan) == 0 then
+		timeSpan:Reset(conditionTimeSpan)
+		self_pool:Release(conditionTimeSpan)
+		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+		return timeSpan, OVALE_DEFAULT_PRIORITY, PutValue(element, 0, 0, 0)
 	end

-	local startB, endB, priorityB, elementB = self:Compute(element.b)
+	local timeSpanB, priorityB, elementB = self:Compute(element.b)
 	-- If the "then" clause is a "wait" node, then only wait if the conditions are true.
-	if elementB and elementB.wait and not HasTime(startA, endA, atTime) then
+	if elementB and elementB.wait and not HasTime(conditionTimeSpan, OvaleState.currentTime) then
 		elementB.wait = nil
 	end
-	-- Take intersection of A and B.
-	startB, endB = Intersect(startA, endA, startB, endB)
-	if Measure(startB, endB) == 0 then
-		Ovale:Logf("%s return nil [%d]", element.type, element.nodeId)
-	else
-		Ovale:Logf("%s return %f, %f [%d]", element.type, startB, endB, element.nodeId)
-	end
-	return startB, endB, priorityB, elementB
+	-- Take intersection of the condition and B.
+	Intersect(conditionTimeSpan, timeSpanB, timeSpan)
+	self_pool:Release(conditionTimeSpan)
+
+	Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+	return timeSpan, priorityB, elementB
 end

 local function ComputeLua(element)
 	local ret = loadstring(element.lua)()
 	Ovale:Logf("lua %s [%d]", ret, element.nodeId)
-	return 0, math.huge, OVALE_DEFAULT_PRIORITY, PutValue(element, ret, 0, 0)
+
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
+	timeSpan[1], timeSpan[2] = 0, math.huge
+	return timeSpan, OVALE_DEFAULT_PRIORITY, PutValue(element, ret, 0, 0)
 end

 local function ComputeNot(element)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
-	local startA, endA = self:ComputeBool(element.a)
-	startA, endA = Complement(startA, endA, OvaleState.currentTime)
-	Ovale:Logf("%s returns %s, %s [%d]", element.type, startA, endA, element.nodeId)
-	return startA, endA
+	local timeSpanA = self:ComputeBool(element.a)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
+	Complement(timeSpanA, timeSpan)
+	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+	return timeSpan
 end

 local function ComputeOr(element)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
-	local startA, endA = self:ComputeBool(element.a)
-	local startB, endB = self:ComputeBool(element.b)
-	-- Take union of (startA, endA) and (startB, endB)
-	startA, endA = Union(startA, endA, startB, endB)
-	Ovale:Logf("%s returns %s, %s [%d]", element.type, startA, endA, element.nodeId)
-	return startA, endA
+	local timeSpanA = self:ComputeBool(element.a)
+	local timeSpanB = self:ComputeBool(element.b)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
+	-- Take union of A and B.
+	Union(timeSpanA, timeSpanB, timeSpan)
+	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+	return timeSpan
 end

 local function ComputeValue(element)
 	Ovale:Logf("value %s", element.value)
-	return 0, math.huge, OVALE_DEFAULT_PRIORITY, element
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
+	timeSpan[1], timeSpan[2] = 0, math.huge
+	return timeSpan, OVALE_DEFAULT_PRIORITY, element
 end

 local function ComputeWait(element)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
-	local startA, endA, priorityA, elementA = self:Compute(element.a)
+	local timeSpanA, priorityA, elementA = self:Compute(element.a)
+	local timeSpan = element.timeSpan
+	timeSpan:Reset()
+
 	if elementA then
 		elementA.wait = true
-		Ovale:Logf("%s return %f, %f [%d]", element.type, startA, endA, element.nodeId)
+		CopyTimeSpan(timeSpanA, timeSpan)
+		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
 	end
-	return startA, endA, priorityA, elementA
+	return timeSpan, priorityA, elementA
 end
 --</private-static-methods>

@@ -594,13 +658,13 @@ function OvaleBestAction:GetActionInfo(element)
 		return nil
 	end

-	local spellId = element.params[1]
 	local target = element.params.target or OvaleCondition.defaultTarget
 	local action
 	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
 		actionUsable, actionShortcut, actionIsCurrent, actionEnable

 	if element.func == "spell" then
+		local spellId = element.params[1]
 		action = OvaleActionBar:GetForSpell(spellId)
 		if not OvaleSpellBook:IsKnownSpell(spellId) and not action then
 			Ovale:Logf("Spell %s not learnt", spellId)
@@ -611,6 +675,7 @@ function OvaleBestAction:GetActionInfo(element)
 		actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target)
 		actionCooldownStart, actionCooldownDuration, actionEnable = OvaleState:GetComputedSpellCD(spellId)

+		-- Verify that the spell may be cast given restrictions specified in SpellInfo().
 		local si = OvaleData.spellInfo[spellId]
 		if si then
 			if si.stance and not OvaleStance:IsStance(si.stance) then
@@ -645,9 +710,10 @@ function OvaleBestAction:GetActionInfo(element)
 		actionUsable = API_IsUsableSpell(spellId)

 	elseif element.func == "macro" then
-		action = OvaleActionBar:GetForMacro(element.params[1])
+		local macro = element.params[1]
+		action = OvaleActionBar:GetForMacro(macro)
 		if not action then
-			Ovale:Logf("Unknown macro %s", element.params[1])
+			Ovale:Logf("Unknown macro %s", macro)
 			return nil
 		end
 		actionTexture = API_GetActionTexture(action)
@@ -675,7 +741,8 @@ function OvaleBestAction:GetActionInfo(element)
 		actionUsable = (spellName ~= nil)

 	elseif element.func == "texture" then
-		actionTexture = "Interface\\Icons\\" .. element.params[1]
+		local texture = element.params[1]
+		actionTexture = "Interface\\Icons\\" .. texture
 		actionInRange = nil
 		actionCooldownStart = OvaleState.maintenant
 		actionCooldownDuration = 0
@@ -712,13 +779,13 @@ function OvaleBestAction:Compute(element)
 end

 function OvaleBestAction:ComputeBool(element)
-	local start, ending, _, newElement = self:Compute(element)
+	local timeSpan, _, newElement = self:Compute(element)
 	-- Match SimC: 0 is false, non-zero is true.
 	--	(https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators)
 	if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then
 		return nil
 	else
-		return start, ending
+		return timeSpan
 	end
 end
 --</public-static-methods>
diff --git a/OvaleCompile.lua b/OvaleCompile.lua
index dc51f8f..185fa56 100644
--- a/OvaleCompile.lua
+++ b/OvaleCompile.lua
@@ -24,6 +24,7 @@ local OvaleScore = Ovale.OvaleScore
 local OvaleScripts = Ovale.OvaleScripts
 local OvaleSpellBook = Ovale.OvaleSpellBook
 local OvaleStance = Ovale.OvaleStance
+local OvaleTimeSpan = Ovale.OvaleTimeSpan

 local ipairs = ipairs
 local pairs = pairs
@@ -42,6 +43,7 @@ local API_GetSpellInfo = GetSpellInfo

 local self_node = {}
 local self_pool = OvalePool("OvaleCompile_pool")
+local self_timeSpanPool = OvalePool("OvaleCompile_timeSpanPool")
 local self_defines = {}
 local self_customFunctions = {}
 local self_missingSpellList = {}
@@ -194,6 +196,7 @@ local function ParseNumber(dummy, value)
 	node.value = tonumber(value)
 	node.origin = 0
 	node.rate = 0
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return dummy..AddNode(node)
 end

@@ -240,6 +243,7 @@ local function ParseFunction(prefix, func, params)
 	end
 	node.func = func
 	node.params = paramList
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	local nodeName = AddNode(node)
 	self_functionCalls[func] = node

@@ -430,6 +434,7 @@ local function ParseIf(a, b)
 	node.type = "if"
 	node.a = self_node[tonumber(a)]
 	node.b = self_node[tonumber(b)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -438,6 +443,7 @@ local function ParseUnless(a, b)
 	node.type = "unless"
 	node.a = self_node[tonumber(a)]
 	node.b = self_node[tonumber(b)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -445,6 +451,7 @@ local function ParseWait(a)
 	local node = self_pool:Get()
 	node.type = "wait"
 	node.a = self_node[tonumber(a)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -453,6 +460,7 @@ local function ParseAnd(a,b)
 	node.type = "and"
 	node.a = self_node[tonumber(a)]
 	node.b = self_node[tonumber(b)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -460,6 +468,7 @@ local function ParseNot(a)
 	local node = self_pool:Get()
 	node.type = "not"
 	node.a = self_node[tonumber(a)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -468,6 +477,7 @@ local function ParseOr(a,b)
 	node.type = "or"
 	node.a = self_node[tonumber(a)]
 	node.b = self_node[tonumber(b)]
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -492,6 +502,7 @@ do
 		node.operator = op
 		node.a = self_node[tonumber(a)]
 		node.b = self_node[tonumber(b)]
+		node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 		return AddNode(node)
 	end
 end
@@ -513,6 +524,7 @@ local function ParseGroup(text)
 	local node = self_pool:Get()
 	node.type = "group"
 	node.nodes = nodes
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -559,6 +571,7 @@ local function ParseLua(text)
 	local node = self_pool:Get()
 	node.type = "lua"
 	node.lua = strsub(text, 2, strlen(text)-1)
+	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	return AddNode(node)
 end

@@ -604,7 +617,7 @@ local function ParseCommands(text)

 	local nodeId
 	if text then
-		nodeId = strmatch(text, "node(%d+)")
+		nodeId = tonumber(strmatch(text, "node(%d+)"))
 	end
 	if not nodeId then
 		Ovale:Print("no master node")
@@ -622,29 +635,32 @@ local function ParseCommands(text)
 end

 local function ParseAddFunction(name, params, text)
-	local nodeId = ParseCommands(text)
-	local node = self_pool:Get()
-	node.type = "customfunction"
-	node.name = name
-	node.params = ParseParameters(params)
-	node.a = self_node[tonumber(nodeId)]
-	if not TestConditions(node.params) then
-		return nil
+	local paramList = ParseParameters(params)
+	if TestConditions(paramList) then
+		local nodeId = ParseCommands(text)
+		if nodeId then
+			local node = self_pool:Get()
+			node.type = "customfunction"
+			node.name = name
+			node.params = paramList
+			node.a = self_node[nodeId]
+			node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
+			return AddNode(node)
+		end
 	end
-	return AddNode(node)
 end

 local function ParseAddIcon(params, text, secure)
-	-- On convertit le numéro de node en node
-	local masterNode = ParseCommands(text)
-	if not masterNode then return nil end
-	masterNode = self_node[tonumber(masterNode)]
-	masterNode.params = ParseParameters(params)
-	masterNode.secure = secure
-	if not TestConditions(masterNode.params) then
-		return nil
+	local paramList = ParseParameters(params)
+	if TestConditions(paramList) then
+		local masterNodeId = ParseCommands(text)
+		if masterNodeId then
+			local masterNode = self_node[masterNodeId]
+			masterNode.params = paramList
+			masterNode.secure = secure
+			return masterNode
+		end
 	end
-	return masterNode
 end

 local function ParseCanStopChannelling(text)
@@ -747,6 +763,7 @@ local function CompileScript(text)
 	-- Return all existing nodes to the node pool.
 	for i, node in pairs(self_node) do
 		self_node[i] = nil
+		self_timeSpanPool:Release(node.timeSpan)
 		self_pool:Release(node)
 	end
 	wipe(self_node)
diff --git a/OvaleCondition.lua b/OvaleCondition.lua
index cc1886b..250c8e5 100644
--- a/OvaleCondition.lua
+++ b/OvaleCondition.lua
@@ -30,15 +30,12 @@ local OvaleSpellDamage = Ovale.OvaleSpellDamage
 local OvaleStance = Ovale.OvaleStance
 local OvaleState = Ovale.OvaleState
 local OvaleSwing = Ovale.OvaleSwing
-local OvaleTimeSpan = Ovale.OvaleTimeSpan

 local floor = floor
 local pairs = pairs
 local select = select
 local tostring = tostring
 local wipe = table.wipe
-local Intersect = OvaleTimeSpan.Intersect
-local Measure = OvaleTimeSpan.Measure
 local API_GetBuildInfo = GetBuildInfo
 local API_GetItemCooldown = GetItemCooldown
 local API_GetItemCount = GetItemCount
@@ -174,7 +171,7 @@ local function TestOvaleValue(start, ending, value, origin, rate, comparator, li
 	if not value or not origin or not rate then
 		return nil
 	elseif not comparator then
-		if Measure(start, ending) > 0 then
+		if start < ending then
 			return start, ending, value, origin, rate
 		else
 			return 0, math.huge, 0, 0, 0
@@ -195,12 +192,16 @@ local function TestOvaleValue(start, ending, value, origin, rate, comparator, li
 			or (comparator == "atMost" and rate > 0)
 			or (comparator == "atLeast" and rate < 0)
 			or (comparator == "more" and rate < 0) then
-		return Intersect(start, ending, 0, (limit - value)/rate + origin)
+		local t = (limit - value)/rate + origin
+		ending = (ending < t) and ending or t
+		return start, ending
 	elseif (comparator == "less" and rate < 0)
 			or (comparator == "atMost" and rate < 0)
 			or (comparator == "atLeast" and rate > 0)
 			or (comparator == "more" and rate > 0) then
-		return Intersect(start, ending, (limit - value)/rate + origin, math.huge)
+		local t = (limit - value)/rate + origin
+		start = (start > t) and start or t
+		return start, math.huge
 	end
 	return nil
 end
diff --git a/OvaleFrame.lua b/OvaleFrame.lua
index 43e03fc..7bc6cdf 100644
--- a/OvaleFrame.lua
+++ b/OvaleFrame.lua
@@ -26,6 +26,7 @@ do
 	local Version = 7

 	local pairs = pairs
+	local tostring = tostring
 	local wipe = table.wipe
 	local API_CreateFrame = CreateFrame
 	local API_GetSpellInfo = GetSpellInfo
@@ -182,7 +183,8 @@ do
 			if forceRefresh or Ovale.refreshNeeded[target] or Ovale.refreshNeeded["player"] or Ovale.refreshNeeded["pet"] then
 				Ovale:Logf("****Master Node %d", k)
 				OvaleBestAction:StartNewAction()
-				local start, ending, priorite, element = OvaleBestAction:Compute(node)
+				local timeSpan, _, element = OvaleBestAction:Compute(node)
+				local start = timeSpan[1]
 				if start then
 					Ovale:Logf("Compute start = %f", start)
 				end
@@ -273,7 +275,8 @@ do
 								spellTarget = target
 							end
 							OvaleState:ApplySpell(spellId, start, start + castTime, nextCast, false, OvaleGUID:GetGUID(spellTarget))
-							start, ending, priorite, element = OvaleBestAction:Compute(node)
+							timeSpan, _, element = OvaleBestAction:Compute(node)
+							start = timeSpan[1]
 							icons[2]:Update(element, start, OvaleBestAction:GetActionInfo(element))
 						else
 							icons[2]:Update(element, nil)
diff --git a/OvaleTimeSpan.lua b/OvaleTimeSpan.lua
index f52e963..4e78cc4 100644
--- a/OvaleTimeSpan.lua
+++ b/OvaleTimeSpan.lua
@@ -13,92 +13,698 @@
 	Infinity is represented by math.huge.
 	Point sets are considered empty.
 	"nil" time spans are considered empty.
+
+	This module supports the following operations on time spans:
+		Complement
+		Union (code kindly contributed by Qrux)
+		Intersection
 --]]

 local _, Ovale = ...
 local OvaleTimeSpan = {}
 Ovale.OvaleTimeSpan = OvaleTimeSpan

+--<private-static-properties>
+--local debugprint = print
+local setmetatable = setmetatable
+local tconcat = table.concat
+local wipe = table.wipe
+--</private-static-properties>
+
+--<public-static-properties>
+OvaleTimeSpan.__index = OvaleTimeSpan
+do
+	-- Class constructor
+	setmetatable(OvaleTimeSpan, { __call = function(self, ...) return self:New(...) end })
+end
+--</public-static-properties>
+
+--<private-static-methods>
+local function CompareIntervals(startA, endA, startB, endB)
+	--debugprint(string.format("  comparing (%s, %s) with (%s, %s)", startA, endA, startB, endB))
+	if startA == startB and endA == endB then
+		-- same (0)
+		return 0
+	elseif startA < startB and endA >= startB and endA <= endB then
+		-- overlap, A comes-before B (-1)
+		return -1
+	elseif startB < startA and endB >= startA and endB <= endA then
+		-- overlap, B comes-before A (1)
+		return 1
+	elseif (startA == startB and endA > endB) or (startA < startB and endA == endB) or (startA < startB and endA > endB) then
+		-- A contains B (-2)
+		return -2
+	elseif (startB == startA and endB > endA) or (startB < startA and endB == endA) or (startB < startA and endB > endA) then
+		-- B contains A (3)
+		return 2
+	elseif endA <= startB then
+		-- A before B (-3)
+		return -3
+	elseif endB <= startA then
+		-- B before A (3)
+		return 3
+	end
+	-- Fail; unreachable (99)
+	return 99
+end
+--</private-static-methods>
+
 --<public-static-methods>
-function OvaleTimeSpan.Complement(startA, endA, atTime)
-	--[[
-		The complement of an interval is as follows:
-
-			COMPLEMENT{} = (0, math.huge)
-			COMPLEMENT(0, math.huge) = {}
-			COMPLEMENT(a, b) = (0, a) UNION (b, math.huge)
-
-		In the second case, it is the union of two intervals.  If the point of interest (atTime)
-		lies in the left interval, then return it.  Otherwise, return the right interval.
-	--]]
-	if not startA or not endA then
-		return 0, math.huge
-	elseif startA == 0 and endA == math.huge then
-		return nil
-	elseif 0 <= atTime and atTime < startA then
-		return 0, startA
+function OvaleTimeSpan:New(...)
+	local A = ...
+	if type(A) == "table" then
+		return setmetatable(A, self)
 	else
-		return endA, math.huge
+		return setmetatable({ ... }, self)
 	end
 end

-function OvaleTimeSpan.HasTime(start, ending, atTime)
-	if not start or not ending then
-		return nil
+function OvaleTimeSpan:__tostring()
+	if not self or #self == 0 then
+		return "empty set"
 	else
-		return start <= atTime and atTime <= ending
+		return tconcat(self, ", ")
 	end
 end

-function OvaleTimeSpan.Intersect(startA, endA, startB, endB)
-	-- If either (startA, endA) or (startB, endB) are the empty set, then return the empty set.
-	if not startA or not endA or not startB or not endB then
-		return nil
+function OvaleTimeSpan:Clone()
+	if not self then
+		return OvaleTimeSpan()
 	end
-	-- Swap around so that (startA, endA) comes "before" (startB, endB).
-	if startA > startB then
-		startA, startB = startB, startA
-		endA, endB = endB, endA
+	return self:CopyTo( {} )
+end
+
+function OvaleTimeSpan:CopyTo(result)
+	if not self then
+		return OvaleTimeSpan(result)
 	end
-	-- If the two time spans don't overlap, then return the empty set.
-	-- Otherwise, the take leftmost right endpoint.
-	if endA <= startB then
-		return nil
-	elseif endB < endA then
-		return startB, endB
-	else
-		return startB, endA
+	for i = 1, #self do
+		result[i] = self[i]
 	end
+	return OvaleTimeSpan(result)
 end

-function OvaleTimeSpan.Measure(startA, endA)
-	if not startA or not endA then
-		return 0
-	elseif startA >= endA then
-		return 0
-	else
-		return endA - startA
+function OvaleTimeSpan:Reset(template)
+	if self then
+		wipe(self)
+		if template then
+			return template:CopyTo(self)
+		else
+			return OvaleTimeSpan(self)
+		end
+	end
+end
+
+function OvaleTimeSpan:IsEmpty()
+	return (not self or #self == 0)
+end
+
+function OvaleTimeSpan:Equals(B)
+	local A = self
+	local countA = A and #A or 0
+	local countB = B and #B or 0
+
+	if countA ~= countB then
+		return false
+	end
+	for k = 1, countA do
+		if A[k] ~= B[k] then
+			return false
+		end
+	end
+	return true
+end
+
+function OvaleTimeSpan:HasTime(atTime)
+	local A = self
+	local countA = A and #A or 0
+	for i = 1, countA, 2 do
+		if A[i] <= atTime and atTime <= A[i+1] then
+			return true
+		end
 	end
+	return false
 end

-function OvaleTimeSpan.Union(startA, endA, startB, endB)
-	-- TODO: this assumes that (startA, endA) and (startB, endB) overlap.
-	-- If either (startA, endA) or (startB, endB) are the empty set, then return the other time span.
-	if not startA or not endA then
-		return startB, endB
-	elseif not startB or not endB then
-		return startA, endA
+function OvaleTimeSpan:Measure()
+	local A = self
+	local countA = A and #A or 0
+	local measure = 0
+	for i = 1, countA, 2 do
+		measure = measure + (A[i+1] - A[i])
 	end
-	-- Swap around so that (startA, endA) comes "before" (startB, endB).
-	if startA > startB then
-		startA, startB = startB, startA
-		endA, endB = endB, endA
+	return measure
+end
+
+function OvaleTimeSpan:Complement(result)
+	local A = self
+	local countA = A and #A or 0
+
+	result = result or {}
+
+	if countA == 0 then
+		result[1], result[2] = 0, math.huge
+		return OvaleTimeSpan(result)
 	end
-	-- Take the rightmost right endpoint.
-	if endA > endB then
-		return startA, endA
+
+	local i, k = 1, 1
+
+	if A[i] == 0 then
+		i = i + 1
 	else
-		return startA, endB
+		result[k] = 0
+		k = k + 1
+	end
+	while i < countA do
+		result[k] = A[i]
+		i, k = i + 1, k + 1
+	end
+	if A[i] < math.huge then
+		result[k], result[k+1] = A[i], math.huge
+	end
+	return OvaleTimeSpan(result)
+end
+
+function OvaleTimeSpan:IntersectInterval(startB, endB, result)
+	local A = self
+	local countA = A and #A or 0
+	result = result or {}
+
+	-- If A is empty, then the intersection is empty.
+	if countA == 0 or not startB or not endB then
+		return OvaleTimeSpan(result)
+	end
+
+	local i, k = 1, 1
+	while true do
+		if i > countA then
+			break
+		end
+
+		local startA, endA = A[i], A[i+1]
+		local compare = CompareIntervals(startA, endA, startB, endB)
+		if compare == 0 then
+			-- Same; output, exit.
+			result[k], result[k+1] = startA, endA
+			break
+		elseif compare == -1 then
+			-- Overlap; A comes before B, output, advance A.
+			result[k], result[k+1] = startB, endA
+			i, k = i + 2, k + 2
+		elseif compare == 1 then
+			-- Overlap; B comes before A, output, exit.
+			result[k], result[k+1] = startA, endB
+			break
+		elseif compare == -2 then
+			-- A contains B; output, exist.
+			result[k], result[k+1] = startB, endB
+			break
+		elseif compare == 2 then
+			-- B contains A; output, advance A.
+			result[k], result[k+1] = startA, endA
+			i, k = i + 2, k + 2
+		elseif compare == -3 then
+			-- A before B
+			i = i + 2
+		elseif compare == 3 then
+			-- B before A
+			break
+		end
+	end
+
+	return OvaleTimeSpan(result)
+end
+
+function OvaleTimeSpan:Intersect(B, result)
+	local A = self
+	local countA = A and #A or 0
+	local countB = B and #B or 0
+	result = result or {}
+
+	-- If either A or B are empty, then the intersection is empty.
+	if countA == 0 or countB == 0 then
+		return OvaleTimeSpan(result)
+	end
+
+	local i, j, k = 1, 1, 1
+	while true do
+		if i > countA or j > countB then
+			break
+		end
+
+		local startA, endA = A[i], A[i+1]
+		local startB, endB = B[j], B[j+1]
+
+		--debugprint(string.format("      A: (%s, %s)", tostring(startA), tostring(endA)))
+		--debugprint(string.format("      B: (%s, %s)", tostring(startB), tostring(endB)))
+
+		local compare = CompareIntervals(startA, endA, startB, endB)
+		--debugprint("  overlap?", compare)
+		if compare == 0 then
+			-- Same; output, advance both.
+			result[k], result[k+1] = startA, endA
+			i, j, k = i + 2, j + 2, k + 2
+			--debugprint("         ADV(A)")
+			--debugprint("         ADV(B)")
+		elseif compare == -1 then
+			-- Overlap; A comes before B, output, advance A.
+			result[k], result[k+1] = startB, endA
+			i, k = i + 2, k + 2
+			--debugprint("         ADV(A)")
+		elseif compare == 1 then
+			-- Overlap; B comes before A, output, advance B.
+			result[k], result[k+1] = startA, endB
+			j, k = j + 2, k + 2
+			--debugprint("         ADV(B)")
+		elseif compare == -2 then
+			-- A contains B; output, advance B.
+			result[k], result[k+1] = startB, endB
+			j, k = j + 2, k + 2
+			--debugprint("         ADV(B)")
+		elseif compare == 2 then
+			-- B contains A; output, advance A.
+			result[k], result[k+1] = startA, endA
+			i, k = i + 2, k + 2
+			--debugprint("         ADV(A)")
+		elseif compare == -3 then
+			-- A before B
+			i = i + 2
+			--debugprint("         ADV(A)")
+		elseif compare == 3 then
+			-- B before A
+			j = j + 2
+			--debugprint("         ADV(B)")
+		else
+		--debugprint("WTF--can't happen; ABORT NAO!")
+			i = i + 2
+			j = j + 2
+		end
+	end
+
+	return OvaleTimeSpan(result)
+end
+
+function OvaleTimeSpan:Union(B, result)
+	local A = self
+	local countA = A and #A or 0
+	local countB = B and #B or 0
+	result = result or {}
+
+	-- If either A or B are empty, then return the other one.
+	if countA == 0 and countB == 0 then
+		return OvaleTimeSpan(result)
+	elseif countA == 0 then
+		return not B and OvaleTimeSpan(result) or B:CopyTo(result)
+	elseif countB == 0 then
+		return not A and OvaleTimeSpan(result) or A:CopyTo(result)
+	end
+
+	local i, j, k = 1, 1, 1
+
+	local startTemp, endTemp = A[i], A[i+1]
+
+	local holdingA = true
+	local scanningA = false
+
+	while true do
+		local startA, endA, startB, endB
+
+		if i > countA and j > countB then
+			-- Write the final temp to output.
+			result[k], result[k+1] = startTemp, endTemp
+			break
+		end
+		if scanningA and i > countA then
+			-- Past the end of A; Flip-scan; Flip-hold.
+			holdingA = not holdingA
+			scanningA = not scanningA
+		else
+			-- Normal; not past the end of A.
+			startA, endA = A[i], A[i+1]
+		end
+		if not scanningA and j > countB then
+			-- Past the end of B; Flip-scan; Flip-hold.
+			holdingA = not holdingA
+			scanningA = not scanningA
+		else
+			-- Normal; not past the end of B.
+			startB, endB = B[j], B[j+1]
+		end
+
+		local startCurrent = scanningA and startA or startB
+		local endCurrent = scanningA and endA or endB
+
+		--debugprint(string.format("   temp: (%s, %s)", tostring(startTemp), tostring(endTemp)))
+        --debugprint(string.format("      A: (%s, %s)", tostring(startA), tostring(endA)))
+		--debugprint(string.format("      B: (%s, %s)", tostring(startB), tostring(endB)))
+		--debugprint(string.format("current: (%s, %s)", tostring(startCurrent), tostring(endCurrent)))
+		--debugprint("         holdA", holdingA)
+		--debugprint("         scanA", scanningA)
+
+		--[[
+			Comparing pairs (temp, current):
+
+			 0 is (2, 3) - (2, 3) (temp    equals        current) - Advance-scan.
+			-2 is (1, 5) - (2, 4) (temp    contains      current) - Advance-scan.
+
+			-1 is (1, 3) - (2, 5) (temp    starts-before current) - Update temp-end (to cur2); advance-scan.
+			 1 is (2, 5) - (1, 3) (current starts-before temp   ) - Update temp-start (to cur1); advance-scan.
+
+			 2 is (1, 5) - (2, 4) (current contains      temp   ) - Reset-temp (to cur); Flip-scan; Flip-hold.
+
+			-3 is (1, 2) - (3, 4) (temp    is-before     current) - Flip-scan; advance-cur.
+			 3 is (3, 4) - (1, 2) (current is-before     temp   ) - Reset-temp (to cur); Flip-scan; Flip-hold.
+		--]]
+
+		local compare = CompareIntervals(startTemp, endTemp, startCurrent, endCurrent)
+		--debugprint("  overlap?", compare)
+		if compare == 0 then
+			-- Skip.
+			if scanningA then i = i + 2 else j = j + 2 end
+		elseif compare == -2 then
+			-- Simplest cases; advance input-currently-being-scanned.
+			if scanningA then i = i + 2 else j = j + 2 end
+		elseif compare == -1 then
+			-- Update temp-END, advance.
+			endTemp = endCurrent
+			if scanningA then i = i + 2 else j = j + 2 end
+		elseif compare == 1 then
+			-- update temp-START, advance.
+			startTemp = startCurrent
+			if scanningA then i = i + 2 else j = j + 2 end
+		elseif compare == 2 then
+			-- We need to flip the side we're scanning (and holding), because the other side contains this side.
+			startTemp, endTemp = startCurrent, endCurrent
+			holdingA = not holdingA
+			scanningA = not scanningA
+			if scanningA then i = i + 2 else j = j + 2 end
+		elseif compare == -3 then
+			-- This (and 3) are the only situations where we capture the output.
+			--debugprint("    (-3) holdA", holdingA)
+			--debugprint("    (-3) scanA", scanningA)
+			if holdingA == scanningA then
+				result[k], result[k+1] = startTemp, endTemp
+				startTemp, endTemp = startCurrent, endCurrent
+				scanningA = not scanningA
+				k = k + 2
+			else
+				scanningA = not scanningA
+				if scanningA then
+					i = i + 2
+					--debugprint("         ADV(A)")
+				else
+					j = j + 2
+					--debugprint("         ADV(B)")
+				end
+			end
+		elseif compare == 3 then
+			-- This (and -3) are the only situations where we capture the output.
+			startTemp, endTemp = startCurrent, endCurrent
+			holdingA = not holdingA
+			scanningA = not scanningA
+		else
+			--debugprint("WTF--can't happen; ABORT NAO!")
+			i = i + 2
+			j = j + 2
+		end
 	end
+
+	return OvaleTimeSpan(result)
 end
 --</public-static-methods>
+
+do
+	local testFunction = {}
+
+	testFunction[#testFunction + 1] = function()
+		local A = OvaleTimeSpan(1, 2, 3, 4)
+		local atTime = 0
+		if A:HasTime(atTime) then
+			print(string.format("%s should not contain %s", tostring(A), tostring(atTime)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A = OvaleTimeSpan(1, 2, 3, 4)
+		local atTime = 1
+		if not A:HasTime(atTime) then
+			print(string.format("%s should contain %s", tostring(A), tostring(atTime)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 2, 3, 4)
+		local expected = OvaleTimeSpan(0, 1, 2, 3, 4, math.huge)
+		local result = A:Complement()
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan()
+		local expected = OvaleTimeSpan(0, math.huge)
+		local result = A:Complement()
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(0, math.huge)
+		local expected = OvaleTimeSpan()
+		local result = A:Complement()
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 2, 3, 4)
+		local B        = OvaleTimeSpan(2, 3, 4, 5)
+		local expected = OvaleTimeSpan(1, 5)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(2, 3, 4, 5)
+		local B        = OvaleTimeSpan(1, 2, 3, 4)
+		local expected = OvaleTimeSpan(1, 5)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 2)
+		local B        = OvaleTimeSpan(2, 3, 4, 5)
+		local expected = OvaleTimeSpan(1, 3, 4, 5)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(2, 3, 4, 5)
+		local B        = OvaleTimeSpan(1, 2)
+		local expected = OvaleTimeSpan(1, 3, 4, 5)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(2, 3, 4, 5)
+		local B        = OvaleTimeSpan(1, 4)
+		local expected = OvaleTimeSpan(1, 5)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 5, 6, 10, 15, 17, 20, 30, 99, 101)
+		local B        = OvaleTimeSpan(2, 3, 7, 11, 14, 18, 21, 29, 42, 47, 99, 101)
+		local expected = OvaleTimeSpan(1, 5, 6, 11, 14, 18, 20, 30, 42, 47, 99, 101)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 5, 6, 10, 15, 17, 20, 30, 99, 101)
+		local B        = OvaleTimeSpan(2, 3, 7, 11, 14, 18, 21, 29, 42, 47, 101, 105)
+		local expected = OvaleTimeSpan(1, 5, 6, 11, 14, 18, 20, 30, 42, 47, 99, 105)
+
+		local result = A:Union(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A = OvaleTimeSpan()
+		local B = OvaleTimeSpan()
+
+		local a = math.floor(math.random(1, 10))
+		local b = math.floor(math.random(1, 10))
+
+		for i = 1, 20 do
+			local da = math.floor(math.random(1, 10))
+			local db = math.floor(math.random(1, 10))
+			A[i], B[i] = a, b
+			a, b = a + da, b + db
+		end
+
+		--print(string.format("        A: %s", tostring(A)))
+		--print(string.format("        B: %s", tostring(B)))
+
+		local result = A:Union(B)
+		--print(string.format("A union B: %s", tostring(result)))
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan()
+		local B        = OvaleTimeSpan()
+		local expected = OvaleTimeSpan()
+
+		local result = A:Intersect(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 3)
+		local B        = OvaleTimeSpan(2, 4)
+		local expected = OvaleTimeSpan(2, 3)
+
+		local result = A:Intersect(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 3, 4, 6)
+		local B        = OvaleTimeSpan(2, 5)
+		local expected = OvaleTimeSpan(2, 3, 4, 5)
+
+		local result = A:Intersect(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(1, 5, 6, 10, 15, 17, 20, 30, 99, 101)
+		local B        = OvaleTimeSpan(2, 3, 7, 11, 14, 18, 21, 29, 42, 47, 99, 101)
+		local expected = OvaleTimeSpan(2, 3, 7, 10, 15, 17, 21, 29, 99, 101)
+
+		local result = A:Intersect(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A        = OvaleTimeSpan(2, math.huge)
+		local B        = OvaleTimeSpan(3, math.huge)
+		local expected = OvaleTimeSpan(3, math.huge)
+
+		local result = A:Intersect(B)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	testFunction[#testFunction + 1] = function()
+		local A            = OvaleTimeSpan(1, 3, 4, 6)
+		local startB, endB = 2, 5
+		local expected     = OvaleTimeSpan(2, 3, 4, 5)
+
+		local result = A:IntersectInterval(startB, endB)
+		if not result:Equals(expected) then
+			print(string.format("  result: %s", tostring(result)))
+			print(string.format("expected: %s", tostring(expected)))
+			return false
+		end
+		return true
+	end
+
+	local function TestDriver()
+		for i, func in ipairs(testFunction) do
+			local result = func()
+			local resultString = result and "true" or "FAILED!"
+			print(string.format("Test %d: %s", i, resultString))
+			if not result then
+				break
+			end
+		end
+	end
+	--TestDriver()
+end
diff --git a/compiler.pl b/compiler.pl
index 032da09..d7b1cd8 100644
--- a/compiler.pl
+++ b/compiler.pl
@@ -108,8 +108,10 @@ $sp{OvaleQueue}{NewDeque} = true;
 $sp{OvaleQueue}{RemoveFront} = true;

 $sp{OvaleTimeSpan}{Complement} = true;
+$sp{OvaleTimeSpan}{CopyTo} = true;
 $sp{OvaleTimeSpan}{HasTime} = true;
 $sp{OvaleTimeSpan}{Intersect} = true;
+$sp{OvaleTimeSpan}{IntersectInterval} = true;
 $sp{OvaleTimeSpan}{Measure} = true;
 $sp{OvaleTimeSpan}{Union} = true;