Module:Set

--

-- Library for building and manipulating sets. -- -- @author User:DarthKitty -- @version 0.1.0 -- -- @TODO Consider using `Dev:Inspect` for `Set:tostring`.

local p = {} local checkType = require("libraryUtil").checkType

-- Pass-by-reference placeholders for values that cannot be used as table keys. -- We use functions instead of tables, since the latter are mutable.

local placeholders = { ["nil"] = function end, ["NaN"] = function end, }

-- Wraps a given value for use as a table key. -- -- @param {*} value --    The value to wrap. -- @returns {*} --    The wrapped value.

local function encode(value) -- Per IEEE 754, NaN is the only value which does not equal itself. Tables -- cannot generate false-positives, since the `__eq` metamethod is not -- called if the values being compared are primitively equal. local isNaN = value ~= value

if value == nil then return placeholders["nil"] elseif isNaN then return placeholders["NaN"] else return value end end

-- Unwraps a given value after being used as a table key. -- -- @param {*} value --    The value to unwrap. -- @returns {*} --    The unwrapped value.

local function decode(value) if value == placeholders["nil"] then return nil elseif value == placeholders["NaN"] then return 0 / 0 -- There's no `NaN` literal. else return value end end

-- An ephemeron table is the closest approximation of JavaScript's `WeakMap` -- object, and can be used to store private, per-instance data. This is more -- efficient than using a closure, since it doesn't rebuild the whole -- metatable for every instance. -- -- @see 

local internals = setmetatable({}, {   __mode = "k",    __index = function         error("This set has not been initialized correctly. Please use `Dev:Set.new` to construct new instances")    end, })

-- Prototype for set instances.

local Set = {}

-- Adds one element to a set. -- -- @param {table} self --    The set to add to. -- @param {*} element --    The element to add. -- @returns {table} --    The same set instance.

function Set:add(element) checkType("Set:add", 1, self, "table")

if not self:contains(element) then internals[self].elements[encode(element)] = true internals[self].size = internals[self].size + 1 end

return self end

-- Removes one element from a set. -- -- @param {table} self --    The set to remove from. -- @param {*} element --    The element to remove. -- @returns {table} --    The same set instance.

function Set:remove(element) checkType("Set:remove", 1, self, "table")

if self:contains(element) then internals[self].elements[encode(element)] = nil internals[self].size = internals[self].size - 1 end

return self end

-- Removes all elements from a set. -- -- @param {table} self --    The set to remove from. -- @returns {table} --    The same set instance.

function Set:clear checkType("Set:clear", 1, self, "table")

for i = 1, self:size do       self:remove(self:nextElement) end

return self end

-- Checks if a set has a particular element. -- -- @param {table} self --    The set to search. -- @param {*} element --    The element to search for. -- @returns {boolean} --    Whether the set contains the element.

function Set:contains(element) checkType("Set:contains", 1, self, "table")

return not not internals[self].elements[encode(element)] end

-- Fetches the number of elements in a set. -- -- @see Cardinality -- -- @param {table} self --    A set. -- @returns {number} --    The number of elements in the set.

function Set:size checkType("Set:size", 1, self, "table")

return internals[self].size end

-- Fetches the "next" element in a set, wrapping around to the "beginning" if -- the "previous" was also the "last". Since sets are unordered, the meaning of -- those terms is determined by the built-in `next` function. -- -- This method is provided as a workaround for Lua's horribly-designed iterator -- "protocol". Iterators use `nil` to signal the end of a sequence, but since -- this "class" accepts `nil` elements, it is impossible to write an iterator -- which will always loop over every element. Furthermore, because sets are not -- ordered, such an iterator would stop at an arbitrary position. -- -- Instead, combining this method with `Set:size` allows users to loop over a -- set numerically, much like the `"n"` field trick utilized for sequential -- tables with holes. -- -- @param {table} self --    A set. -- @returns {*} --    An element in the set.

function Set:nextElement checkType("Set:nextElement", 1, self, "table")

local element = next(internals[self].elements, internals[self].prevElement)

-- If we've reached the end of the set, start over from the beginning. if element == nil then element = next(internals[self].elements, element) end

-- Advance the pointer. internals[self].prevElement = element

return decode(element) end

-- Collects every element of a set and places them in an unsorted, sequential -- table. -- -- @param {table} self --    A set. -- @returns {table} --    A sequence containing every element in the set.

function Set:toSequence checkType("Set:toSequence", 1, self, "table")

local sequence = {}

for i = 1, self:size do       sequence[i] = self:nextElement end

return sequence end

-- Builds a new set from two existing ones, where each element of the former is -- also an element of both of the latter. -- -- @see Intersection (set theory) -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {table} --    A new set.

function Set:intersection(other) checkType("Set:intersection", 1, self, "table") checkType("Set:intersection", 2, other, "table")

local newSet = p.new

for i = 1, self:size do       local element = self:nextElement

if other:contains(element) then newSet:add(element) end end

return newSet end

-- Builds a new set from two existing ones, where each element of the former is -- also an element of either of the latter. -- -- @see Union (set theory) -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {table} --    A new set.

function Set:union(other) checkType("Set:union", 1, self, "table") checkType("Set:union", 2, other, "table")

local newSet = p.new

for _, oldSet in ipairs{self, other} do       for i = 1, oldSet:size do            newSet:add(oldSet:nextElement) end end

return newSet end

-- Builds a new set from two existing ones, where each element of the former is -- also an element of the first of the latter, but not the second. -- -- @see Complement (set theory) -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {table} --    A new set.

function Set:difference(other) checkType("Set:difference", 1, self, "table") checkType("Set:difference", 2, other, "table")

local newSet = p.new

for i = 1, self:size do       local element = self:nextElement

if not other:contains(element) then newSet:add(element) end end

return newSet end

-- Builds a new set from two existing ones, where each element of the former is -- also an element of one of the latter, but not both. -- -- @see Symmetric difference -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {table} --    A new set.

function Set:symmetricDifference(other) checkType("Set:symmetricDifference", 1, self, "table") checkType("Set:symmetricDifference", 2, other, "table")

local newSet = p.new

for _, oldSet in ipairs{self, other} do       for i = 1, oldSet:size do            local element = oldSet:nextElement

if newSet:contains(element) then newSet:remove(element) else newSet:add(element) end end end

return newSet end

-- Checks if two sets have no elements in common. -- -- @see Disjoint sets -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {boolean} --    Whether the two sets have no elements in common.

function Set:isDisjointFrom(other) checkType("Set:isDisjointFrom", 1, self, "table") checkType("Set:isDisjointFrom", 2, other, "table")

for i = 1, self:size do       if other:contains(self:nextElement) then return false end end

return true end

-- Checks if every element of one set is also an element of another. -- -- @see Subset -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {boolean} --    Whether every element of the first set is also an element of the second.

function Set:isSubsetOf(other) checkType("Set:isSubsetOf", 1, self, "table") checkType("Set:isSubsetOf", 2, other, "table")

for i = 1, self:size do       if not other:contains(self:nextElement) then return false end end

return true end

-- Checks if every element of one set is also an element of another, but not the -- inverse. -- -- @see Subset -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {boolean} --    Whether every element of the first set is also an element of the second, --    but not the inverse.

function Set:isProperSubsetOf(other) checkType("Set:isProperSubsetOf", 1, self, "table") checkType("Set:isProperSubsetOf", 2, other, "table")

return self:size < other:size and self:isSubsetOf(other) end

-- Checks if every element of one set is also an element of another, and vice -- versa. -- -- @param {table} self --    A set. -- @param {table} other --    A set. -- @returns {boolean} --    Whether every element of the first set is also an element of the second, --    and vice versa.

function Set:equals(other) checkType("Set:equals", 1, self, "table") checkType("Set:equals", 2, other, "table")

return self:size == other:size and self:isSubsetOf(other) end

-- Generates a string representation of a set. -- -- @param {table} self --    A set. -- @returns {string} --    A string representation of the set.

function Set:tostring checkType("Set:tostring", 1, self, "table")

local tmp = {}

for i = 1, self:size do       tmp[i] = tostring(self:nextElement) end

return "Set { " .. table.concat(tmp, ", ") .. " }" end

-- Constructs a set instance, optionally adding one or more elements to it. -- -- @param {...*} ... --    Zero or more elements to add to the set. -- @returns {table} --    A new set.

function p.new(...) local self = setmetatable({}, {       __index = Set,        __mul = Set.intersection,        __add = Set.union,        __sub = Set.difference,        __pow = Set.symmetricDifference,        __le = Set.isSubsetOf,        __lt = Set.isProperSubsetOf,        __eq = Set.equals,        __tostring = Set.tostring,    })

internals[self] = { elements = {}, size = 0, prevElement = nil, }

for i = 1, select("#", ...) do       self:add(select(i, ...)) end

return self end

-- Makes it slightly more convenient to construct set instances, by allowing -- users to omit ".new".

return setmetatable(p, {   __call = function (self, ...)        return self.new(...)    end, })