Quantcast

Implement time spans, which are continuous open intervals of time.

Johnny C. Lam [10-15-13 - 01:24]
Implement time spans, which are continuous open intervals of time.

Convert OvaleBestAction and OvaleCondition to use actual time spans
instead of start/ending/nil to represent times.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1045 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
Ovale.toc
OvaleBestAction.lua
OvaleCondition.lua
OvaleTimeSpan.lua
compiler.pl
diff --git a/Ovale.toc b/Ovale.toc
index 294f457..69042a2 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -44,6 +44,7 @@ OvaleRecount.lua
 OvaleSkada.lua
 OvaleSpellBook.lua
 OvaleStance.lua
+OvaleTimeSpan.lua
 #
 OvaleDamageTaken.lua
 OvalePaperDoll.lua
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index 5bfdec3..06b1fde 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -22,6 +22,7 @@ local OvalePower = Ovale.OvalePower
 local OvaleSpellBook = Ovale.OvaleSpellBook
 local OvaleStance = Ovale.OvaleStance
 local OvaleState = Ovale.OvaleState
+local OvaleTimeSpan = Ovale.OvaleTimeSpan

 local floor = math.floor
 local ipairs = ipairs
@@ -31,6 +32,11 @@ local select = select
 local strfind = string.find
 local tonumber = tonumber
 local tostring = tostring
+local Complement = OvaleTimeSpan.Complement
+local HasTime = OvaleTimeSpan.HasTime
+local Intersect = OvaleTimeSpan.Intersect
+local Measure = OvaleTimeSpan.Measure
+local Union = OvaleTimeSpan.Union
 local API_GetActionCooldown = GetActionCooldown
 local API_GetActionTexture = GetActionTexture
 local API_GetItemIcon = GetItemIcon
@@ -52,50 +58,6 @@ local self_serial = 0
 --</private-static-properties>

 --<private-static-methods>
-local function addTime(time1, duration)
-	if not time1 then
-		return nil
-	else
-		return time1 + duration
-	end
-end
-
-local function isBeforeEqual(time1, time2)
-	return time1 and (not time2 or time1<=time2)
-end
-
-local function isBefore(time1, time2)
-	return time1 and (not time2 or time1<time2)
-end
-
-local function isAfterEqual(time1, time2)
-	return not time1 or (time2 and time1>=time2)
-end
-
-local function isAfter(time1, time2)
-	return not time1 or (time2 and time1>time2)
-end
-
-local function minTime(time1, time2)
-	if isBefore(time1, time2) then
-		return time1
-	else
-		return time2
-	end
-end
-
-local function isBetween(checkTime, startTime, endTime)
-	return isBeforeEqual(startTime, checkTime) and isAfterEqual(endTime, checkTime)
-end
-
-local function maxTime(time1, time2)
-	if isAfter(time1, time2) then
-		return time1
-	else
-		return time2
-	end
-end
-
 local function PutValue(element, value, origin, rate)
 	if not element.result then
 		element.result = { type = "value" }
@@ -111,36 +73,16 @@ local function ComputeAnd(element, atTime)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
 	local startA, endA = self:ComputeBool(element.a, atTime)
-	if not startA then
-		Ovale:Logf("%s return nil [%d]", element.type, element.nodeId)
-		return nil
-	end
-	if startA == endA then
-		Ovale:Logf("%s return startA=endA [%d]", element.type, element.nodeId)
-		return nil
-	end
-	local startB, endB, prioriteB, elementB
-	if element.type == "if" then
-		startB, endB, prioriteB, elementB = self:Compute(element.b, atTime)
-	else
-		startB, endB, prioriteB, elementB = self:ComputeBool(element.b, atTime)
-	end
-	-- If the "then" clause is a "wait" node, then only wait if the conditions are true.
-	if elementB and elementB.wait and not isBetween(atTime, startA, endA) then
-		elementB.wait = nil
-	end
-	if isAfter(startB, endA) or isAfter(startA, endB) then
-		Ovale:Logf("%s return nil [%d]", element.type, element.nodeId)
+	-- 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
 	end
-	if isBefore(startB, startA) then
-		startB = startA
-	end
-	if isAfter(endB, endA) then
-		endB = endA
-	end
-	Ovale:Logf("%s return %s, %s [%d]", element.type, startB, endB, element.nodeId)
-	return startB, endB, prioriteB, elementB
+	local startB, endB = self:ComputeBool(element.b, atTime)
+	-- 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
 end

 local function ComputeArithmetic(element, atTime)
@@ -149,13 +91,9 @@ local function ComputeArithmetic(element, atTime)
 	local startB, endB, _, elementB = self:Compute(element.b, atTime)

 	-- Take intersection of (startA, endA) and (startB, endB)
-	if isBefore(startA, startB) then
-		startA = startB
-	end
-	if isAfter(endA, endB) then
-		endA = endB
-	end
-	if not isBefore(startA, endA) then
+	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
 	end

@@ -251,13 +189,8 @@ local function ComputeCompare(element, atTime)
 	local startB, endB, _, elementB = self:Compute(element.b, atTime)

 	-- Take intersection of (startA, endA) and (startB, endB)
-	if isBefore(startA, startB) then
-		startA = startB
-	end
-	if isAfter(endA, endB) then
-		endA = endB
-	end
-	if not isBefore(startA, endA) then
+	startA, endA = Intersect(startA, endA, startB, endB)
+	if Measure(startA, endA) == 0 then
 		return nil
 	end

@@ -273,8 +206,9 @@ local function ComputeCompare(element, atTime)
 	local x = elementB and elementB.value or 0
 	local y = elementB and elementB.origin or 0
 	local z = elementB and elementB.rate or 0
+	local operator = element.operator

-	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, element.operator, x, y, z, element.nodeId)
+	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)
@@ -286,34 +220,30 @@ local function ComputeCompare(element, atTime)
 	local A = a - b*c
 	local B = x - y*z
 	if c == z then
-		if (element.operator == "==" and A == B)
-				or (element.operator == "<" and A < B)
-				or (element.operator == "<=" and A <= B)
-				or (element.operator == ">" and A > B)
-				or (element.operator == ">=" and A >= B) then
-			return startA, endA
+		if not ((operator == "==" and A == B)
+				or (operator == "<" and A < B)
+				or (operator == "<=" and A <= B)
+				or (operator == ">" and A > B)
+				or (operator == ">=" and A >= B)) then
+			startA, endA = nil, nil
 		end
 	else
 		local t = (B - A)/(c - z)
-		if (c > z and element.operator == "<")
-				or (c > z and element.operator == "<=")
-				or (c < z and element.operator == ">")
-				or (c < z and element.operator == ">=") then
-			-- (startA, endA) intersect (-inf, t)
-			endA = minTime(endA, t)
-		end
-		if (c < z and element.operator == "<")
-				or (c < z and element.operator == "<=")
-				or (c > z and element.operator == ">")
-				or (c > z and element.operator == ">=") then
-			-- (startA, endA) intersect (t, inf)
-			startA = maxTime(startA, t)
+		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)
 		end
-		if isBefore(startA, endA) then
-			return startA, endA
+		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)
 		end
 	end
-	return nil
+	Ovale:Logf("compare %s returns %s, %s [%d]", operator, startA, endA, element.nodeId)
+	return startA, endA
 end

 local function ComputeCustomFunction(element, atTime)
@@ -364,7 +294,9 @@ local function ComputeFunction(element, atTime)
 		end

 		if actionEnable and actionEnable > 0 then
-			local start
+			local start, ending = 0, math.huge
+			local priority = element.params.priority or OVALE_DEFAULT_PRIORITY
+
 			if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then
 				start = actionCooldownDuration + actionCooldownStart
 			else
@@ -394,8 +326,7 @@ local function ComputeFunction(element, atTime)
 				end
 			end
 			Ovale:Logf("Action %s can start at %f", element.params[1], start)
-			local priority = element.params.priority or OVALE_DEFAULT_PRIORITY
-			return start, nil, priority, element
+			return start, ending, priority, element
 		else
 			Ovale:Logf("Action %s not enabled", element.params[1])
 		end
@@ -436,13 +367,9 @@ local function ComputeGroup(element, atTime)

 	for k, v in ipairs(element.nodes) do
 		local start, ending, priority, newElement = self:Compute(v, atTime)
-
-		if start and start < atTime then
-			start = atTime
-		end
-
-		if start and (not ending or start <= ending) then
-			-- The node has a valid time interval.
+		-- We only care about actions that are available at time t > atTime.
+		start, ending = Intersect(start, ending, atTime, math.huge)
+		if Measure(start, ending) > 0 then
 			local castTime
 			if newElement then
 				castTime = newElement.castTime
@@ -459,17 +386,17 @@ local function ComputeGroup(element, atTime)
 					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 previous one, then choose the
+					-- 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 and start - bestStart < 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 previous one, then choose the
+					-- 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
@@ -481,12 +408,11 @@ local function ComputeGroup(element, atTime)
 					end
 				else
 					-- If the spells have the same priority, then pick the one with an earlier cast time.
-					-- TODO: why have a 0.01 second threshold here?
 					if bestElement and bestElement.params and bestElement.params.wait then
 						if bestStart - start > bestElement.params.wait then
 							replace = true
 						end
-					elseif bestStart - start > 0.01 then
+					elseif bestStart > start then
 						replace = true
 					end
 				end
@@ -504,7 +430,7 @@ local function ComputeGroup(element, atTime)
 	end

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

@@ -513,107 +439,83 @@ local function ComputeGroup(element, atTime)
 		if bestElement.params then
 			id = bestElement.params[1]
 		end
-		Ovale:Logf("group best action %s remains %s, %s [%d]", id, bestStart, bestEnding, element.nodeId)
+		Ovale:Logf("group best action %s remains %f, %f [%d]", id, bestStart, bestEnding, element.nodeId)
 	else
-		Ovale:Logf("group no best action returns %s, %s [%d]", bestStart, bestEnding, element.nodeId)
+		Ovale:Logf("group no best action returns %f, %f [%d]", bestStart, bestEnding, element.nodeId)
 	end
 	return bestStart, bestEnding, bestPriority, bestElement
 end

-local function ComputeLua(element, atTime)
-	local ret = loadstring(element.lua)()
-	Ovale:Logf("lua %s", ret)
-	return 0, nil, OVALE_DEFAULT_PRIORITY, PutValue(element, ret, 0, 0)
-end
-
-local function ComputeNot(element, atTime)
+local function ComputeIf(element, atTime)
+	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
+
 	local startA, endA = self:ComputeBool(element.a, atTime)
-	--[[
-		NOT start < t < ending    ==>  0 < t < start  OR  ending < t < infinity
-		NOT start < t < infinity  ==>  0 < t < start
-		NOT nil                   ==>  0 < t < infinity
-	]]--
-	if startA and endA then
-		-- TODO: This is not quite right since we ignore 0 < t < startA.
-		return endA, nil
-	elseif startA then
-		return 0, startA
+	-- "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)
+	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
+	end
+
+	local startB, endB, priorityB, elementB = self:Compute(element.b, atTime)
+	-- 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
+		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
-		return 0, nil
+		Ovale:Logf("%s return %f, %f [%d]", element.type, startB, endB, element.nodeId)
 	end
+	return startB, endB, priorityB, elementB
 end

-local function ComputeOr(element, atTime)
-	Ovale:Log(element.type)
+local function ComputeLua(element, atTime)
+	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)
+end
+
+local function ComputeNot(element, atTime)
+	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
 	local startA, endA = self:ComputeBool(element.a, atTime)
-	local startB, endB = self:ComputeBool(element.b, atTime)
-
-	if isBefore(endA, atTime) then
-		return startB, endB
-	elseif isBefore(endB, atTime) then
-		return startA, endA
-	end
-	if isBefore(endA, startB) then
-		return startA, endA
-	elseif isBefore(endB, startA) then
-		return startB, endB
-	end
-	if isBefore(startA, startB) then
-		startB = startA
-	end
-	if isAfter(endA, endB) then
-		endB = endA
-	end
-	return startB, endB
+	startA, endA = Complement(startA, endA, atTime)
+	Ovale:Logf("%s returns %s, %s [%d]", element.type, startA, endA, element.nodeId)
+	return startA, endA
 end

-local function ComputeUnless(element, atTime)
+local function ComputeOr(element, atTime)
 	Ovale:Logf("%s [%d]", element.type, element.nodeId)
 	local self = OvaleBestAction
 	local startA, endA = self:ComputeBool(element.a, atTime)
-	local startB, endB, prioriteB, elementB = self:Compute(element.b, atTime)
-	-- If the "then" clause is a "wait" node, then only wait if the conditions are false.
-	if elementB and elementB.wait and isBetween(atTime, startA, endA) then
-		elementB.wait = nil
-	end
-	-- unless {t: startA < t < endA} then {t: startB < t < endB}
-	-- if not {t: startA < t < endA} then {t: startB < t < endB}
-	-- if {t: t < startA} or {t: t > endA} then {t: startB < t < endB}
-	-- {t: t < startA and startB < t < endB} or {t: startB < t < endB and endA < t}
-	if isBeforeEqual(startA, startB) and isAfterEqual(endA, endB) then
-		Ovale:Logf("%s return nil [%d]", element.type, element.nodeId)
-		return nil
-	end
-	if isAfterEqual(startA, startB) and isBefore(endA, endB) then
-		Ovale:Logf("%s return %s, %s [%d]", element.type, endA, endB, element.nodeId)
-		return endA, endB, prioriteB, elementB
-	end
-	if isAfter(startA, startB) and isBefore(startA, endB) then
-		endB = startA
-	end
-	if isAfter(endA, startB) and isBefore(endA, endB) then
-		startB = endA
-	end
-	Ovale:Logf("%s return %s, %s [%d]", element.type, startB, endB, element.nodeId)
-	return startB, endB, prioriteB, elementB
+	local startB, endB = self:ComputeBool(element.b, atTime)
+	-- 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
 end

 local function ComputeValue(element, atTime)
 	Ovale:Logf("value %s", element.value)
-	return 0, nil, OVALE_DEFAULT_PRIORITY, element
+	return 0, math.huge, OVALE_DEFAULT_PRIORITY, element
 end

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

@@ -626,11 +528,11 @@ local OVALE_COMPUTE_VISITOR =
 	["customfunction"] = ComputeCustomFunction,
 	["function"] = ComputeFunction,
 	["group"] = ComputeGroup,
-	["if"] = ComputeAnd,
+	["if"] = ComputeIf,
 	["lua"] = ComputeLua,
 	["not"] = ComputeNot,
 	["or"] = ComputeOr,
-	["unless"] = ComputeUnless,
+	["unless"] = ComputeIf,
 	["value"] = ComputeValue,
 	["wait"] = ComputeWait,
 }
@@ -649,16 +551,12 @@ function OvaleBestAction:GetActionInfo(element)
 	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

-	local target = element.params.target
-	if (not target) then
-		target = OvaleCondition.defaultTarget
-	end
-
-	if (element.func == "spell" ) then
+	if (element.func == "spell") then
 		action = OvaleActionBar:GetForSpell(spellId)
 		if not OvaleSpellBook:IsKnownSpell(spellId) and not action then
 			Ovale:Logf("Spell %s not learnt", spellId)
@@ -774,7 +672,7 @@ function OvaleBestAction:ComputeBool(element, atTime)
 	if element and element.type == "value" and element.value == 0 and element.rate == 0 then
 		return nil
 	else
-		return start, ending, priority, element
+		return start, ending
 	end
 end
 --</public-static-methods>
diff --git a/OvaleCondition.lua b/OvaleCondition.lua
index e0535cd..471841c 100644
--- a/OvaleCondition.lua
+++ b/OvaleCondition.lua
@@ -138,47 +138,24 @@ local function TimeWithHaste(time1, haste)
 	end
 end

--- Return time1 + duration.
-local function AddToTime(time1, duration)
-	if not time1 or not duration then
-		-- time1 or duration is infinite.
-		return nil
-	else
-		return time1 + duration
-	end
-end
-
--- Return time2 - time1
-local function DiffTime(time1, time2)
-	if not time1 then
-		-- time1 is infinite, so return the lowest possible time (zero).
-		return 0
-	elseif not time2 then
-		-- time2 is infinite.
-		return nil
-	else
-		return time2 - time1
-	end
-end
-
 local function Compare(a, comparison, b)
 	if not comparison then
-		return 0, nil, a, 0, 0 -- this is not a compare, returns the value a
+		return 0, math.huge, a, 0, 0 -- this is not a compare, returns the value a
 	elseif comparison == "more" then
-		if (not b or (a~=nil and a>b)) then
-			return 0
+		if not b or (a and a > b) then
+			return 0, math.huge
 		else
 			return nil
 		end
 	elseif comparison == "equal" then
 		if b == a then
-			return 0
+			return 0, math.huge
 		else
 			return nil
 		end
 	elseif comparison == "less" then
-		if (not a or (b~=nil and a<b)) then
-			return 0
+		if not a or (b and a < b) then
+			return 0, math.huge
 		else
 			return nil
 		end
@@ -188,15 +165,15 @@ local function Compare(a, comparison, b)
 end

 local function TestBoolean(a, condition)
-	if (condition == "yes" or not condition) then
-		if (a) then
-			return 0
+	if condition == "yes" or not condition then
+		if a then
+			return 0, math.huge
 		else
 			return nil
 		end
 	else
-		if (not a) then
-			return 0
+		if not a then
+			return 0, math.huge
 		else
 			return nil
 		end
@@ -207,25 +184,22 @@ local function TestValue(comparator, limit, value, atTime, rate)
 	if not value or not atTime then
 		return nil
 	elseif not comparator then
-		return 0, nil, value, atTime, rate
+		return 0, math.huge, value, atTime, rate
 	else
-		if rate == 0 then
-			if comparator == "more" then
-				if value > limit then return 0 else return nil end
-			elseif comparator == "less" then
-				if value < limit then return 0 else return nil end
-			else
-				Ovale:Errorf("Unknown operator %s", comparator)
-				return nil
-			end
+		local start, ending = 0, math.huge
+		if comparator == "more" and rate == 0 then
+			if value <= limit then return nil end
+		elseif comparator == "less" and rate == 0 then
+			if value >= limit then return nil end
 		elseif (comparator == "more" and rate > 0) or (comparator == "less" and rate < 0) then
-			return (limit - value) / rate + atTime
+			start = (limit - value)/rate + atTime
 		elseif (comparator == "more" and rate < 0) or (comparator == "less" and rate > 0) then
-			return 0, (limit - value) / rate + atTime
+			ending = (limit - value)/rate + atTime
 		else
 			Ovale:Errorf("Unknown operator %s", comparator)
 			return nil
 		end
+		return start, ending
 	end
 end

@@ -471,18 +445,16 @@ OvaleCondition.defaultTarget = "target"
 		or operations.

 	The endpoint of a time interval must be between 0 and infinity, where
-	infinity is represented by "nil".  Time is a value such as returned by
+	infinity is represented by math.huge.  Time is a value such as returned by
 	the API function GetTime().

 	Examples:

-	(1)	(0, nil) means the condition is always true.  This can be shortened
-		to just return 0.
+	(1)	(0, math.huge) means the condition is always true.

-	(2)	(nil, nil) means the condition is always false.  This can be shortened
-		to just return nil.
+	(2)	nil is the empty set and means the condition is always false.

-	(3)	(0, nil, constant, 0, 0) means the condition has a constant value.
+	(3)	(0, math.huge, constant, 0, 0) means the condition has a constant value.

 	(4)	(start, ending, ending - start, start, -1) means the condition has a
 		value of f(t) = ending - t, at time t between start and ending.  This
@@ -569,7 +541,7 @@ OvaleCondition.conditions.buffattackpower = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, attackPower, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffattackpower = OvaleCondition.conditions.buffattackpower
@@ -594,7 +566,7 @@ OvaleCondition.conditions.buffrangedattackpower = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, rangedAttackPower, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffrangedattackpower = OvaleCondition.conditions.buffrangedattackpower
@@ -618,7 +590,7 @@ OvaleCondition.conditions.buffcombopoints = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, comboPoints, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffcombopoints = OvaleCondition.conditions.buffcombopoints
@@ -644,7 +616,7 @@ OvaleCondition.conditions.buffdamagemultiplier = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, baseDamageMultiplier * damageMultiplier, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffdamagemultiplier = OvaleCondition.conditions.buffdamagemultiplier
@@ -674,7 +646,7 @@ OvaleCondition.conditions.buffmeleecritchance = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, critChance, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffmeleecritchance = OvaleCondition.conditions.buffmeleecritchance
@@ -705,7 +677,7 @@ OvaleCondition.conditions.buffrangedcritchance = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, critChance, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffrangedcritchance = OvaleCondition.conditions.buffrangedcritchance
@@ -735,7 +707,7 @@ OvaleCondition.conditions.buffspellcritchance = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, critChance, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffspellcritchance = OvaleCondition.conditions.buffspellcritchance
@@ -759,7 +731,7 @@ OvaleCondition.conditions.buffmastery = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, masteryEffect, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffmastery = OvaleCondition.conditions.buffmastery
@@ -783,7 +755,7 @@ OvaleCondition.conditions.buffspellpower = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, spellBonusDamage, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffspellpower = OvaleCondition.conditions.buffspellpower
@@ -807,7 +779,7 @@ OvaleCondition.conditions.buffspellhaste = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, spellHaste, start, 0
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffspellhaste = OvaleCondition.conditions.buffspellhaste
@@ -856,7 +828,8 @@ OvaleCondition.conditions.debuffcount = OvaleCondition.conditions.buffcount
 OvaleCondition.conditions.buffduration = function(condition)
 	local start, ending = GetAura(condition)
 	start = start or 0
-	return Compare(DiffTime(start, ending), condition[2], condition[3])
+	ending = ending or math.huge
+	return Compare(ending - start, condition[2], condition[3])
 end
 OvaleCondition.conditions.debuffduration = OvaleCondition.conditions.buffduration

@@ -887,10 +860,10 @@ OvaleCondition.conditions.buffexpires = function(condition)
 	local start, ending = GetAura(condition)
 	local timeBefore = TimeWithHaste(condition[2], condition.haste)
 	if not start then
-		ending = 0
+		return 0, math.huge
 	end
 	Ovale:Logf("timeBefore = %s, ending = %s", timeBefore, ending)
-	return AddToTime(ending, -timeBefore)
+	return ending - timeBefore, math.huge
 end
 OvaleCondition.conditions.debuffexpires = OvaleCondition.conditions.buffexpires

@@ -915,7 +888,7 @@ OvaleCondition.conditions.buffremains = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, ending - start, start, -1
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.debuffremains = OvaleCondition.conditions.buffremains
@@ -929,7 +902,7 @@ OvaleCondition.conditions.buffgain = function(condition)
 	Ovale:Error("not implemented")
 	if true then return nil end
 	local gain = select(4, GetAura(condition)) or 0
-	return 0, nil, 0, 0, 1
+	return 0, math.huge, 0, 0, 1
 end
 OvaleCondition.conditions.debuffgain = OvaleCondition.conditions.buffgain

@@ -962,7 +935,7 @@ OvaleCondition.conditions.buffpresent = function(condition)
 		return nil
 	end
 	local timeBefore = TimeWithHaste(condition[2], condition.haste)
-	return start, AddToTime(ending, -timeBefore)
+	return start, ending - timeBefore
 end
 OvaleCondition.conditions.debuffpresent = OvaleCondition.conditions.buffpresent

@@ -987,6 +960,7 @@ OvaleCondition.conditions.debuffpresent = OvaleCondition.conditions.buffpresent
 OvaleCondition.conditions.buffstacks = function(condition)
 	local start, ending, stacks = GetAura(condition)
 	start = start or 0
+	ending = ending or math.huge
 	stacks = stacks or 0
 	return start, ending, stacks, 0, 0
 end
@@ -1023,22 +997,16 @@ OvaleCondition.conditions.burningembers = function(condition)
 	return TestValue(condition[1], condition[2], OvaleState.state.burningembers, OvaleState.currentTime, OvaleState.powerRate.burningembers)
 end

-	-- Check if the player can cast (cooldown is down)
-	-- 1: spellId
-	-- returns: bool
+--- Check if the player can cast the given spell (not on cooldown).
+-- @name CanCast
+-- @paramsig boolean
+-- @param id The spell ID to check.
+-- @return True if the spell cast be cast; otherwise, false.
+
 OvaleCondition.conditions.cancast = function(condition)
 	local spellId = condition[1]
 	local actionCooldownStart, actionCooldownDuration = OvaleState:GetComputedSpellCD(spellId)
-	local startCast = actionCooldownStart + actionCooldownDuration
-	if startCast < OvaleState.currentTime then
-		startCast = OvaleState.currentTime
-	end
-	--TODO why + castTime?
-	local castTime
-	if spellId then
-		castTime = select(7, API_GetSpellInfo(spellId))
-	end
-	return startCast + castTime/1000
+	return actionCooldownStart + actionCooldownDuration, math.huge
 end

 --- Test if the target is casting the given spell.
@@ -1171,7 +1139,7 @@ OvaleCondition.conditions.checkboxoff = function(condition)
 			return nil
 		end
 	end
-	return 0
+	return 0, math.huge
 end

 --- Test if all of the listed checkboxes are on.
@@ -1191,7 +1159,7 @@ OvaleCondition.conditions.checkboxon = function(condition)
 			return nil
 		end
 	end
-	return 0
+	return 0, math.huge
 end

 --- Get the current amount of stored Chi for monks.
@@ -1225,8 +1193,8 @@ end
 -- if target.Class(PRIEST) Spell(cheap_shot)

 OvaleCondition.conditions.class = function(condition)
-	local loc, noloc = API_UnitClass(GetTarget(condition))
-	return TestBoolean(noloc == condition[1], condition[2])
+	local class, classToken = API_UnitClass(GetTarget(condition))
+	return TestBoolean(classToken == condition[1], condition[2])
 end

 --- Test whether the target's classification matches the given classification.
@@ -1247,17 +1215,17 @@ end
 OvaleCondition.conditions.classification = function(condition)
 	local classification
 	local target = GetTarget(condition)
-	if API_UnitLevel(target) == -1 then
+	if API_UnitLevel(target) < 0 then
 		classification = "worldboss"
 	else
-		classification = API_UnitClassification(target);
-		if (classification == "rareelite") then
+		classification = API_UnitClassification(target)
+		if classification == "rareelite" then
 			classification = "elite"
-		elseif (classification == "rare") then
+		elseif classification == "rare" then
 			classification = "normal"
 		end
 	end
-	return TestBoolean(condition[1]==classification, condition[2])
+	return TestBoolean(classification == condition[1], condition[2])
 end

 --- Get the number of combo points on the currently selected target for a feral druid or a rogue.
@@ -1311,7 +1279,8 @@ end
 --     Spell(hibernate)

 OvaleCondition.conditions.creaturefamily = function(condition)
-	return TestBoolean(API_UnitCreatureFamily(GetTarget(condition)) == LBCT[condition[1]], condition[2])
+	local family = API_UnitCreatureFamily(GetTarget(condition))
+	return TestBoolean(family == LBCT[condition[1]], condition[2])
 end

 --- Test if the target is any of the listed creature types.
@@ -1330,9 +1299,9 @@ end

 OvaleCondition.conditions.creaturetype = function(condition)
 	local creatureType = API_UnitCreatureType(GetTarget(condition))
-	for _,v in pairs(condition) do
-		if (creatureType == LBCT[v]) then
-			return 0
+	for _, v in pairs(condition) do
+		if creatureType == LBCT[v] then
+			return 0, math.huge
 		end
 	end
 	return nil
@@ -1352,7 +1321,7 @@ OvaleCondition.conditions.critdamage = function(condition)
 	-- TODO: Need to account for increased crit effect from meta-gems.
 	local critFactor = 2
 	local start, ending, value, origin, rate = OvaleCondition.conditions.damage(condition)
-	return start, ending, critFactor * value, critFactor * origin, critFactor * rate
+	return start, ending, critFactor * value, origin, critFactor * rate
 end

 --- Get the current estimated damage of a spell on the target.
@@ -1377,7 +1346,7 @@ OvaleCondition.conditions.damage = function(condition)
 	local spellId = condition[1]
 	local value, origin, rate = ComputeFunctionParam(spellId, "damage")
 	if value then
-		return 0, nil, value, origin, rate
+		return 0, math.huge, value, origin, rate
 	else
 		local ap = OvalePaperDoll.stat.attackPower
 		local sp = OvalePaperDoll.stat.spellBonusDamage
@@ -1385,7 +1354,7 @@ OvaleCondition.conditions.damage = function(condition)
 		local oh = OvalePaperDoll.stat.offHandWeaponDamage
 		local bdm = OvalePaperDoll.stat.baseDamageMultiplier
 		local dm = OvaleState:GetDamageMultiplier(spellId)
-		return 0, nil, OvaleData:GetDamage(spellId, ap, sp, mh, oh, combo) * bdm * dm, 0, 0
+		return 0, math.huge, OvaleData:GetDamage(spellId, ap, sp, mh, oh, combo) * bdm * dm, 0, 0
 	end
 end

@@ -1404,7 +1373,7 @@ OvaleCondition.conditions.damagemultiplier = function(condition)
 	local spellId = condition[1]
 	local bdm = OvalePaperDoll.stat.baseDamageMultiplier
 	local dm = OvaleState:GetDamageMultiplier(spellId)
-	return 0, nil, bdm * dm, 0, 0
+	return 0, math.huge, bdm * dm, 0, 0
 end

 --- Get the damage taken by the player in the previous time interval.
@@ -1424,7 +1393,7 @@ OvaleCondition.conditions.damagetaken = function(condition)
 	if interval > 0 then
 		damage = OvaleDamageTaken:GetRecentDamage(interval)
 	end
-	return 0, nil, damage, 0, 0
+	return 0, math.huge, damage, 0, 0
 end
 OvaleCondition.conditions.incomingdamage = OvaleCondition.conditions.damagetaken

@@ -1867,7 +1836,7 @@ end

 OvaleCondition.conditions.itemcooldown = function(condition)
 	local actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(condition[1])
-	return 0, nil, actionCooldownDuration, actionCooldownStart, -1
+	return 0, math.huge, actionCooldownDuration, actionCooldownStart, -1
 end

 --- Get the current number of the given item in the player's inventory.
@@ -2085,7 +2054,7 @@ OvaleCondition.conditions.lastestimateddamage = function(condition)
 	local combo = OvaleFuture:GetLastSpellInfo(guid, spellId, "comboPoints") or 1
 	local bdm = OvaleFuture:GetLastSpellInfo(guid, spellId, "baseDamageMultiplier") or 1
 	local dm = OvaleFuture:GetLastSpellInfo(guid, spellId, "damageMultiplier") or 1
-	return 0, nil, OvaleData:GetDamage(spellId, ap, sp, mh, oh, combo) * bdm * dm, 0, 0
+	return 0, math.huge, OvaleData:GetDamage(spellId, ap, sp, mh, oh, combo) * bdm * dm, 0, 0
 end
 OvaleCondition.conditions.lastspellestimateddamage = OvaleCondition.conditions.lastestimateddamage

@@ -2305,7 +2274,7 @@ OvaleCondition.conditions.lastspellrangedcritchance = OvaleCondition.conditions.
 -- @see NextSwing

 OvaleCondition.conditions.lastswing = function(condition)
-	return 0, nil, 0, OvaleSwing:GetLast(condition[1]), 1
+	return 0, math.huge, 0, OvaleSwing:GetLast(condition[1]), 1
 end

 --- Get the most recent estimate of roundtrip latency in milliseconds.
@@ -2320,7 +2289,7 @@ end
 -- if Latency(more 1000) Spell(sinister_strike)

 OvaleCondition.conditions.latency = function(condition)
-	return 0, nil, OvaleLatency:GetLatency() * 1000, 0, 0
+	return 0, math.huge, OvaleLatency:GetLatency() * 1000, 0, 0
 end

 --- Get the level of the target.
@@ -2358,9 +2327,9 @@ end
 -- if List(opt_curse coe) Spell(curse_of_the_elements)

 OvaleCondition.conditions.list = function(condition)
-	if (condition[1]) then
-		if (Ovale:GetListValue(condition[1]) == condition[2]) then
-			return 0
+	if condition[1] then
+		if Ovale:GetListValue(condition[1]) == condition[2] then
+			return 0, math.huge
 		end
 	end
 	return nil
@@ -2692,7 +2661,7 @@ end
 -- @see LastSwing

 OvaleCondition.conditions.nextswing = function(condition)
-	return 0, nil, 0, OvaleSwing:GetNext(condition[1]), 0, -1
+	return 0, math.huge, OvaleSwing:GetNext(condition[1]), 0, -1
 end

 --- Get the number of seconds until the next tick of a periodic aura on the target.
@@ -2716,7 +2685,7 @@ OvaleCondition.conditions.nexttick = function(condition)
 		while ending - tick > OvaleState.currentTime do
 			ending = ending - tick
 		end
-		return 0, nil, 0, ending, -1
+		return 0, math.huge, 0, ending, -1
 	end
 	return nil
 end
@@ -2740,10 +2709,10 @@ OvaleCondition.conditions.otherdebuffexpires = function(condition)
 	local start, ending = GetAuraOnAnyTarget(condition, "target")
 	local timeBefore = TimeWithHaste(condition[2], condition.haste)
 	if not start then
-		ending = 0
+		return nil
 	end
 	Ovale:Logf("timeBefore = %s, ending = %s", timeBefore, ending)
-	return AddToTime(ending, -timeBefore)
+	return ending - timeBefore, math.huge
 end
 OvaleCondition.conditions.otherbuffexpires = OvaleCondition.conditions.otherdebuffexpires

@@ -2768,7 +2737,7 @@ OvaleCondition.conditions.otherdebuffpresent = function(condition)
 		return nil
 	end
 	local timeBefore = TimeWithHaste(condition[2], condition.haste)
-	return start, AddToTime(ending, -timeBefore)
+	return start, ending - timeBefore
 end
 OvaleCondition.conditions.otherbuffpresent = OvaleCondition.conditions.otherdebuffpresent

@@ -2790,7 +2759,7 @@ OvaleCondition.conditions.otherdebuffremains = function(condition)
 	if start and ending and start <= ending then
 		return start, ending, ending - start, start, -1
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.otherbuffremains = OvaleCondition.conditions.otherdebuffremains
@@ -2807,7 +2776,7 @@ OvaleCondition.conditions.otherbuffremains = OvaleCondition.conditions.otherdebu

 OvaleCondition.conditions.powercost = function(condition)
 	local cost = select(4, API_GetSpellInfo(condition[1])) or 0
-	return 0, nil, cost, 0, 0
+	return 0, math.huge, cost, 0, 0
 end
 OvaleCondition.conditions.energycost = OvaleCondition.conditions.powercost
 OvaleCondition.conditions.focuscost = OvaleCondition.conditions.powercost
@@ -2966,7 +2935,7 @@ OvaleCondition.conditions.remainingcasttime = function(condition)
 	if not endTime then
 		return nil
 	end
-	return 0, nil, 0, endTime/1000, -1
+	return 0, math.huge, 0, endTime/1000, -1
 end

 --- Test if the current rune count meets the minimum rune requirements set out in the parameters.
@@ -3003,7 +2972,7 @@ end
 --     Spell(obliterate)

 OvaleCondition.conditions.runecount = function(condition)
-	return 0, nil, GetRuneCount(condition[1], condition.death)
+	return 0, math.huge, GetRuneCount(condition[1], condition.death)
 end

 --- Get the number of seconds before the rune conditions are met.
@@ -3031,7 +3000,7 @@ OvaleCondition.conditions.runescooldown = function(condition)
 	if ret < OvaleState.maintenant then
 		ret = OvaleState.maintenant
 	end
-	return 0, nil, 0, ret, -1
+	return 0, math.huge, 0, ret, -1
 end

 --- Get the current amount of runic power for death knights.
@@ -3194,9 +3163,9 @@ OvaleCondition.spellbookConditions.spellcharges = true
 OvaleCondition.conditions.spellchargecooldown = function(condition)
 	local charges, maxCharges, cooldownStart, cooldownDuration = API_GetSpellCharges(condition[1])
 	if charges < maxCharges then
-		return 0, nil, cooldownDuration, cooldownStart, -1
+		return 0, math.huge, cooldownDuration, cooldownStart, -1
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.spellbookConditions.spellchargecooldown = true
@@ -3215,15 +3184,15 @@ OvaleCondition.conditions.spellcooldown = function(condition)
 	if type(spellId) == "string" then
 		local sharedCd = OvaleState.state.cd[spellId]
 		if sharedCd then
-			return 0, nil, sharedCd.duration, sharedCd.start, -1
+			return 0, math.huge, sharedCd.duration, sharedCd.start, -1
 		else
 			return nil
 		end
 	elseif not OvaleSpellBook:IsKnownSpell(spellId) then
-		return 0, nil, 0, OvaleState.currentTime + 3600, -1
+		return 0, math.huge, 0, OvaleState.currentTime + 3600, -1
 	else
 		local actionCooldownStart, actionCooldownDuration, actionEnable = OvaleState:GetComputedSpellCD(spellId)
-		return 0, nil, actionCooldownDuration, actionCooldownStart, -1
+		return 0, math.huge, actionCooldownDuration, actionCooldownStart, -1
 	end
 end
 -- OvaleCondition.spellbookConditions.spellcooldown = true / may be a sharedcd
@@ -3244,7 +3213,7 @@ OvaleCondition.conditions.spelldata = function(condition)
 	if si then
 		local ret = si[condition[2]]
 		if ret then
-			return 0, nil, ret, 0, 0
+			return 0, math.huge, ret, 0, 0
 		end
 	end
 	return nil
@@ -3293,7 +3262,7 @@ OvaleCondition.conditions.staggerremains = function(condition)
 		local stagger = API_UnitStagger(target)
 		return start, ending, 0, ending, -1 * stagger / (ending - start)
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end

@@ -3307,7 +3276,7 @@ end

 OvaleCondition.conditions.stance = function(condition)
 	if OvaleStance:IsStance(condition[1]) then
-		return 0
+		return 0, math.huge
 	else
 		return nil
 	end
@@ -3448,7 +3417,7 @@ end
 -- @return The number of added ticks.

 OvaleCondition.conditions.ticksadded = function(condition)
-	return 0, nil, 0, 0, 0
+	return 0, math.huge, 0, 0, 0
 end

 --- Get the remaining number of ticks of a periodic aura on a target.
@@ -3472,9 +3441,9 @@ OvaleCondition.conditions.ticksremain = function(condition)
 	local start, ending = GetAura(condition, self_auraFound)
 	local tick = self_auraFound.tick
 	if ending and tick and tick > 0 then
-		return 0, nil, 1, ending, -1/tick
+		return 0, math.huge, 1, ending, -1/tick
 	end
-	return 0, nil, 0, 0, 0
+	return 0, math.huge, 0, 0, 0
 end

 --- Get the number of seconds between ticks of a periodic aura on a target.
@@ -3534,7 +3503,7 @@ end

 OvaleCondition.conditions.timetodie = function(condition)
 	local timeToDie = TimeToDie(GetTarget(condition))
-	return 0, nil, timeToDie, OvaleState.maintenant, -1
+	return 0, math.huge, timeToDie, OvaleState.maintenant, -1
 end
 OvaleCondition.conditions.deadin = OvaleCondition.conditions.timetodie

@@ -3556,9 +3525,9 @@ OvaleCondition.conditions.timetohealthpercent = function(condition)
 	local healthPercent = health / maxHealth * 100
 	if healthPercent >= percent then
 		local t = timeToDie * (healthPercent - percent) / healthPercent
-		return 0, nil, t, OvaleState.maintenant, -1
+		return 0, math.huge, t, OvaleState.maintenant, -1
 	end
-	return 0, nil, 0, 0, 0
+	return 0, math.huge, 0, 0, 0
 end
 OvaleCondition.conditions.timetolifepercent = OvaleCondition.conditions.timetohealthpercent

@@ -3578,12 +3547,12 @@ OvaleCondition.conditions.timetopowerfor = function(condition)
 	if currentPower < cost then
 		if powerRate > 0 then
 			local t = OvaleState.currentTime + (cost - currentPower) / powerRate
-			return 0, nil, 0, t, -1
+			return 0, math.huge, 0, t, -1
 		else
-			return 0, nil, OvaleState.currentTime + 3600, 0, 0
+			return 0, math.huge, OvaleState.currentTime + 3600, 0, 0
 		end
 	else
-		return 0, nil, 0, 0, 0
+		return 0, math.huge, 0, 0, 0
 	end
 end
 OvaleCondition.conditions.timetoenergyfor = OvaleCondition.conditions.timetopowerfor
@@ -3603,7 +3572,7 @@ OvaleCondition.spellbookConditions.timetopowerfor = true
 OvaleCondition.conditions.timetomaxenergy = function(condition)
 	local maxEnergy = OvalePower.maxPower.energy or 0
 	local t = OvaleState.currentTime + (maxEnergy - OvaleState.state.energy) / OvaleState.powerRate.energy
-	return 0, nil, 0, t, -1
+	return 0, math.huge, 0, t, -1
 end

 --- Get the time scaled by the specified haste type, defaulting to spell haste.
@@ -3621,7 +3590,7 @@ end

 OvaleCondition.conditions.timewithhaste = function(condition)
 	haste = condition.haste or "spell"
-	return 0, nil, TimeWithHaste(condition[1], haste), 0, 0
+	return 0, math.huge, TimeWithHaste(condition[1], haste), 0, 0
 end

 --- Test if the totem for shamans, the ghoul for death knights, or the statue for monks has expired.
@@ -3640,18 +3609,19 @@ end
 -- if TotemPresent(water totem=healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)

 OvaleCondition.conditions.totemexpires = function(condition)
-	if type(condition[1]) ~= "number" then
-		condition[1] = OVALE_TOTEMTYPE[condition[1]]
+	local totemId = condition[1]
+	local seconds = condition[2] or 0
+	if type(totemId) ~= "number" then
+		totemId = OVALE_TOTEMTYPE[totemId]
 	end
-
-	local haveTotem, totemName, startTime, duration = API_GetTotemInfo(condition[1])
+	local haveTotem, totemName, startTime, duration = API_GetTotemInfo(totemId)
 	if not startTime then
-		return 0
+		return 0, math.huge
 	end
 	if condition.totem and OvaleSpellBook:GetSpellName(condition.totem) ~= totemName then
-		return 0
+		return 0, math.huge
 	end
-	return AddToTime(startTime + duration, -(condition[2] or 0))
+	return startTime + duration - seconds, math.huge
 end

 --- Test if the totem for shamans, the ghoul for death knights, or the statue for monks is present.
@@ -3668,11 +3638,11 @@ end
 -- if TotemPresent(water totem=healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)

 OvaleCondition.conditions.totempresent = function(condition)
-	if type(condition[1]) ~= "number" then
-		condition[1] = OVALE_TOTEMTYPE[condition[1]]
+	local totemId = condition[1]
+	if type(totemId) ~= "number" then
+		totemId = OVALE_TOTEMTYPE[totemId]
 	end
-
-	local haveTotem, totemName, startTime, duration = API_GetTotemInfo(condition[1])
+	local haveTotem, totemName, startTime, duration = API_GetTotemInfo(totemId)
 	if not startTime then
 		return nil
 	end
@@ -3705,7 +3675,7 @@ end
 -- @return A boolean value.

 OvaleCondition.conditions["true"] = function(condition)
-	return 0, nil
+	return TestBoolean(true)
 end

 --- Test if the weapon imbue on the given weapon has expired or will expire after a given number of seconds.
@@ -3723,23 +3693,23 @@ OvaleCondition.conditions.weaponenchantexpires = function(condition)
 	local hasMainHandEnchant, mainHandExpiration, mainHandCharges, hasOffHandEnchant, offHandExpiration, offHandCharges = API_GetWeaponEnchantInfo()
 	if (condition[1] == "mainhand") then
 		if (not hasMainHandEnchant) then
-			return 0
+			return 0, math.huge
 		end
 		mainHandExpiration = mainHandExpiration/1000
 		if ((condition[2] or 0) >= mainHandExpiration) then
-			return 0
+			return 0, math.huge
 		else
-			return OvaleState.maintenant + mainHandExpiration - (condition[2] or 60)
+			return OvaleState.maintenant + mainHandExpiration - (condition[2] or 60), math.huge
 		end
 	else
 		if (not hasOffHandEnchant) then
-			return 0
+			return 0, math.huge
 		end
 		offHandExpiration = offHandExpiration/1000
 		if ((condition[2] or 0) >= offHandExpiration) then
-			return 0
+			return 0, math.huge
 		else
-			return OvaleState.maintenant + offHandExpiration - (condition[2] or 60)
+			return OvaleState.maintenant + offHandExpiration - (condition[2] or 60), math.huge
 		end
 	end
 end
@@ -3764,6 +3734,6 @@ OvaleCondition.conditions.weapondamage = function(condition)
 	else -- if hand == "mainhand" or hand == "main" then
 		damage = OvalePaperDoll.stat.mainHandWeaponDamage
 	end
-	return 0, nil, damage, 0, 0
+	return 0, math.huge, damage, 0, 0
 end
 --</public-static-properties>
diff --git a/OvaleTimeSpan.lua b/OvaleTimeSpan.lua
new file mode 100644
index 0000000..acb1e19
--- /dev/null
+++ b/OvaleTimeSpan.lua
@@ -0,0 +1,96 @@
+--[[--------------------------------------------------------------------
+    Ovale Spell Priority
+    Copyright (C) 2013 Johnny C. Lam
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License in the LICENSE
+    file accompanying this program.
+--]]--------------------------------------------------------------------
+
+--[[
+	Time spans are continuous open intervals (start, ending) that are subsets of (0, infinity).
+
+	Infinity is represented by math.huge.
+	Point sets are considered empty.
+	"nil" time spans are considered empty.
+--]]
+
+local _, Ovale = ...
+local OvaleTimeSpan = {}
+Ovale.OvaleTimeSpan = OvaleTimeSpan
+
+--<public-static-methods>
+function OvaleTimeSpan.Complement(startA, endA, atTime)
+	-- The complement of an interval is actually 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 0 < startA and atTime <= startA then
+		return 0, startA
+	else
+		return endA, math.huge
+	end
+end
+
+function OvaleTimeSpan.HasTime(start, ending, atTime)
+	if not start or not ending then
+		return nil
+	else
+		return start <= atTime and atTime <= ending
+	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
+	end
+	-- If the two time spans don't overlap, then return the empty set.
+	if endB <= startA or endA <= startB then
+		return nil
+	end
+	-- Take the rightmost left endpoint.
+	if startA < startB then
+		startA = startB
+	end
+	-- Take the leftmost right endpoint.
+	if endA > endB then
+		endA = endB
+	end
+	-- Sanity check that it's a valid time span, otherwise, return the empty set.
+	if startA >= endA then
+		return nil
+	end
+	return startA, endA
+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
+	end
+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
+	end
+	-- Take the leftmost left endpoint.
+	if startA > startB then
+		startA = startB
+	end
+	-- Take the rightmost right endpoint.
+	if endA < endB then
+		endA = endB
+	end
+	return startA, endA
+end
+--</public-static-methods>
diff --git a/compiler.pl b/compiler.pl
index f843410..23315b3 100644
--- a/compiler.pl
+++ b/compiler.pl
@@ -98,6 +98,7 @@ $sp{Ovale}{OvalePool} = true;
 $sp{Ovale}{OvalePoolGC} = true;
 $sp{Ovale}{OvaleSkada} = true;
 $sp{Ovale}{OvaleState} = true;
+$sp{Ovale}{OvaleTimeSpan} = true;

 $sp{OvaleQueue}{Front} = true;
 $sp{OvaleQueue}{FrontToBackIterator} = true;
@@ -106,6 +107,12 @@ $sp{OvaleQueue}{InsertFront} = true;
 $sp{OvaleQueue}{NewDeque} = true;
 $sp{OvaleQueue}{RemoveFront} = true;

+$sp{OvaleTimeSpan}{Complement} = true;
+$sp{OvaleTimeSpan}{HasTime} = true;
+$sp{OvaleTimeSpan}{Intersect} = true;
+$sp{OvaleTimeSpan}{Measure} = true;
+$sp{OvaleTimeSpan}{Union} = true;
+
 opendir(DIR, ".");
 while (defined($r = readdir(DIR)))
 {