Module:User:Theknightwho/context

local class = require("Module:User:Theknightwho/middleclass")

local logic_and = { [0] = {[0] = 0, 0, 0, 0},	[1] = {[0] = 0, 1, 0, 1},	[2] = {[0] = 0, 0, 2, 2},	[3] = {[0] = 0, 1, 2, 3}, } local logic_or = { [0] = {[0] = 0, 1, 2, 3},	[1] = {[0] = 1, 1, 3, 3},	[2] = {[0] = 2, 3, 2, 3},	[3] = {[0] = 3, 3, 3, 3}, } local logic_xor = { [0] = {[0] = 0, 1, 2, 3},	[1] = {[0] = 1, 0, 3, 2},	[2] = {[0] = 2, 3, 0, 1},	[3] = {[0] = 3, 2, 1, 0}, }

local function comb(self, args, nargs, s, t)	local pow = 1 local ret = 0 -- We work in pairs of bits, even if the upper bit hasn't been assigned. for b = 0, math.floor(self._width / 2) * 2 + 1, 2 do		local c = s		for i = 1, nargs do			c = t[c][args[i] % 4] args[i] = math.floor(args[i] / 4) end ret = ret + c * pow pow = pow * 4 end return ret end

local function bnot(self, x)	return (-x - 1) % self._next_bit end

local function band(self, ...) return comb(self, {...}, select('#', ...), 3, logic_and) end

local function bor(self, ...) return comb(self, {...}, select('#', ...), 0, logic_or) end

local function bxor(self, ...) return comb(self, {...}, select('#', ...), 0, logic_xor) end

local Context = class("Context")

-- Given a list of contexts, returns true if all are set; otherwise returns false. Logical "and". function Context:has(...) local contexts = {} for _, context in ipairs{...} do		table.insert(contexts, self._contexts[context]) end return bor(self, self._context, unpack(contexts)) == self._context end

-- Given a list of contexts, returns true if at least one of them has been set; otherwise returns false. Logical "or". -- If `subcontext` is true, then aggregate contexts are subdivided into their subcontexts (i.e. it will return true if any subcontext is set). Otherwise, aggregate contexts are considered indivisible. function Context:any_of(subcontext, ...) if subcontext then local contexts = {} for _, context in ipairs{...} do			table.insert(contexts, self._contexts[context]) end return band(self, self._context, unpack(contexts)) > 0 else for _, context in ipairs{...} do			if bor(self, self._context, self._contexts[context]) == self._context then return true end end return false end end

-- Given a list of contexts, returns true if exactly one of them has been set; otherwise returns false. Similar to logical "xor". -- TODO: `subcontext` function Context:only_one_of(subcontext, ...) local found = false for _, context in ipairs{...} do		local check = bor(self, self._context, self._contexts[context]) == self._context if check and found then return false elseif check then found = true end end return not not found end

-- Given a list of contexts, returns the number of them which are currently set. Useful as part of a comparison, e.g. "no more than two of" can be checked as context:has_n( ... ) <= 2. function Context:has_n(...) local n = 0 for _, context in ipairs{...} do		if bor(self, self._context, self._contexts[context]) == self._context then n = n + 1 end end return n end

-- Given a list of contexts, returns a table of those which are currently set. function Context:which(...) local contexts = {} for _, context in ipairs{...} do		if bor(self, self._context, self._contexts[context]) == self._context then table.insert(contexts, context) end end return contexts end

-- Given a list of input contexts, sets them all to true. function Context:set(...) local contexts = {} for _, context in ipairs{...} do		table.insert(contexts, self._contexts[context]) end self._context = bor(self, self._context, unpack(contexts)) return self end

-- Given a list of input contexts, sets them all to false. function Context:unset(...) local contexts = {} for _, context in ipairs{...} do		table.insert(contexts, self._contexts[context]) end self._context = band(self, self._context, bnot(self, bor(self, unpack(contexts)))) return self end

-- Given a list of input contexts, toggles each of their boolean values. If a given context is listed more than once (e.g. as part of an aggregate context), these will not cancel each other out. function Context:toggle(...) local contexts = {} for _, context in ipairs{...} do		table.insert(contexts, self._contexts[context]) end contexts = bor(self, unpack(contexts)) self._context = bxor(self, self._context, contexts) return self end

-- Sets all contexts to false. -- Optionally takes a list of contexts to set. function Context:reset(...) self._context = 0 self:set(...) return self end

-- Given a list of new contexts, adds them to the list of possible contexts: -- Primary contexts are given as strings. -- Aggregate contexts (i.e. sets of other contexts) are given as tables, with the `name` key defining the name. e.g. {name = "AGG_CONTEXT1", "CONTEXT1", "CONTEXT2"} would generate the aggregate context "AGG_CONTEXT1", composed of "CONTEXT1" and "CONTEXT2". -- The list is processed in order, which means that it is possible to define new aggregate contexts using other new contexts given earlier in the list. -- Aggregate contexts can contain other aggregate contexts, but if they are being defined together, they must be given as separate items in the same list (i.e. don't try to input nested tables). function Context:add(...) for i, context in ipairs{...} do -- Aggregate contexts. if type(context) == "table" then if not context.name then error("New aggregate context has not been given a name.") elseif (self._primary[context.name] or self._aggregate[context.name]) and not self._removed[context.name] then error("Name \"" .. context.name .. "\" already in use by another context.") elseif self._removed[context.name] then error("Not allowed to re-use the name \"" .. context.name .. "\" for a new aggregate context, as it is the name of a removed primary context.") end local subcontexts = {} for _, subcontext in ipairs(context) do				table.insert(subcontexts, self._contexts[subcontext]) end self._aggregate[context.name] = bor(self, self._context, unpack(subcontexts)) -- Primary contexts. else if (self._primary[context] or self._aggregate[context]) and not self._removed[context] then error("Name \"" .. context .. "\" already in use by another context.") -- If a removed primary context is being re-added, then we can just reactivate it instead of widening the integer. elseif self._removed[context] == true then self._removed[context] = nil else self._primary[context] = self._next_bit self._width = self._width + 1 self._next_bit = self._next_bit * 2 end end end return self end

-- Given a list of contexts, removes them as possible contexts. If given an aggregate context, it will only remove the aggregate context itself; not its members. function Context:remove(...) for _, context in ipairs{...} do		if self._primary[context] then if self:has(context) then error("Cannot remove context \"" .. context .. "\", as it is currently set.") end for c, val in pairs(self._aggregate) do				if band(self, self._primary[context], val) > 0 then error("Cannot remove primary context \"" .. context .. "\" while it is still part of an aggregate context.") end end -- We note removed contexts in a separate table instead of setting them to nil, so as to retain compatibility with any saved context states. self._removed[context] = true -- Aggregate contexts can just be deleted. elseif self._aggregate[context] then self._aggregate[context] = nil -- Anything not found in either table will throw an error, due to the self._contexts metatable. elseif self._contexts[context] then end end return self end

-- Returns the internal context state as an integer. This can be used to reload the same state using `load`. function Context:save return self._context end

-- Sets the internal context state directly, given a saved context state. This will still work even if contexts have since been added to or removed from the context object. However, it will throw an error if the loaded state would set a context which has since been removed. function Context:load(val) for i, context in pairs(self._removed) do		if band(self, val, context) then error("Could not load context state, because the context object no longer has \"" .. context .. "\" as one of its available contexts. It must be re-enabled before loading this context state.") end end self._context = val return self end

-- Returns the internal context state as a table. function Context:getTable local contexts = self._primary for i, context in pairs(contexts) do		if not self._removed[i] then contexts[i] = band(self, self._context, context) == context else contexts[i] = nil end end return contexts end

function Context:initialize(...) self._context = 0 self._contexts = {} self._primary = {} self._aggregate = {} self._removed = {} self._width = 0 self._next_bit = 1 setmetatable(self._contexts, {		__index = function(t, k)			if self._primary[k] and not self._removed[k] then				return self._primary[k]			elseif self._aggregate[k] and not self._removed[k] then				return self._aggregate[k]			else				return error("Context object does not have \"" .. k .. "\" as one of its available contexts.")			end		end,		__newindex = function(t, k, v)			error("Not allowed to assign new key to _contexts directly.")		end}	) self:add(...) end

return Context