Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
--- A double-linked list implementation
-- @classmod LinkedList
local Core = require('__stdlib__/stdlib/core')
local table = require('__stdlib__/stdlib/utils/table')
local Is = require('__stdlib__/stdlib/utils/is')
-- dumb shallow copy suitable for cloning instance metatables in subclasses
local _mtcopy = function(self)
local result = {}
for k, v in pairs(self._mt) do
result[k] = v
end
return result
end
-- @class LinkedListNode
-- @usage local llnode = linkedlist.append(item)
local LinkedListNode = setmetatable(
{
__class = 'linked_list',
__class_name = 'LinkedListNode',
_is_LinkedListNode = true,
_mt = {},
_mtcopy = _mtcopy
},
{
__index = Core
}
)
LinkedListNode._mt.__index = LinkedListNode
LinkedListNode.__class = LinkedListNode
-- @module linked_list
-- @usage local LinkedList = require('stdlib.utils.classes.linked_list')
local LinkedList = setmetatable(
{
__class = 'linked_list',
__class_name = 'LinkedList',
_is_LinkedList = true,
_node_class = LinkedListNode,
_mt = {},
_mtcopy = _mtcopy
},
{
__index = Core
}
)
LinkedList.__class = LinkedList
function LinkedList.new(self)
-- support LinkedList.new() syntax compatible with most stdlib classes
if Is.Nil(self) then self = LinkedList end
-- determine if this is a class or an instance; if an instance, assume they intended
-- to create a node and provide a hopefully-not-too-confusing error message.
Is.Assert.Nil(self.next, function()
return 'Use foo:new_node(), not foo:new(), to create new ' .. self.__class_name .. ' nodes'
end)
local result = setmetatable({ __class = self }, self._mt)
-- live_iterators is a set/bag (see _Programming_In_Lua_ 1st Ed. §11.5). It uses weak keys
-- so garbage collected iterators will be automatically removed (see :new_node_iter below).
result.live_iterators = setmetatable({}, { __mode = 'k' })
result.next = result
result.prev = result
return result
end
function LinkedList:new_node(item, prev, next)
-- only way to determine if this is a class or an instance
Is.Assert.Not.Nil(self, 'Use foo:new_node, not foo.new_node to create new nodes')
-- Retrieve the node class from the class if we are an instance
local node_class = Is.Nil(self.next) and self._node_class
or self.__class and self.__class._node_class
or error('LinkedList:new_node: cannot find node class, improper invocation?')
local result = setmetatable({ __class = node_class }, node_class._mt)
result.next = next or result
result.prev = prev or result
result.item = item
result.owner = self
return result
end
function LinkedList:from_stack(stack, allow_insane_sparseness)
Is.Assert.Not.Nil(self.__class, [[LinkedList:from_stack is a class method, not a static function; \z
For example LinkedList:from_stack(stack) would be a correct invocation syntax']])
-- since linkedlists effectively support sparse keys, ensure we can
-- round-trip various configurations by supporting sparse pseudo-stacks
local sparse_keys = {}
for k in pairs(stack) do
if type(k) == 'number' and math.floor(k) == k and math.max(1, k) == k then
table.insert(sparse_keys, k)
else
log('LinkedList.from_stack ignoring non-stackish key value "' .. tostring(k) .. '"')
end
end
table.sort(sparse_keys)
local result = self.__class:new()
-- subclasses could theoretically override the allow_insane_sparseness
-- object-level override in __class:new(), so respect their wishes here.
result.allow_insane_sparseness = result.allow_insane_sparseness or allow_insane_sparseness
local last_key = 0
for _, k in ipairs(sparse_keys) do
last_key = last_key + 1
if last_key < k then
if k - last_key >= 999 then
Is.Assert(result.allow_insane_sparseness, function()
return 'Refusing to create insanely sparse list at key ' .. tostring(k)
end)
end
repeat
last_key = last_key + 1
result.prev.next = result:new_node(nil, result.prev, result)
result.prev = result.prev.next
until last_key == k
end
result.prev.next = result:new_node(stack[k], result.prev, result)
result.prev = result.prev.next
end
return result
end
function LinkedList:to_stack()
local result = {}
local index = 1
for node in self:nodes() do
if Is.Not.Nil(node.item) then
result[index] = node.item
end
index = index + 1
end
return result
end
function LinkedList:length()
local result = 0
for _ in self:nodes() do
result = result + 1
end
return result
end
LinkedList._mt.__len = LinkedList.length
function LinkedList:is_empty()
Is.Assert.Not.Nil(self, 'LinkedList.is_empty called without self argument', 3)
Is.Assert.Not.Nil(self.next, 'LinkedList.next property is missing: structural error or bad argument', 3)
return self.next == self
end
function LinkedList:last_node()
return self.prev ~= self and self.prev or nil
end
function LinkedList:last_item()
local node = self:last_node()
return node and node.item or nil
end
function LinkedList:first_node()
return self.next ~= self and self.next or nil
end
function LinkedList:first_item()
local node = self:first_node()
return node and node.item or nil
end
function LinkedList:concatenate(other)
if Is.Nil(other) then
return self:copy()
else
Is.Assert(other._is_LinkedList, 'cannot concatenate non-linked-list with linked-list')
end
local self_copy = self:copy()
if not other:is_empty() then
local other_copy = other:copy()
self_copy.prev.next = other_copy.next
other_copy.next.prev = self_copy.prev
self_copy.prev = other_copy.prev
other_copy.prev.next = self_copy
end
return self_copy
end
LinkedList._mt.__concat = LinkedList.concatenate
function LinkedList._mt.__index(self, k)
if type(k) ~= 'number' or math.floor(k) ~= k or k < 1 then
-- any non-special index goes to the class from here
return self.__class[k]
end
local count = 1
local node = self.next
while node ~= self do
if count == k then
return node.item
end
node = node.next
count = count + 1
end
end
function LinkedList._mt.__newindex(self, k, v)
if type(k) ~= 'number' or math.floor(k) ~= k or k < 1 then
-- any non-special index goes straight into the table (the class is
-- immutable, but the object may opt to override class functions)
return rawset(self, k, v)
end
local count = 1
local node = self.next
while node ~= self do
if count == k then
node.item = v
return nil
end
node = node.next
count = count + 1
end
-- They have requested a new node to be appended, perhaps with a certain
-- number of intervening empty nodes. But, would the request create an
-- insanely sparse index?
Is.Assert(k - count < 999 or self.allow_insane_sparseness,
'Setting requested index in linkedlist would create insanely sparse list')
repeat
-- this is a bit gross; we increment count one /exta/ time here, on the
-- first iteration; so now count == self.length + 2
count = count + 1
node = self:append(nil)
-- nb: count == self.length + 1
until count > k
node.item = v
end
function LinkedList:append(item)
self.prev.next = self:new_node(item, self.prev, self)
self.prev = self.prev.next
return self.prev
end
function LinkedList:prepend(item)
self.next.prev = self:new_node(item, self, self.next)
self.next = self.next.prev
return self.next
end
function LinkedList:insert(item, index)
if not index then
return self:append(item)
elseif index == 1 then
return self:prepend(item)
end
Is.Assert(type(index) == 'number' and math.floor(index) == index and index >= 1,
'LinkedList:insert with irregular index')
local length = self:length()
Is.Assert(index - length <= 999 or self.allow_insane_sparseness,
'LinkedList:insert would create insanely sparse list.')
if length + 1 < index then
repeat
length = length + 1
self:append(nil)
until length + 1 == index
return self:append(item)
else
local node = self
while index > 1 do
node = node.next
index = index - 1
end
node.next.prev = self:new_node(item, node, node.next)
node.next = node.next.prev
return node.next.prev
end
end
function LinkedList:remove(index)
Is.Assert.Not.Nil(self, 'LinkedList:remove called without self argument.', 3)
Is.Assert.Not.Nil(index, 'LinkedList:remove without index argument', 3)
Is.Assert(type(index) == 'number' and math.floor(index) == index and index >= 1,
'LinkedList:remove with irregular index argument.', 3)
if self:is_empty() then
return nil
end
local count = 1
local node = self.next
while node ~= self do
if count == index then
return node:remove()
else
count = count + 1
node = node.next
end
end
end
function LinkedListNode:graft_after(target)
Is.Assert.Not.Nil(target, 'LinkedListNode.graft_after: Missing node argument or not invoked as node:graft_after(target)', 3)
repeat
target = target.next
until not target.is_tombstone
self.next = target
self.prev = target.prev
target.prev = self
self.prev.next = self
end
function LinkedListNode:graft_before(target)
Is.Assert.Not.Nil(target, 'LinkedListNode.graft_after: Missing node argument or not invoked as node:graft_after(target)', 3)
repeat
target = target.prev
until not target.is_tombstone
self.prev = target
self.next = target.next
target.next = self
self.next.prev = self
end
function LinkedListNode:prune()
Is.Assert.Not.Nil(self, 'LinkedListNode.prune: Missing self argument (invoke as node:prune())', 3)
self.prev.next = self.next
self.next.prev = self.prev
for live_iterator in pairs(self.owner.live_iterators) do
if live_iterator.at == self then
-- if live_iterator.is_forward_iterator then
live_iterator.forced = self.prev
-- else
-- live_iterator.forced = self.next
-- end
end
end
return self
end
function LinkedListNode:remove()
Is.Assert.Not.Nil(self, 'LinkedListNode.remove: Missing self argument (invoke as node:remove())', 3)
Is.Assert.Not(self.is_tombstone, 'LinkedListNode.remove: Double-removal detected.', 3)
self.is_tombstone = true
return self:prune()
end
function LinkedList:clear()
Is.Assert.Not.Nil(self, 'LinkedList.clear: Missing self argument (invoke as list:clear())', 3)
-- don't pull the rug out from under live iterators; tombstone each node as applicable,
-- skipping any nodes that were already iterated.
for iterator in pairs(self.live_iterators) do
if iterator.at then
local iterator_at = iterator.at
iterator.at = nil
while iterator_at ~= self do
iterator_at.is_tombstone = true
iterator_at = iterator_at.next
end
end
end
self.prev = self
self.next = self
end
function LinkedListNode:_copy_with_to(copy_fn, other_node)
other_node.item = copy_fn(self.item)
end
function LinkedList:_copy_with_to(copy_fn, other)
local last_node = other
for self_node in self:nodes() do
last_node.next = self:new_node(nil, last_node, other)
last_node = last_node.next
self_node:_copy_with_to(copy_fn, last_node)
end
other.prev = last_node
end
function LinkedList:_copy_with(copy_fn)
-- LinkedList.new does not permit instance:new(), so use class
local result = self.__class:new()
self:_copy_with_to(copy_fn, result)
return result
end
local function identity(x)
return x
end
function LinkedList:copy()
return self:_copy_with(identity)
end
LinkedList.deepcopy = table.flexcopy
function LinkedList:new_node_iterator()
Is.Assert.Not.Nil(self, 'LinkedList:new_node_iterator called without self argument \z
(did you mean to use ":" instead of "."?)', 2)
local iteration_tracker = {}
self.live_iterators[iteration_tracker] = true
return function(linked_list, node)
Is.Assert.True(linked_list == self, 'Wrong Linked List provided to node iterator', 3)
local next_node = iteration_tracker.forced or node
iteration_tracker.forced = nil
-- if items have been removed during iteration, we may encounter
-- tombstones here. Once we reach the next non-tombstoned node,
-- we have found our way back to the remaining legitimate nodes
repeat
next_node = next_node.next
until not next_node.is_tombstone
next_node = (next_node ~= self and next_node or nil)
iteration_tracker.at = next_node
if next_node == nil then
-- Technically, we could skip this step and rely on the garbage
-- collector, but even so we'd need iteration_tracker to be an upvalue.
-- Anyhow, why wait for GC? We know we're done, now.
self.live_iterators[iteration_tracker] = nil
iteration_tracker = nil
end
return next_node
end
end
function LinkedList:nodes()
return self:new_node_iterator(), self, self
end
function LinkedList:items()
-- we "need" a closure here in order to track the node, since it is not
-- returned by the iterator.
local iter = self:new_node_iterator()
local node = self
return function()
-- not much we can do about nils here so ignore them
repeat
node = iter(self, node)
if node and node.item ~= nil then
return node.item
end
until node == nil
end
end
function LinkedList:ipairs()
local i = 0
local node = self
local iter = self:new_node_iterator()
-- we kind-of "need" a closure here or else we'll end up having to
-- chase down the indexed node every iteration at potentially huge cost.
return function()
repeat
i = i + 1
node = iter(self, node)
if node ~= nil and node.item ~= nil then
return i, node.item
end
until node == nil
end
end
LinkedList._mt.__ipairs = LinkedList.ipairs
function LinkedList:tostring()
local result = self.__class_name .. ':from_stack {'
local skipped = false
local firstrep = true
local count = 0
for node in self:nodes() do
count = count + 1
local node_rep
if node.tostring then
node_rep = node:tostring()
elseif Is.False(node.item) then
node_rep = 'false'
elseif node.item then
if Is.String(node.item) then
node_rep = '"' .. node.item .. '"'
else
node_rep = tostring(node.item)
end
end -- else it is nil and we skip it
if node_rep then
if not firstrep then
result = result .. ', '
else
firstrep = false
end
if skipped then
-- if any index has been skipped then we provide
-- explicit lua index syntax i.e., {[2] = 'foo'}
result = result .. '[' .. tostring(count) .. '] = '
end
result = result .. node_rep
else
skipped = true
end
end
return result .. '}'
end
LinkedList._mt.__tostring = LinkedList.tostring
function LinkedList:validate_integrity()
if self.next == self then
Is.Assert(self.prev == self, 'Empty LinkedList prev and next do not match', 2)
else
Is.Assert.Nil(self.item, 'LinkedList contains item in head node', 2)
Is.Assert.Not.Nil(self._is_LinkedList, 'LinkedList head node does not have _is_LinkedList', 2)
local iteration = 0
local i = self
local prev_i = self
local visited = {}
while i.next ~= self do
iteration = iteration + 1
local err_hint = ' (iteration #' .. tostring(iteration) .. ')'
visited[i] = true
i = i.next
Is.Assert(i.prev == prev_i, 'next.prev mismatch' .. err_hint, 2)
prev_i = i
Is.Assert.Not(visited[i], 'LinkedList contains node-loop' .. err_hint, 2)
Is.Assert(i._is_LinkedListNode, 'LinkedList contains LinkedList as node' .. err_hint, 2)
end
Is.Assert(self.prev == prev_i, 'LinkedList prev is not terminal node in .next chain', 2)
end
return true
end
return LinkedList

View File

@@ -0,0 +1,142 @@
--- String Array Metatable
-- For working with string arrays without duplicate values
-- @classmod string_array
local M = {
__class = 'string-array-class'
}
local metatable
--- Does this array contain name.
-- @tparam string name The string to find.
-- @treturn boolean string is in array
function M:has(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
if not self:has(str) then
return false
end
end
return true
end
assert(type == 'string', 'name must be a string')
for _, str in ipairs(self) do
if str == name then
return true
end
end
return false
end
--- Add a string to the array if it doesn't exist in the array.
-- @tparam string name
-- @treturn self
function M:add(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:add(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for _, str in ipairs(self) do
if str == name then
return self
end
end
table.insert(self, name)
return self
end
--- Remove the string from the array if it exists.
-- @tparam string name
-- @treturn self
function M:remove(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:remove(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for i, str in ipairs(self) do
if str == name then
table.remove(self, i)
return self
end
end
return self
end
--- Toggles the passed name in the array by adding it if not present or removing it if it is.
-- @tparam string name
-- @treturn self
function M:toggle(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:toggle(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for i, str in ipairs(self) do
if str == name then
table.remove(self, i)
return self
end
end
table.insert(self, name)
return self
end
--- Clear the array returning an empty array object
-- @treturn self
function M:clear()
for i = #self, 1, -1 do
table.remove(self, i)
end
return self
end
--- Convert the array to a string
-- @treturn string
function M:tostring()
return table.concat(self, ', ')
end
--- Concat string-arrays and strings together
-- @tparam string|string-array rhs
-- @treturn string-array
function M:concat(rhs)
if getmetatable(self) == metatable then
return self:add(rhs)
else
return rhs:add(self)
end
end
--- The following metamethods are provided.
-- @table metatable
metatable = {
__index = M, -- Index to the string array class.
__tostring = M.tostring, -- tostring.
__concat = M.concat, -- adds the right hand side to the object.
__add = M.add, -- Adds a string to the string-array object.
__sub = M.remove, -- Removes a string from the string-array object.
__unm = M.clear, -- Clears the array.
__call = M.has -- Array contains this string.
}
return function(array)
if type(array) == 'table' then
return setmetatable(array, metatable)
end
end

View File

@@ -0,0 +1,338 @@
--- Unique Array class
-- For working with unique string array values. Creates an array with hash/dictionary indexing.
-- @classmod unique_array
-- Adding or removeing values without using the provided methods can break the functionality of this class.
-- Additional methods exported by requering unique_array are .set and .make_dictionary
-- @usage local Unique_Array = require('__stdlib__/stdlib/utils/classes/unique_array')
-- local my_array = Unique_Array('first')
-- my_array:add('second')
-- if my_array['second'] then
-- print('its truthy')
-- end'
-- @set all=true
local M = {
__class = 'unique_array'
}
local type, ipairs = type, ipairs
local getmetatable, setmetatable, rawset = getmetatable, setmetatable, rawset
local t_remove, t_sort = table.remove, table.sort
local function add(self, class, param)
local index = #self + 1
rawset(self, index, param)
class.dictionary[param] = index
class.len = class.len + 1
return index
end
local function dictionary_sort(self, class)
class.len = 0
for i, v in ipairs(self) do
class.len = class.len + 1
class.dictionary[v] = i
end
end
local function remove(self, class, param)
t_remove(self, class.dictionary[param])
class.dictionary[param] = nil
dictionary_sort(self, class)
end
local wrappers = {}
local function create_class(tab)
if type(tab) == 'table' then
local class = {
__concat = M.concat,
__tostring = M.tostring,
__eq = M.same,
__lt = wrappers.__lt,
__add = wrappers.__add,
__sub = wrappers.__sub,
__lte = wrappers.__lt,
len = 0
}
class.dictionary = M.make_dictionary(tab)
class.__index = function(_, k)
return class.dictionary[k] or M[k]
end
return setmetatable(tab, class)
end
end
local function unique_or_new(tab)
if type(tab) == table and tab.__class == 'unique_array' then
return tab
else
return M.new(tab)
end
end
wrappers.__lt = function(lhs, rhs)
lhs, rhs = unique_or_new(lhs), unique_or_new(rhs)
return rhs:all(lhs)
end
wrappers.__add = function(lhs, rhs)
return M.new(lhs):add(rhs)
end
wrappers.__sub = function(lhs, rhs)
return M.new(lhs):remove(rhs)
end
wrappers.__pairs = function(self)
local dictionary = getmetatable(self).dictionary
return next, dictionary, nil
end
--- Methods
-- @section Methods
--- Create a new unique array.
-- @tparam unique_array|string|{string,...} ... strings to initialize the unique array with
-- @treturn @{unique_array} new
function M.new(...)
return create_class {}:add(type((...)) == 'table' and (...) or { ... })
end
--- Add a string to the array if it doesn't exist in the array.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:add(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if not self[param] then
add(self, class, param)
end
end
return self
end
--- Remove the strings from the array if they exist.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:remove(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
remove(self, class, param)
end
end
return self
end
--- Toggles the passed name in the array by adding it if not present or removing it if it is.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:toggle(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
remove(self, class, param)
else
add(self, class, param)
end
end
return self
end
--- Get all items that are NOT in both arrays.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:diff(other)
other = unique_or_new(other)
local diff = M.new()
for _, v in ipairs(self) do
if not other[v] then
diff:add(v)
end
end
for _, v in ipairs(other) do
if not self[v] then
diff:add(v)
end
end
return diff
end
--- Get all items that are in both arrays.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:intersects(other)
other = unique_or_new(other)
local intersection = M.new()
for _, v in ipairs(self) do
if other[v] then
intersection:add(v)
end
end
for _, v in ipairs(other) do
if self[v] and not intersection[v] then
intersection:add(v)
end
end
return intersection
end
--- Sort the unique_array in place.
-- @tparam[opt] function cmp Comparator @{sort} function to use
-- @treturn @{unique_array} self
function M:sort(cmp)
local class = getmetatable(self)
t_sort(self, cmp)
dictionary_sort(self, class)
return self
end
--- Create a new unique_array by concatenating together.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:concat(other)
return M.new(self):add(other)
end
--- Find all members in a unique array that match the pattern.
-- @tparam string pattern Lua @{pattern}
-- @treturn @{unique_array} new unique array containing all elements that match.
function M:find(pattern)
local matches = M.new()
for _, value in ipairs(self) do
if value:find(pattern) then
matches:add(value)
end
end
return matches
end
--- Clear the array returning an empty array object
-- @treturn @{unique_array} self
function M:clear()
local class = getmetatable(self)
for i = #self, 1, -1 do
self[i] = nil
end
class.dictionary = {}
class.len = 0
return self
end
--- Functions
-- @section Functions
--- Does this array have all of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean every passed string is in the array
function M:all(other)
local params = type(other) == 'table' and other or { other }
local count = 0
for _, param in ipairs(params) do
if self[param] then
count = count + 1
end
end
return count == #params
end
M.has = M.all
--- Does this array have any of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean any passed string is in the array
function M:any(other)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
return true
end
end
return false
end
--- Does this array have none of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean no passed strings are in the array
function M:none(other)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
return false
end
end
return true
end
--- Do both arrays contain the same items
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean
function M:same(other)
other = unique_or_new(other)
if #self == #other then
for _, value in ipairs(self) do
if not other[value] then
return false
end
end
return true
end
return false
end
--- Do the unique arrays have no items in common
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean
function M:disjointed(other)
return #self:intersects(other) == 0
end
--- Convert the array to a string.
-- @treturn string
function M:tostring()
return table.concat(self, ', ')
end
--- Return a dictionary mapping of the array.
-- can be passed a function to set the value of the field.
-- @tparam[opt] function func value, index are passed as the first two paramaters
-- @tparam[opt] any ... additional values to pass to the function
-- @treturn dictionary
function M:make_dictionary(func, ...)
local dict = {}
for index, value in ipairs(self) do
dict[value] = func and func(value, index, ...) or index
end
return dict
end
--- Exports
-- @section Exports
local function from_dictionary(dict)
local array = {}
local i = 0
for k in pairs(dict) do
i = i + 1
array[i] = k
end
return create_class(array)
end
--- These functions are available when requiring this class.
-- @table exports
local exports = {
new = M.new, -- @{unique_array.new}
set = create_class, -- set the class on an existing table
dictionary = M.make_dictionary, -- @{unique_array.make_dictionary}
from_dictionary = from_dictionary
}
setmetatable(
exports,
{
__call = function(_, ...)
return M.new(...)
end
}
)
return exports

View File

@@ -0,0 +1,441 @@
--- For playing with colors.
-- @module Utils.Color
-- @usage local Color = require('__stdlib__/stdlib/utils/color')
local Color = {
__class = 'Color',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Color, Color)
local metatable
local table = require('__stdlib__/stdlib/utils/table')
local math = require('__stdlib__/stdlib/utils/math')
local color_list = require('__stdlib__/stdlib/utils/defines/color_list')
--- @table color @{defines.color}
Color.color = require('__stdlib__/stdlib/utils/defines/color')
--- @table anticolor @{defines.anticolor}
Color.anticolor = require('__stdlib__/stdlib/utils/defines/anticolor')
--- @table lightcolor @{defines.lightcolor}
Color.lightcolor = require('__stdlib__/stdlib/utils/defines/lightcolor')
--- Color Constructors
-- @section Color Constructors
--- Create a new Color object.
-- it can be passed, A Color, a string color name, an array,
-- a list of float paramaters (RGB), a color dictionary, or hex
-- @param any
-- @treturn Color
function Color.new(...)
if (...) == Color then
return Color.new(select(2, ...))
else
local c_type = type((...) or nil)
if not ... then
-- from a hex code or word color string, "red"
return Color.white()
elseif c_type == 'string' then
if (...):find('%x%x%x%x%x%x$') then
return Color.from_hex(...)
else
return Color.from_string(...)
end
elseif c_type == 'number' then
return Color.from_params(...)
elseif c_type == 'table' then
return Color.copy(...)
end
end
end
Color.__call = Color.new
--- Loads the color metatmethods into table without any checking.
-- @tparam Color color
-- @treturn color
function Color.load(color)
return setmetatable(color, metatable)
end
--- Copies the color into a new Color.
-- @tparam Color color
-- @tparam[opt] float alpha Change the alpha of the copied color
-- @treturn Color
function Color.copy(color, alpha)
if type(color) == 'table' then
if color == Color then
return Color.white()
elseif getmetatable(color) == metatable then
return setmetatable({ r = color.r, g = color.g, b = color.b, a = alpha or color.a or 0.5 }, metatable)
elseif type((next(color))) == 'number' then
return Color.from_array(color, alpha)
else
return Color.from_table(color, alpha)
end
else
Color.new(color, alpha)
end
end
--- Returns a white Color.
-- @treturn Color
function Color.white()
local color = { r = 1, g = 1, b = 1, a = 0.5 }
return setmetatable(color, metatable)
end
--- Returns a black color.
-- @treturn Color
function Color.black()
local color = { r = 0, g = 0, b = 0, a = 0.5 }
return setmetatable(color, metatable)
end
--- Returns a color from a string name if known.
-- Returns white if color string is unknown
-- @tparam string string_name
-- @tparam[opt] float alpha
-- @treturn Color
function Color.from_string(string_name, alpha)
local color = Color.color[string_name]
if color then
color.a = alpha or color.a or 0.5
return setmetatable(color, metatable)
end
return Color.white()
end
--- Converts a color in the rgb format to a color table.
-- @tparam[opt=0] float r 0-255 red
-- @tparam[opt=0] float g 0-255 green
-- @tparam[opt=0] float b 0-255 blue
-- @tparam[opt=255] float a 0-255 alpha
-- @treturn Concepts.Color
function Color.from_params(r, g, b, a)
local new = Color.normalize { r = r, g = g or r, b = b or r, a = a or 0.5 }
return setmetatable(new, metatable)
end
--- @see Color.from_params
Color.from_rgb = Color.from_params
--- Converts a color in the array format to a color in the table format.
-- @tparam array color the color to convert &mdash; { [1] = @{float}, [2] = @{float}, [3] = @{float}, [4] = @{float} }
-- @tparam[opt] float alpha
-- @treturn Concepts.Color a converted color &mdash; { r = c\_arr[1], g = c\_arr[2], b = c\_arr[3], a = c\_arr[4] }
function Color.from_array(color, alpha)
return Color.from_params(color[1] or 0, color[2] or 0, color[3] or 0, alpha or color[4] or 0.5)
end
--- Converts a color in the dictionary format to a color in the Color format.
-- @tparam table color the color to convert
-- @tparam[opt] float alpha
-- @treturn Color
function Color.from_table(color, alpha)
return Color.from_params(color.r or 0, color.g or 0, color.b or 0, alpha or color.a or 0.5)
end
--- Get a color table with a hexadecimal string.
-- Optionally provide the value for the alpha channel.
-- @tparam string color hexadecimal color string (#ffffff, not #fff)
-- @tparam[opt=1] float alpha the alpha value to set; such that *** 0 &#8924; value &#8924; 1 ***
-- @treturn Concepts.Color a color table with RGB converted from Hex and with alpha
function Color.from_hex(color, alpha)
assert(type(color) == 'string', 'missing color hex value')
local match = color:match('%x?%x?%x%x%x%x%x%x$')
color = tonumber(match, 16)
local new = { r = 0, g = 0, b = 0, a = 1 }
if #match == 8 then
new.r = bit32.extract(color, 24, 8) / 255
new.g = bit32.extract(color, 16, 8) / 255
new.b = bit32.extract(color, 8, 8) / 255
new.a = bit32.extract(color, 0, 8) / 255
elseif #match == 6 then
new.r = bit32.extract(color, 16, 8) / 255
new.g = bit32.extract(color, 8, 8) / 255
new.b = bit32.extract(color, 0, 8) / 255
new.a = alpha and (alpha > 1 and math.min(alpha, 255) / 255) or alpha or 0.5
end
return setmetatable(new, metatable)
end
--- @section end
--- Color Methods
-- @section Color Methods
---@deprecated
function Color.set(color, alpha)
color = color or { r = 1, g = 1, b = 1 }
if #color > 0 then
color = Color.from_array(color)
end
color.a = alpha or color.a or 1
return color
end
--- Normalizes a color between 0 and 1
-- @tparam Color color
-- @return Color
function Color.normalize(color)
color.a = color.a or 1
if color.r > 1 or color.g > 1 or color.b > 1 or color.a > 1 then
color.r = color.r > 1 and math.min(color.r, 255) / 255 or color.r
color.g = color.g > 1 and math.min(color.g, 255) / 255 or color.g
color.b = color.b > 1 and math.min(color.b, 255) / 255 or color.b
color.a = color.a > 1 and math.min(color.a, 255) / 255 or color.a
end
return color
end
--- Set the alpha channel on a color
-- @tparam Color color
-- @tparam[opt = 1] float alpha
-- @treturn Color
function Color.alpha(color, alpha)
alpha = alpha or 1
alpha = alpha > 1 and alpha / 255 or alpha
color.a = alpha
return color
end
--- Premultiply alpha
-- @tparam Color color
-- @tparam float alpha
-- @return Color
function Color.premul_alpha(color, alpha)
alpha = alpha > 1 and math.min(alpha, 255) / 255 or alpha
local new = {}
new.r = math.clamp(color.r * alpha)
new.g = math.clamp(color.g * alpha)
new.b = math.clamp(color.b * alpha)
new.a = color.a or 1
return setmetatable(new, metatable)
end
local function make_color(lhs, rhs)
if not Color.is_color(rhs) then
rhs = Color.new(rhs)
elseif not Color.is_color(lhs) then
lhs = Color.new(lhs)
end
return lhs, rhs
end
local function clamped(r, g, b, a)
local new = {}
new.r = math.clamp(r)
new.g = math.clamp(g)
new.b = math.clamp(b)
new.a = math.clamp(a)
return setmetatable(new, metatable)
end
--- Add 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.add(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r + rhs.r, lhs.g + rhs.g, lhs.b + rhs.b, math.max(lhs.a, rhs.a))
end
--- Subtract 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.subtract(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r - rhs.r, lhs.g - rhs.g, lhs.b - rhs.b, math.max(lhs.a, rhs.a))
end
--- Multiply 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.multiply(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r * rhs.r, lhs.g * rhs.g, lhs.b * rhs.b, math.max(lhs.a, rhs.a))
end
--- Add 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.divide(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r / rhs.r, lhs.g / rhs.g, lhs.b / rhs.b, math.max(lhs.a, rhs.a))
end
--- Modulo of 2 colors.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn Color
function Color.modulo(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r % rhs.r, lhs.g % rhs.g, lhs.b % rhs.b, math.max(lhs.a, rhs.a))
end
--- Flip a color to white or black.
-- @tparam Color color
-- @treturn Color
function Color.unary(color)
return Color.len(color) < 1.5 and Color.white() or Color.black()
end
--- @section end
--- Color Functions
-- @section Color Functions
--- Get the length of a color by adding all its values together
-- @tparam Color color
-- @treturn number
function Color.len(color)
return color.r + color.g + color.b
end
--- Are both colors equal.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.equals(lhs, rhs)
return lhs.r == rhs.r and lhs.g == rhs.g and lhs.b == rhs.b and lhs.a == rhs.a
end
--- Is LHS less than RHS.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.less_than(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return Color.len(lhs) < Color.len(rhs)
end
--- Is LHS less than or equal to RHS.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.less_than_eq(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return Color.len(lhs) <= Color.len(rhs)
end
--- Return a hex formated string from a color.
-- @tparam Color color
-- @treturn string
function Color.to_hex(color)
local str = {
string.format('%x', color.r * 255),
string.format('%x', color.g * 255),
string.format('%x', color.b * 255),
string.format('%x', (color.a or 1) * 255)
}
return '#' .. table.concat(str, '')
end
--- Return an array with 4 paramaters.
-- @tparam Color color
-- @treturn array
function Color.to_array(color)
return { color.r, color.g, color.b, color.a }
end
--- @see Color.to_array
Color.pack = Color.to_array
--- Return the color as 4 paramaters.
-- @tparam Color color
-- @return float
-- @return float
-- @return float
-- @return float
function Color.to_params(color)
return color.r, color.g, color.b, color.a
end
--- @see Color.to_params
Color.unpack = Color.to_params
--- Return the Color as a string.
-- @tparam Color color
-- @treturn string
function Color.to_string(color)
local str = {
'r = ' .. (color.r or 0),
'g = ' .. (color.g or 0),
'b = ' .. (color.b or 0),
'a = ' .. (color.a or 1)
}
return '{' .. table.concat(str, ', ') .. '}'
end
--- Is this a correctly formatted color.
-- @tparam Color color
-- @treturn boolean
function Color.is_complex(color)
local r, g, b, a
if type(color) == 'table' then
r = color.r and color.r <= 1
g = color.g and color.g <= 1
b = color.b and color.b <= 1
a = color.b and color.a <= 1
end
return r and b and g and a
end
--- Is this a Color object.
-- @tparam Color color
-- @treturn boolean
function Color.is_Color(color)
return getmetatable(color) == metatable
end
--- Is this a Color object or correctly formatted color table.
-- @tparam Color color
-- @treturn boolean
function Color.is_color(color)
return Color.is_Color(color) or Color.is_complex(color)
end
local function get_color_distance(lhs, rhs)
local dist = { r = lhs.r - rhs.r, g = lhs.g - rhs.g, b = lhs.b - rhs.b }
return (dist.r * dist.r + dist.g * dist.g + dist.b * dist.b)
end
--Takes a color and table of colors, finds key of color in table that most closely matches given color
function Color.best_color_match(color)
local closest
local min = 1
for color_name, compare in pairs(color_list) do
local distance = get_color_distance(color, compare)
min = (distance < min) and distance or min
if distance == min then
closest = color_name
end
end
return closest
end
--- @section end
metatable = {
__class = 'color',
__index = Color,
__call = Color.copy,
__add = Color.add,
__sub = Color.subtract,
__mul = Color.multiply,
__div = Color.divide,
__mod = Color.modulo,
__unm = Color.unary,
__eq = Color.equals,
__lt = Color.less_than,
__le = Color.less_than_eq,
__len = Color.len,
__tostring = Color.to_string,
__concat = _ENV.concat
}
return Color

View File

@@ -0,0 +1,74 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/anticolor')
-- @module defines.anticolor
-- @see Concepts.Color
--- Returns white for dark colors or black for lighter colors.
-- @table anticolor
-- @tfield Concepts.Color green defines.color.black
-- @tfield Concepts.Color grey defines.color.black
-- @tfield Concepts.Color lightblue defines.color.black
-- @tfield Concepts.Color lightgreen defines.color.black
-- @tfield Concepts.Color lightgrey defines.color.black
-- @tfield Concepts.Color lightred defines.color.black
-- @tfield Concepts.Color orange defines.color.black
-- @tfield Concepts.Color white defines.color.black
-- @tfield Concepts.Color yellow defines.color.black
-- @tfield Concepts.Color black defines.color.white
-- @tfield Concepts.Color blue defines.color.white
-- @tfield Concepts.Color brown defines.color.white
-- @tfield Concepts.Color darkblue defines.color.white
-- @tfield Concepts.Color darkgreen defines.color.white
-- @tfield Concepts.Color darkgrey defines.color.white
-- @tfield Concepts.Color darkred defines.color.white
-- @tfield Concepts.Color pink defines.color.white
-- @tfield Concepts.Color purple defines.color.white
-- @tfield Concepts.Color red defines.color.white
local anticolor = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local anticolors = {
green = colors.black,
grey = colors.black,
lightblue = colors.black,
lightgreen = colors.black,
lightgrey = colors.black,
lightred = colors.black,
orange = colors.black,
white = colors.black,
yellow = colors.black,
black = colors.white,
blue = colors.white,
brown = colors.white,
darkblue = colors.white,
darkgreen = colors.white,
darkgrey = colors.white,
darkred = colors.white,
pink = colors.white,
purple = colors.white,
red = colors.white
}
local _mt = {
__index = function(_, c)
return anticolors[c] and { r = anticolors[c]['r'], g = anticolors[c]['g'], b = anticolors[c]['b'], a = anticolors[c]['a'] or 1 } or
{ r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = anticolors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
setmetatable(anticolor, _mt)
_G.defines = _G.defines or {}
_G.defines.anticolor = anticolor
return anticolor

View File

@@ -0,0 +1,53 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/color')
-- @module defines.color
-- @see Concepts.Color
-- defines table is automatically required in all mod loading stages.
--- A table of colors allowing retrieval by color name.
-- @table color
-- @usage color = defines.color.red
-- @tfield Concepts.Color white
-- @tfield Concepts.Color black
-- @tfield Concepts.Color darkgrey
-- @tfield Concepts.Color grey
-- @tfield Concepts.Color lightgrey
-- @tfield Concepts.Color red
-- @tfield Concepts.Color darkred
-- @tfield Concepts.Color lightred
-- @tfield Concepts.Color green
-- @tfield Concepts.Color darkgreen
-- @tfield Concepts.Color lightgreen
-- @tfield Concepts.Color blue
-- @tfield Concepts.Color darkblue
-- @tfield Concepts.Color lightblue
-- @tfield Concepts.Color orange
-- @tfield Concepts.Color yellow
-- @tfield Concepts.Color pink
-- @tfield Concepts.Color purple
-- @tfield Concepts.Color brown
local color = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local _mt = {
__index = function(_, c)
return colors[c] and { r = colors[c]['r'], g = colors[c]['g'], b = colors[c]['b'], a = colors[c]['a'] or 1 } or { r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = colors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
setmetatable(color, _mt)
_G.defines = _G.defines or {}
_G.defines.color = color
return color

View File

@@ -0,0 +1,149 @@
return {
darkgray = { r = 0.6627, g = 0.6627, b = 0.6627, a = 1 },
aliceblue = { r = 0.9412, g = 0.9725, b = 1.0000, a = 1 },
antiquewhite = { r = 0.9804, g = 0.9216, b = 0.8431, a = 1 },
aqua = { r = 0.0000, g = 1.0000, b = 1.0000, a = 1 },
aquamarine = { r = 0.4980, g = 1.0000, b = 0.8314, a = 1 },
azure = { r = 0.9412, g = 1.0000, b = 1.0000, a = 1 },
beige = { r = 0.9608, g = 0.9608, b = 0.8627, a = 1 },
bisque = { r = 1.0000, g = 0.8941, b = 0.7686, a = 1 },
black = { r = 0.0000, g = 0.0000, b = 0.0000, a = 1 },
blanchedalmond = { r = 1.0000, g = 0.9216, b = 0.8039, a = 1 },
blue = { r = 0.0000, g = 0.0000, b = 1.0000, a = 1 },
blueviolet = { r = 0.5412, g = 0.1686, b = 0.8863, a = 1 },
brown = { r = 0.6471, g = 0.1647, b = 0.1647, a = 1 },
burlywood = { r = 0.8706, g = 0.7216, b = 0.5294, a = 1 },
cadetblue = { r = 0.3725, g = 0.6196, b = 0.6275, a = 1 },
chartreuse = { r = 0.4980, g = 1.0000, b = 0.0000, a = 1 },
chocolate = { r = 0.8235, g = 0.4118, b = 0.1176, a = 1 },
coral = { r = 1.0000, g = 0.4980, b = 0.3137, a = 1 },
cornflowerblue = { r = 0.3922, g = 0.5843, b = 0.9294, a = 1 },
cornsilk = { r = 1.0000, g = 0.9725, b = 0.8627, a = 1 },
crimson = { r = 0.8627, g = 0.0784, b = 0.2353, a = 1 },
cyan = { r = 0.0000, g = 1.0000, b = 1.0000, a = 1 },
darkblue = { r = 0.0000, g = 0.0000, b = 0.5451, a = 1 },
darkcyan = { r = 0.0000, g = 0.5451, b = 0.5451, a = 1 },
darkgoldenrod = { r = 0.7216, g = 0.5255, b = 0.0431, a = 1 },
darkgreen = { r = 0.0000, g = 0.3922, b = 0.0000, a = 1 },
darkgrey = { r = 0.6627, g = 0.6627, b = 0.6627, a = 1 },
darkkhaki = { r = 0.7412, g = 0.7176, b = 0.4196, a = 1 },
darkmagenta = { r = 0.5451, g = 0.0000, b = 0.5451, a = 1 },
darkolivegreen = { r = 0.3333, g = 0.4196, b = 0.1843, a = 1 },
darkorange = { r = 1.0000, g = 0.5490, b = 0.0000, a = 1 },
darkorchid = { r = 0.6000, g = 0.1961, b = 0.8000, a = 1 },
darkred = { r = 0.5451, g = 0.0000, b = 0.0000, a = 1 },
darksalmon = { r = 0.9137, g = 0.5882, b = 0.4784, a = 1 },
darkseagreen = { r = 0.5608, g = 0.7373, b = 0.5608, a = 1 },
darkslateblue = { r = 0.2824, g = 0.2392, b = 0.5451, a = 1 },
darkslategray = { r = 0.1843, g = 0.3098, b = 0.3098, a = 1 },
darkslategrey = { r = 0.1843, g = 0.3098, b = 0.3098, a = 1 },
darkturquoise = { r = 0.0000, g = 0.8078, b = 0.8196, a = 1 },
darkviolet = { r = 0.5804, g = 0.0000, b = 0.8275, a = 1 },
deeppink = { r = 1.0000, g = 0.0784, b = 0.5765, a = 1 },
deepskyblue = { r = 0.0000, g = 0.7490, b = 1.0000, a = 1 },
dimgray = { r = 0.4118, g = 0.4118, b = 0.4118, a = 1 },
dimgrey = { r = 0.4118, g = 0.4118, b = 0.4118, a = 1 },
dodgerblue = { r = 0.1176, g = 0.5647, b = 1.0000, a = 1 },
firebrick = { r = 0.6980, g = 0.1333, b = 0.1333, a = 1 },
floralwhite = { r = 1.0000, g = 0.9804, b = 0.9412, a = 1 },
forestgreen = { r = 0.1333, g = 0.5451, b = 0.1333, a = 1 },
fuchsia = { r = 1.0000, g = 0.0000, b = 1.0000, a = 1 },
gainsboro = { r = 0.8627, g = 0.8627, b = 0.8627, a = 1 },
ghostwhite = { r = 0.9725, g = 0.9725, b = 1.0000, a = 1 },
gold = { r = 1.0000, g = 0.8431, b = 0.0000, a = 1 },
goldenrod = { r = 0.8549, g = 0.6471, b = 0.1255, a = 1 },
gray = { r = 0.5020, g = 0.5020, b = 0.5020, a = 1 },
green = { r = 0.0000, g = 1.0000, b = 0.0000, a = 1 },
greenyellow = { r = 0.6784, g = 1.0000, b = 0.1843, a = 1 },
grey = { r = 0.5020, g = 0.5020, b = 0.5020, a = 1 },
honeydew = { r = 0.9412, g = 1.0000, b = 0.9412, a = 1 },
hotpink = { r = 1.0000, g = 0.4118, b = 0.7059, a = 1 },
indianred = { r = 0.8039, g = 0.3608, b = 0.3608, a = 1 },
indigo = { r = 0.2941, g = 0.0000, b = 0.5098, a = 1 },
ivory = { r = 1.0000, g = 1.0000, b = 0.9412, a = 1 },
khaki = { r = 0.9412, g = 0.9020, b = 0.5490, a = 1 },
lavender = { r = 0.9020, g = 0.9020, b = 0.9804, a = 1 },
lavenderblush = { r = 1.0000, g = 0.9412, b = 0.9608, a = 1 },
lawngreen = { r = 0.4863, g = 0.9882, b = 0.0000, a = 1 },
lemonchiffon = { r = 1.0000, g = 0.9804, b = 0.8039, a = 1 },
lightblue = { r = 0.6784, g = 0.8471, b = 0.9020, a = 1 },
lightcoral = { r = 0.9412, g = 0.5020, b = 0.5020, a = 1 },
lightcyan = { r = 0.8784, g = 1.0000, b = 1.0000, a = 1 },
lightgoldenrodyellow = { r = 0.9804, g = 0.9804, b = 0.8235, a = 1 },
lightgray = { r = 0.8275, g = 0.8275, b = 0.8275, a = 1 },
lightgreen = { r = 0.5647, g = 0.9333, b = 0.5647, a = 1 },
lightgrey = { r = 0.8275, g = 0.8275, b = 0.8275, a = 1 },
lightpink = { r = 1.0000, g = 0.7137, b = 0.7569, a = 1 },
lightsalmon = { r = 1.0000, g = 0.6275, b = 0.4784, a = 1 },
lightseagreen = { r = 0.1255, g = 0.6980, b = 0.6667, a = 1 },
lightskyblue = { r = 0.5294, g = 0.8078, b = 0.9804, a = 1 },
lightslategray = { r = 0.4667, g = 0.5333, b = 0.6000, a = 1 },
lightslategrey = { r = 0.4667, g = 0.5333, b = 0.6000, a = 1 },
lightsteelblue = { r = 0.6902, g = 0.7686, b = 0.8706, a = 1 },
lightyellow = { r = 1.0000, g = 1.0000, b = 0.8784, a = 1 },
lime = { r = 0.0000, g = 0.5020, b = 0.0000, a = 1 },
limegreen = { r = 0.1961, g = 0.8039, b = 0.1961, a = 1 },
linen = { r = 0.9804, g = 0.9412, b = 0.9020, a = 1 },
magenta = { r = 1.0000, g = 0.0000, b = 1.0000, a = 1 },
maroon = { r = 0.5020, g = 0.0000, b = 0.0000, a = 1 },
mediumaquamarine = { r = 0.4000, g = 0.8039, b = 0.6667, a = 1 },
mediumblue = { r = 0.0000, g = 0.0000, b = 0.8039, a = 1 },
mediumorchid = { r = 0.7294, g = 0.3333, b = 0.8275, a = 1 },
mediumpurple = { r = 0.5765, g = 0.4392, b = 0.8588, a = 1 },
mediumseagreen = { r = 0.2353, g = 0.7020, b = 0.4431, a = 1 },
mediumslateblue = { r = 0.4824, g = 0.4078, b = 0.9333, a = 1 },
mediumspringgreen = { r = 0.0000, g = 0.9804, b = 0.6039, a = 1 },
mediumturquoise = { r = 0.2824, g = 0.8196, b = 0.8000, a = 1 },
mediumvioletred = { r = 0.7804, g = 0.0824, b = 0.5216, a = 1 },
midnightblue = { r = 0.0980, g = 0.0980, b = 0.4392, a = 1 },
mintcream = { r = 0.9608, g = 1.0000, b = 0.9804, a = 1 },
mistyrose = { r = 1.0000, g = 0.8941, b = 0.8824, a = 1 },
moccasin = { r = 1.0000, g = 0.8941, b = 0.7098, a = 1 },
navajowhite = { r = 1.0000, g = 0.8706, b = 0.6784, a = 1 },
navy = { r = 0.0000, g = 0.0000, b = 0.5020, a = 1 },
oldlace = { r = 0.9922, g = 0.9608, b = 0.9020, a = 1 },
olive = { r = 0.5020, g = 0.5020, b = 0.0000, a = 1 },
olivedrab = { r = 0.4196, g = 0.5569, b = 0.1373, a = 1 },
orange = { r = 1.0000, g = 0.6471, b = 0.0000, a = 1 },
orangered = { r = 1.0000, g = 0.2706, b = 0.0000, a = 1 },
orchid = { r = 0.8549, g = 0.4392, b = 0.8392, a = 1 },
palegoldenrod = { r = 0.9333, g = 0.9098, b = 0.6667, a = 1 },
palegreen = { r = 0.5961, g = 0.9843, b = 0.5961, a = 1 },
paleturquoise = { r = 0.6863, g = 0.9333, b = 0.9333, a = 1 },
palevioletred = { r = 0.8588, g = 0.4392, b = 0.5765, a = 1 },
papayawhip = { r = 1.0000, g = 0.9373, b = 0.8353, a = 1 },
peachpuff = { r = 1.0000, g = 0.8549, b = 0.7255, a = 1 },
peru = { r = 0.8039, g = 0.5216, b = 0.2471, a = 1 },
pink = { r = 1.0000, g = 0.7529, b = 0.7961, a = 1 },
plum = { r = 0.8667, g = 0.6275, b = 0.8667, a = 1 },
powderblue = { r = 0.6902, g = 0.8784, b = 0.9020, a = 1 },
purple = { r = 0.5020, g = 0.0000, b = 0.5020, a = 1 },
red = { r = 1.0000, g = 0.0000, b = 0.0000, a = 1 },
rosybrown = { r = 0.7373, g = 0.5608, b = 0.5608, a = 1 },
royalblue = { r = 0.2549, g = 0.4118, b = 0.8824, a = 1 },
saddlebrown = { r = 0.5451, g = 0.2706, b = 0.0745, a = 1 },
salmon = { r = 0.9804, g = 0.5020, b = 0.4471, a = 1 },
sandybrown = { r = 0.9569, g = 0.6431, b = 0.3765, a = 1 },
seagreen = { r = 0.1804, g = 0.5451, b = 0.3412, a = 1 },
seashell = { r = 1.0000, g = 0.9608, b = 0.9333, a = 1 },
sienna = { r = 0.6275, g = 0.3216, b = 0.1765, a = 1 },
silver = { r = 0.7529, g = 0.7529, b = 0.7529, a = 1 },
skyblue = { r = 0.5294, g = 0.8078, b = 0.9216, a = 1 },
slateblue = { r = 0.4157, g = 0.3529, b = 0.8039, a = 1 },
slategray = { r = 0.4392, g = 0.5020, b = 0.5647, a = 1 },
slategrey = { r = 0.4392, g = 0.5020, b = 0.5647, a = 1 },
snow = { r = 1.0000, g = 0.9804, b = 0.9804, a = 1 },
springgreen = { r = 0.0000, g = 1.0000, b = 0.4980, a = 1 },
steelblue = { r = 0.2745, g = 0.5098, b = 0.7059, a = 1 },
tan = { r = 0.8235, g = 0.7059, b = 0.5490, a = 1 },
teal = { r = 0.0000, g = 0.5020, b = 0.5020, a = 1 },
thistle = { r = 0.8471, g = 0.7490, b = 0.8471, a = 1 },
tomato = { r = 1.0000, g = 0.3882, b = 0.2784, a = 1 },
turquoise = { r = 0.2510, g = 0.8784, b = 0.8157, a = 1 },
violet = { r = 0.9333, g = 0.5098, b = 0.9333, a = 1 },
wheat = { r = 0.9608, g = 0.8706, b = 0.7020, a = 1 },
white = { r = 1.0000, g = 1.0000, b = 1.0000, a = 1 },
whitesmoke = { r = 0.9608, g = 0.9608, b = 0.9608, a = 1 },
yellow = { r = 1.0000, g = 1.0000, b = 0.0000, a = 1 },
yellowgreen = { r = 0.6039, g = 0.8039, b = 0.1961, a = 1 }
}

View File

@@ -0,0 +1,55 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/lightcolor')
-- @module defines.lightcolor
-- @see Concepts.Color
-- defines table is automatically required in all mod loading stages.
--- Returns a lighter color of a named color.
-- @table lightcolor
-- @tfield Concepts.Color white defines.color.lightgrey
-- @tfield Concepts.Color grey defines.color.darkgrey
-- @tfield Concepts.Color lightgrey defines.color.grey
-- @tfield Concepts.Color red defines.color.lightred
-- @tfield Concepts.Color green defines.color.lightgreen
-- @tfield Concepts.Color blue defines.color.lightblue
-- @tfield Concepts.Color yellow defines.color.orange
-- @tfield Concepts.Color pink defines.color.purple
local lightcolor = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local lightcolors = {
white = colors.lightgrey,
grey = colors.darkgrey,
lightgrey = colors.grey,
red = colors.lightred,
green = colors.lightgreen,
blue = colors.lightblue,
yellow = colors.orange,
pink = colors.purple
}
local _mt = {
{
__index = function(_, c)
return lightcolors[c] and { r = lightcolors[c]['r'], g = lightcolors[c]['g'], b = lightcolors[c]['b'], a = lightcolors[c]['a'] or 1 } or
{ r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = lightcolors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
}
setmetatable(lightcolor, _mt)
_G.defines = _G.defines or {}
_G.defines.lightcolor = lightcolor
return lightcolor

View File

@@ -0,0 +1,31 @@
--- A defines module for retrieving the number of ticks in 1 unit of time.
-- Extends the Factorio defines table.
-- @module defines.time
-- defines table is automatically required in all mod loading stages.
local SECOND = 60
local MINUTE = SECOND * 60
local HOUR = MINUTE * 60
local DAY = HOUR * 24
local WEEK = DAY * 7
local MONTH = DAY * 30
local YEAR = DAY * 365
--- Returns the number of ticks in a second, minute, hour, day, week, month, or year.
-- @table time
-- @usage local ten_seconds = defines.time.second * 10
local time = {
second = SECOND, -- the number of Factorio ticks in a second
minute = MINUTE, -- the number of Factorio ticks in a second
hour = HOUR, -- the number of Factorio ticks in an hour
day = DAY, -- the number of Factorio ticks in an day
week = WEEK, -- the number of Factorio ticks in a week
month = MONTH, -- the number of Factorio ticks in a month (30 days)
year = YEAR -- the number of Factorio ticks in a year (365 days)
}
_G.defines = _G.defines or {}
_G.defines.time = time
return time

View File

@@ -0,0 +1,202 @@
--- Additional lua globals
-- @module Utils.Globals
_ENV = _ENV or _G
local config = require('__stdlib__/stdlib/config')
local Table = require('__stdlib__/stdlib/utils/table')
local Math = require('__stdlib__/stdlib/utils/math')
local String = require('__stdlib__/stdlib/utils/string')
local STDLIB = {
Math = Math,
String = String,
Table = Table
}
rawset(_ENV, 'STDLIB', STDLIB)
--Since debug can be overridden we define a fallback function here.
local ignored = {
data_traceback = true,
log_trace = true
}
local traceback = type(debug) == 'table' and debug.traceback or function()
return ''
end
rawset(_ENV, 'traceback', traceback)
local data_traceback = type(debug) == 'table' and debug.getinfo and function()
local str = {}
local level = 1
while true do
local trace = debug.getinfo(level)
if trace then
level = level + 1
if (trace.what == 'Lua' or trace.what == 'main') and not ignored[trace.name] then
local cur = trace.source:gsub('.*__stdlib__', '__stdlib__'):gsub('.*/Factorio%-Stdlib', '__stdlib__')
cur = cur .. ':' .. (trace.currentline or '0') .. ' in ' .. (trace.name or '???')
str[#str + 1] = cur
end
if trace.what == 'main' then
break
end
else
break
end
end
return ' [' .. table.concat(str, ', ') .. ']'
end or function()
return ''
end
rawset(_ENV, 'data_traceback', data_traceback)
local inspect = require('__stdlib__/stdlib/vendor/inspect')
rawset(_ENV, 'inspect', inspect)
-- Defines Mutates
require('__stdlib__/stdlib/utils/defines/color')
require('__stdlib__/stdlib/utils/defines/anticolor')
require('__stdlib__/stdlib/utils/defines/lightcolor')
require('__stdlib__/stdlib/utils/defines/time')
--- Require a file that may not exist
-- @tparam string module path to the module
-- @tparam boolean suppress_all suppress all errors, not just file_not_found
-- @treturn mixed
local function prequire(module, suppress_all)
local ok, err = pcall(require, module)
if ok then
return err
elseif not suppress_all and not err:find('^module .* not found') then
error(err)
end
end
rawset(_ENV, 'prequire', prequire)
--- Temporarily removes __tostring handlers and calls tostring
-- @tparam mixed t object to call rawtostring on
-- @treturn string
local function rawtostring(t)
local m = getmetatable(t)
if m then
local f = m.__tostring
m.__tostring = nil
local s = tostring(t)
m.__tostring = f
return s
else
return tostring(t)
end
end
rawset(_ENV, 'rawtostring', rawtostring)
--- Returns t if the expression is true. f if false
-- @tparam mixed exp The expression to evaluate
-- @tparam mixed t the true return
-- @tparam mixed f the false return
-- @treturn boolean
local function inline_if(exp, t, f)
if exp then
return t
else
return f
end
end
rawset(_ENV, 'inline_if', inline_if)
local function concat(lhs, rhs)
return tostring(lhs) .. tostring(rhs)
end
rawset(_ENV, 'concat', concat)
--- install the Table library into global table
function STDLIB.install_table()
for k, v in pairs(Table) do
_G.table[k] = v
end
end
--- Install the Math library into global math
function STDLIB.install_math()
for k, v in pairs(Math) do
_G.math[k] = v
end
end
--- Install the string library into global string
function STDLIB.install_string()
for k, v in pairs(String) do
_G.string[k] = v
end
setmetatable(string, nil)
end
--- Install Math, String, Table into their global counterparts.
function STDLIB.install_global_utils()
STDLIB.install.math()
STDLIB.install.string()
STDLIB.install.table()
end
--- Reload a required file, NOT IMPLEMENTED
function STDLIB.reload_class()
end
--- load the stdlib into globals, by default it loads everything into an ALLCAPS name.
-- Alternatively you can pass a dictionary of `[global names] -> [require path]`.
-- @tparam[opt] table files
-- @usage
-- STDLIB.create_stdlib_globals()
function STDLIB.create_stdlib_globals(files)
files =
files or
{
GAME = 'stdlib/game',
AREA = 'stdlib/area/area',
POSITION = 'stdlib/area/position',
TILE = 'stdlib/area/tile',
SURFACE = 'stdlib/area/surface',
CHUNK = 'stdlib/area/chunk',
COLOR = 'stdlib/utils/color',
ENTITY = 'stdlib/entity/entity',
INVENTORY = 'stdlib/entity/inventory',
RESOURCE = 'stdlib/entity/resource',
CONFIG = 'stdlib/misc/config',
LOGGER = 'stdlib/misc/logger',
QUEUE = 'stdlib/misc/queue',
EVENT = 'stdlib/event/event',
GUI = 'stdlib/event/gui',
PLAYER = 'stdlib/event/player',
FORCE = 'stdlib/event/force',
TABLE = 'stdlib/utils/table',
STRING = 'stdlib/utils/string',
MATH = 'stdlib/utils/math'
}
for glob, path in pairs(files) do
rawset(_ENV, glob, require('__stdlib__/' .. (path:gsub('%.', '/')))) -- extra () required to emulate select(1)
end
end
function STDLIB.create_stdlib_data_globals(files)
files =
files or
{
RECIPE = 'stdlib/data/recipe',
ITEM = 'stdlib/data/item',
FLUID = 'stdlib/data/fluid',
ENTITY = 'stdlib/data/entity',
TECHNOLOGY = 'stdlib/data/technology',
CATEGORY = 'stdlib/data/category',
DATA = 'stdlib/data/data',
TABLE = 'stdlib/utils/table',
STRING = 'stdlib/utils/string',
MATH = 'stdlib/utils/math',
COLOR = 'stdlib/utils/color'
}
STDLIB.create_stdlib_globals(files)
end
return STDLIB

View File

@@ -0,0 +1,458 @@
--- Is expression library
-- @module Utils.Is
-- @usage
-- local Is = require('__stdlib__/stdlib/utils/is')
-- Is.True(true)
-- Is.Not.True(false)
-- Is.Assert.True(true)
-- Is.Assert.Not.True(false)
--- Is Table
-- @section Table
--- Is the test true.
-- @table Is
-- @field Not Is the test not true.
-- @field Assert Assert that the test is true.
-- @field Assert.Not Assert that the test is not true.
--- Is Table Callers
-- @section Callers
--- Is the test truthy
-- @function Is
-- @tparam mixed var
-- @treturn boolean
local Is = {}
--- Is the test not truthy
-- @function Not
-- @tparam mixed var
-- @treturn boolean
Is.Not = {}
--- Assert that the test is Truthy
-- @function Assert
-- @tparam mixed var
-- @treturn boolean
Is.Assert = {}
--- Assert that the test is not Truthy
-- @function Assert.Not
-- @tparam mixed var
-- @treturn boolean
Is.Assert.Not = {}
--- Functions
-- @section Functions
local M = {}
local type = type
local floor = math.floor
local huge = math.huge
--- Returns the var if the passed variable is a table.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Table(var)
return type(var) == 'table' and var
end
M.table = M.Table
--- Returns the var if the passed variable is a string.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.String(var)
return type(var) == 'string' and var
end
M.string = M.String
--- Returns the var if the passed variable is a number.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Number(var)
return type(var) == 'number' and var
end
M.number = M.Number
function M.Thread(var)
return type(var) == 'thread' and var
end
M.thread = M.Thread
function M.Userdata(var)
return type(var) == 'userdata' and var
end
M.userdata = M.Userdata
--- Returns true if the passed variable is nil.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Nil(var)
return type(var) == 'nil'
end
M.is_nil = M.Nil
--- Returns true if the passed variable is a boolean.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Boolean(var)
return type(var) == 'boolean'
end
M.boolean = M.boolean
--- Returns true if the passed variable is true
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.True(var)
return var == true
end
M.is_true = M.True
--- Returns the var if the passed variable is not nil or false.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Truthy(var)
return var or false
end
M.truthy = M.Truthy
--- Returns true if the passed variable is false.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.False(var)
return var == false
end
M.is_false = M.False
--- Returns true if the passed variable is false or nil.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Falsy(var)
return not var
end
M.falsy = M.Falsy
--- Returns true if the passed variable is nil, an empty table, or an empty string.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Empty(var)
if M.Table(var) then
return table_size and table_size(var) == 0 or next(var) == nil
elseif M.String(var) then
return #string == 0
end
return M.Nil(var)
end
M.empty = M.Empty
function M.None(var)
return M.Empty(var) or M.False(var) or var == 0 or var ~= var
end
M.none = M.None
--- Returns the passed var if it is positive.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Positive(var)
return M.Number(var) and var >= 0 and var
end
M.positive = M.Positive
--- Returns the passed var if it is odd.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.odd(var)
return M.number(var) and (var % 2 ~= 0) and var
end
--- Returns the passed var if it is even.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.even(var)
return M.number(var) and (var % 2 == 0) and var
end
--- Returns the passed var if it is negative.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Negative(var)
return M.Number(var) and var < 0 and var
end
M.negative = M.Negative
--- Returns the passed var if it is not a number.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.NaN(var)
return var ~= var
end
M.nan = M.NaN
--- Returns the passed var if it is finite.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Finite(var)
return M.Number(var) and (var < huge and var > -huge) and var
end
M.finite = M.Finite
--- Returns the passed var if it is an int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int(var)
return M.Finite(var) and rawequal(floor(var), var) and var
end
M.int = M.Int
--- Returns the passed var if it is an int8.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int8(var)
return M.Int(var) and var >= -128 and var <= 127 and var
end
M.int8 = M.Int8
--- Returns the passed var if it is an int16.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int16(var)
return M.Int(var) and var >= -32768 and var <= 32767 and var
end
M.int16 = M.Int16
--- Returns the passed var if it is an int32.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int32(var)
return M.Int(var) and var >= -2147483648 and var <= 2147483647 and var
end
M.int32 = M.Int32
--- Returns the passed var if it is an unsigned int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Unsigned(var)
return Is.Number(var) and (var < huge and var >= 0) and var
end
M.unsigned = M.Unsigned
--- Returns the passed var if it is an unsigned int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt(var)
return M.Unsigned(var) and rawequal(floor(var), var) and var
end
M.uint = M.UInt
--- Returns the passed var if it is an unsigned int8.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt8(var)
return M.UInt(var) and var <= 255 and var
end
M.uint8 = M.UInt8
--- Returns the passed var if it is an unsigned int16.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt16(var)
return M.UInt(var) and var <= 65535 and var
end
M.uint16 = M.UInt16
--- Returns the passed var if it is an unsigned int32.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt32(var)
return M.UInt(var) and var <= 4294967295 and var
end
M.uint32 = M.UInt32
--- Returns the passed var if it is a float.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Float(var)
return M.number(var) and var >= 0 and var < 1 and var
end
M.float = M.Float
--- Returns the passed var if it is a full position.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Position(var)
return M.Table(var) and (var.x and var.y) and var
end
M.position = M.Position
--- Returns the passed var if it is a full area.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Area(var)
return M.Table(var) and (M.Position(var.left_top) and M.Position(var.right_bottom)) and var
end
M.area = M.Area
--- Returns the passed var if it is a simple position/vector.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Vector(var)
return M.Table(var) and ((M.Number(var[1]) and M.Number(var[2])) or M.Position(var)) and var
end
M.vector = M.Vector
--- Returns the passed var if it is a simple area/boundingbox.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.BoundingBox(var)
return M.Table(var) and (M.Vector(var[1]) and M.Vector(var[2]))
end
M.boundingbox = M.BoundingBox
M.bounding_box = M.BoundingBox
M.Bounding_Box = M.BoundingBox
--- Returns the hex value of the passed var if it is hexadecimal.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Hex(var)
return M.String(var) and var:match('%x%x%x%x%x%x$')
end
M.hex = M.Hex
--- Returns true if the passed variable is a single alphbetical word.
-- Does not allow any special chars
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.StrictWord(var)
return M.String(var) and var:find('^[%a]+$') == 1
end
M.strict_word = M.StrictWord
--- Returns true if the passed variable is a single alphbetical word.
-- Allows _ and - as part of the word
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.AlphabetWord(var)
return M.String(var) and var:find('^[%a%_%-]+$') == 1
end
M.Word = M.AlphabetWord
--- Returns true if the passed variable is a single alphbetical word.
-- Must start with a letter, allows _ and - as part of the word
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.AlphanumWord(var)
return M.String(var) and var:find('^%a+[%w%_%-]*$') == 1
end
M.Alpha = M.AlphanumWord
M.alpha = M.AlphanumWord
M.alphanumword = M.AlphanumWord
--- Is this a factorio object
-- @tparam LuaObject var The variable to check
-- @treturn mixed the var if this is an LuaObject
function M.Object(var)
return M.Table(var) and var.__self and var
end
M.object = M.Object
--- Is this factorio object valid
-- @tparam LuaObject var The variable to check
-- @treturn mixed the var if this is a valid LuaObject
function M.Valid(var)
return M.Object(var) and var.valid and var
end
M.valid = M.Valid
--- Returns true if the passed variable is a callable function.
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a callable function
function M.Callable(var)
return type(var) == 'function' or type((getmetatable(var) or {}).__call) == 'function'
end
M.callable = M.Callable
M.Function = M.Callable
M.is_function = M.Callable
setmetatable(
Is,
{
__index = function(_, k)
return M[k] and function(_assert)
return M[k](_assert)
end or nil
end,
__call = function(_, ...)
return (...)
end
}
)
setmetatable(
Is.Not,
{
__index = function(_, k)
return M[k] and function(_assert)
return not M[k](_assert)
end or nil
end,
__call = function(_, ...)
return not (...)
end
}
)
Is.is_not = Is.Not
-- convenience function for un-lambda-ing deferred error messages
local function safe_invoke(f)
local ok, msg = xpcall(f, debug.traceback)
if not ok then
-- ensure msg really is a string so there is theoretically no chance
-- of a triple fault (i.e.: from a monkey-patched debug.traceback
-- returning something that now fails to concatenate to a string)
if type(msg) == 'string' then
msg = '<<< DOUBLE FAULT: ' .. msg .. ' >>>'
end
end
-- for sanity-preservation, always return something truthy
return msg or 'Unknown Error'
end
setmetatable(
Is.Assert,
{
__index = function(_, k)
return M[k] and function(_assert, _message, _level)
_level = tonumber(_level) or 3 ---@type integer
return M[k](_assert) or error(type(_message) == 'function' and safe_invoke(_message) or _message or 'assertion failed', _level)
end or nil
end,
__call = function(_, ...)
local param = { ... }
local _level = tonumber(param[3]) or 3 --[[@as integer]]
return param[1] or error(type(param[2]) == 'function' and safe_invoke(param[2]) or param[2] or 'assertion failed', _level)
end
}
)
Is.assert = Is.Assert
setmetatable(
Is.Assert.Not,
{
__index = function(_, k)
return M[k] and function(_assert, _message, _level)
_level = tonumber(_level) or 3 ---@type integer
return not M[k](_assert) or error(type(_message) == 'function' and safe_invoke(_message) or _message or 'assertion failed', _level)
end or nil
end,
__call = function(_, ...)
local param = { ... }
local _level = tonumber(param[3]) or 3 --[[@as integer]]
return not param[1] or error(type(param[2]) == 'function' and safe_invoke(param[2]) or param[2] or 'assertion failed', _level)
end
}
)
Is.assert.is_not = Is.Assert.Not
return Is

View File

@@ -0,0 +1,95 @@
--- Iterator library.
-- @module Utils.Iter
local Iter = {}
local pairs = pairs
local ipairs = ipairs
Iter.pairs = pairs
Iter.ipairs = ipairs
function Iter.spairs(t, order)
-- collect the keys
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(
keys,
function(a, b)
return order(t, a, b)
end
)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
function Iter.top(t, stop)
local start = #t
stop = stop or 1
return function()
if start >= stop and t[start] ~= nil then
local cur = start
start = start - 1
return cur, t[cur]
end
return nil
end
end
function Iter.wrap(t, start, reverse)
--[[
-- Returns an iterator that iterates over integer keys in table `t` from the specified start position, wrapping
-- around and ending when it reaches `start` again.
--
-- `dir` specifies the direction to iterate (negative values for reverse, otherwise forward)
-- `start` specifies the start location. If `nil`, the first returned item will be at the at the start of the table
-- (or the end of the table, if `dir` is negative)
--
-- Behavior if the table changes size during iteration is undefined.
]]
local dir = (reverse and -1) or 1
local len = #t
local i = start
start = start or (reverse and len) or 1
return function()
if i == nil then
i = start
return i, t[i]
end
i = i + dir
if i < 1 then
i = i + len
elseif i > len then
i = i - len
end
if i == start then
return nil
end
return i, t[i]
end
end
function Iter.tpairs(...)
return ipairs(type(...) == 'table' and ... or { ... })
end
return Iter

View File

@@ -0,0 +1,287 @@
--- Extends Lua 5.2 math.
-- @module Utils.math
-- @see math
-- @usage local math = require('__stdlib__/stdlib/utils/math')
local Math = {}
Math.frexp = math.frexp
Math.sqrt = math.sqrt
Math.asin = math.asin
Math.random = math.random
Math.huge = math.huge
Math.abs = math.abs
Math.ldexp = math.ldexp
Math.exp = math.exp
Math.pow = math.pow
Math.pi = math.pi
Math.tan = math.tan
Math.acos = math.acos
Math.ceil = math.ceil
Math.atan2 = math.atan2
Math.tanh = math.tanh
Math.sin = math.sin
Math.min = math.min
Math.deg = math.deg
Math.sinh = math.sinh
Math.rad = math.rad
Math.randomseed = math.randomseed
Math.cosh = math.cosh
Math.modf = math.modf
Math.cos = math.cos
Math.atan = math.atan
Math.max = math.max
Math.log = math.log
Math.fmod = math.fmod
Math.floor = math.floor
for k, v in pairs(math) do if not Math[k] then Math[k] = v end end
local math_abs = math.abs
local math_floor = math.floor
local math_ceil = math.ceil
local math_min = math.min
local math_max = math.max
local math_huge = math.huge
local math_pi = math.pi
local math_log = math.log
local unpack = table.unpack
-- (( Math Constants
Math.DEG2RAD = math_pi / 180
Math.RAD2DEG = 180 / math_pi
Math.EPSILON = 1.401298e-45
Math.MAXINT8 = 128
Math.MININT8 = -128
Math.MAXUINT8 = 255
Math.MAX_INT8 = Math.MAXINT8
Math.MIN_INT8 = Math.MININT8
Math.MAX_UINT8 = Math.MAXUINT8
Math.MAXINT16 = 32768
Math.MININT16 = -32768
Math.MAXUINT16 = 65535
Math.MAX_INT16 = Math.MAXINT16
Math.MIN_INT16 = Math.MININT16
Math.MAX_UINT16 = Math.MAXUINT16
Math.MAXINT = 2147483648
Math.MAX_INT = Math.MAXINT
Math.MAXINT32 = Math.MAXINT
Math.MAX_INT32 = Math.MAXINT
Math.MAXUINT = 4294967296
Math.MAX_UINT = Math.MAXUINT
Math.MAXUINT32 = Math.MAXUINT
Math.MAX_UINT32 = Math.MAXUINT
Math.MININT = -2147483648
Math.MIN_INT = Math.MININT
Math.MININT32 = Math.MININT
Math.MIN_INT32 = Math.MININT
Math.MAXINT64 = 9223372036854775808
Math.MININT64 = -9223372036854775808
Math.MAXUINT64 = 18446744073709551615
Math.MAX_INT64 = Math.MAXINT64
Math.MIN_INT64 = Math.MININT64
Math.MAX_UINT64 = Math.MAXUINT64
-- ))
local function tuple(...)
return type(...) == 'table' and ... or { ... }
end
function Math.log10(x)
return math_log(x, 10)
end
--- Round a number.
-- @tparam number x
-- @treturn number the rounded number
function Math.round(x)
return x >= 0 and math_floor(x + 0.5) or math_ceil(x - 0.5)
end
-- Returns the number x rounded to p decimal places.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to round to
-- @treturn number rounded to p decimal spaces.
function Math.round_to(x, p)
local e = 10 ^ (p or 0)
return math_floor(x * e + 0.5) / e
end
-- Returns the number floored to p decimal spaces.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to floor to
-- @treturn number floored to p decimal spaces.
function Math.floor_to(x, p)
if (p or 0) == 0 then return math_floor(x) end
local e = 10 ^ p
return math_floor(x * e) / e
end
-- Returns the number ceiled to p decimal spaces.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to ceil to
-- @treturn number ceiled to p decimal spaces.
function Math.ceil_to(x, p)
local e = 10 ^ (p or 0)
return math_ceil(x * e + 0.5) / e
end
-- Various average (means) algorithms implementation
-- See: http://en.wikipedia.org/wiki/Average
--- Calculates the sum of a sequence of values.
-- @tparam tuple ... a tuple of numbers
-- @treturn the sum
function Math.sum(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + v end
return s
end
--- Calculates the arithmetic mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the arithmetic mean
function Math.arithmetic_mean(...)
local x = tuple(...)
return (Math.sum(x) / #x)
end
Math.avg = Math.arithmetic_mean
Math.average = Math.arithmetic_mean
--- Calculates the geometric mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the geometric mean
function Math.geometric_mean(...)
local x = tuple(...)
local prod = 1
for _, v in ipairs(x) do prod = prod * v end
return (prod ^ (1 / #x))
end
--- Calculates the harmonic mean of a set of values.
-- @tparam tuple ... an array of numbers
-- @treturn number the harmonic mean
function Math.harmonic_mean(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + (1 / v) end
return (#x / s)
end
--- Calculates the quadratic mean of a set of values.
-- @tparam tuple ... an array of numbers
-- @treturn number the quadratic mean
function Math.quadratic_mean(...)
local x = tuple(...)
local squares = 0
for _, v in ipairs(x) do squares = squares + (v * v) end
return math.sqrt((1 / #x) * squares)
end
--- Calculates the generalized mean (to a specified power) of a set of values.
-- @tparam number p power
-- @tparam tuple ... an array of numbers
-- @treturn number the generalized mean
function Math.generalized_mean(p, ...)
local x = tuple(...)
local sump = 0
for _, v in ipairs(x) do sump = sump + (v ^ p) end
return ((1 / #x) * sump) ^ (1 / p)
end
--- Calculates the weighted mean of a set of values.
-- @tparam array x an array of numbers
-- @tparam array w an array of number weights for each value
-- @treturn number the weighted mean
function Math.weighted_mean(x, w)
local sump = 0
for i, v in ipairs(x) do sump = sump + (v * w[i]) end
return sump / Math.sum(w)
end
--- Calculates the midrange mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the midrange mean
function Math.midrange_mean(...)
local x = tuple(...)
return 0.5 * (math_min(unpack(x)) + math_max(unpack(x)))
end
--- Calculates the energetic mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the energetic mean
function Math.energetic_mean(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + (10 ^ (v / 10)) end
return 10 * Math.log10((1 / #x) * s)
end
--- Returns the number x clamped between the numbers min and max.
-- @tparam number x
-- @tparam[opt=0] number min
-- @tparam[opt=1] number max
-- @treturn number clamped between min and max
function Math.clamp(x, min, max)
min, max = min or 0, max or 1
return x < min and min or (x > max and max or x)
end
--- Linear interpolation or 2 numbers.
-- @tparam number a
-- @tparam number b
-- @tparam float amount
-- @treturn number
function Math.lerp(a, b, amount)
return a + (b - a) * Math.clamp(amount, 0, 1)
end
--- Smooth.
-- @tparam number a
-- @tparam number b
-- @tparam float amount
-- @treturn number
function Math.smooth(a, b, amount)
local t = Math.clamp(amount, 0, 1)
local m = t * t * (3 - 2 * t)
return a + (b - a) * m
end
--- Approximately the same
-- @tparam number a
-- @tparam number b
-- @treturn boolean
function Math.approximately(a, b)
return math_abs(b - a) < math_max(1e-6 * math_max(math_abs(a), math_abs(b)), 1.121039e-44)
end
--- Is x a number.
-- @tparam number x
-- @treturn boolean
function Math.is_number(x)
return x == x and x ~= math_huge
end
--- Is x an integer.
-- @tparam number x
-- @treturn boolean
function Math.is_integer(x)
return x == math_ceil(x)
end
--- Is x unsigned.
-- @tparam number x
-- @treturn boolean
function Math.is_unsigned(x)
return x >= 0
end
return Math

View File

@@ -0,0 +1,284 @@
--- Extends Lua 5.2 string.
-- @module Utils.string
-- @see string
-- @usage local string = require('__stdlib__/stdlib/utils/string')
local String = {}
String.find = string.find
String.lower = string.lower
String.gmatch = string.gmatch
String.sub = string.sub
String.byte = string.byte
String.char = string.char
String.reverse = string.reverse
String.dump = string.dump
String.rep = string.rep
String.format = string.format
String.match = string.match
String.gsub = string.gsub
String.len = string.len
String.upper = string.upper
getmetatable('').__index = String -- Allow string syntatic sugar to work with this class
for k, v in pairs(string) do if not String[k] then String[k] = v end end
local concat = table.concat
local insert = table.insert
local ceil = math.ceil
local abs = math.abs
--- Returns a copy of the string with any leading or trailing whitespace from the string removed.
-- @tparam string s the string to remove leading or trailing whitespace from
-- @treturn string a copy of the string without leading or trailing whitespace
function String.trim(s)
return (s:gsub([[^%s*(.-)%s*$]], '%1'))
end
--- Tests if a string starts with a given substring.
-- @tparam string s the string to check for the start substring
-- @tparam string start the substring to test for
-- @treturn boolean true if the start substring was found in the string
function String.starts_with(s, start)
return s:find(start, 1, true) == 1
end
--- Tests if a string ends with a given substring.
-- @tparam string s the string to check for the end substring
-- @tparam string ends the substring to test for
-- @treturn boolean true if the end substring was found in the string
function String.ends_with(s, ends)
return #s >= #ends and s:find(ends, #s - #ends + 1, true) and true or false
end
--- Tests if a string contains a given substring.
-- @tparam string s the string to check for the substring
-- @tparam string contains the substring to test for
-- @treturn boolean true if the substring was found in the string
function String.contains(s, contains)
return s and s:find(contains) ~= nil
end
--- Tests whether a string is empty.
-- @tparam string s the string to test
-- @treturn boolean true if the string is empty
function String.is_empty(s)
return s == nil or s == ''
end
--- does s only contain alphabetic characters?
-- @string s a string
function String.is_alpha(s)
return s:find('^%a+$') == 1
end
--- does s only contain digits?
-- @string s a string
function String.is_digit(s)
return s:find('^%d+$') == 1
end
--- does s only contain alphanumeric characters?
-- @string s a string
function String.is_alnum(s)
return s:find('^%w+$') == 1
end
--- does s only contain spaces?
-- @string s a string
function String.is_space(s)
return s:find('^%s+$') == 1
end
--- does s only contain lower case characters?
-- @string s a string
function String.is_lower(s)
return s:find('^[%l%s]+$') == 1
end
--- does s only contain upper case characters?
-- @string s a string
function String.is_upper(s)
return s:find('^[%u%s]+$') == 1
end
--- iniital word letters uppercase ('title case').
-- Here 'words' mean chunks of non-space characters.
-- @string s the string
-- @return a string with each word's first letter uppercase
function String.title(s)
return (s:gsub([[(%S)(%S*)]], function(f, r)
return f:upper() .. r:lower()
end))
end
local ellipsis = '...'
local n_ellipsis = #ellipsis
--- Return a shortened version of a string.
-- Fits string within w characters. Removed characters are marked with ellipsis.
-- @string s the string
-- @int w the maxinum size allowed
-- @bool tail true if we want to show the end of the string (head otherwise)
-- @usage ('1234567890'):shorten(8) == '12345...'
-- @usage ('1234567890'):shorten(8, true) == '...67890'
-- @usage ('1234567890'):shorten(20) == '1234567890'
function String.shorten(s, w, tail)
if #s > w then
if w < n_ellipsis then return ellipsis:sub(1, w) end
if tail then
local i = #s - w + 1 + n_ellipsis
return ellipsis .. s:sub(i)
else
return s:sub(1, w - n_ellipsis) .. ellipsis
end
end
return s
end
--- concatenate the strings using this string as a delimiter.
-- @string s the string
-- @param seq a table of strings or numbers
-- @usage (' '):join {1,2,3} == '1 2 3'
function String.join(s, seq)
return concat(seq, s)
end
local function _just(s, w, ch, left, right)
local n = #s
if w > n then
if not ch then ch = ' ' end
local f1, f2
if left and right then
local rn = ceil((w - n) / 2)
local ln = w - n - rn
f1 = ch:rep(ln)
f2 = ch:rep(rn)
elseif right then
f1 = ch:rep(w - n)
f2 = ''
else
f2 = ch:rep(w - n)
f1 = ''
end
return f1 .. s .. f2
else
return s
end
end
--- left-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.ljust(s, w, ch)
return _just(s, w, ch, true, false)
end
--- right-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.rjust(s, w, ch)
return _just(s, w, ch, false, true)
end
--- center-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.center(s, w, ch)
return _just(s, w, ch, true, true)
end
local noop = function(...)
return ...
end
--- Splits a string into an array.
-- Note: Empty split substrings are not included in the resulting table.
-- <p>For example, `string.split("foo.bar...", ".", false)` results in the table `{"foo", "bar"}`.
-- @tparam string s the string to split
-- @tparam[opt="."] string sep the separator to use.
-- @tparam[opt=false] boolean pattern whether to interpret the separator as a lua pattern or plaintext for the string split
-- @tparam[opt] function func pass each split string through this function.
-- @treturn {string,...} an array of strings
function String.split(s, sep, pattern, func)
sep = sep or '.'
sep = sep ~= '' and sep or '.'
sep = not pattern and sep:gsub('([^%w])', '%%%1') or sep
func = func or noop
local fields = {}
local start_idx, end_idx = s:find(sep)
local last_find = 1
while start_idx do
local substr = s:sub(last_find, start_idx - 1)
if substr:len() > 0 then table.insert(fields, func(s:sub(last_find, start_idx - 1))) end
last_find = end_idx + 1
start_idx, end_idx = s:find(sep, end_idx + 1)
end
local substr = s:sub(last_find)
if substr:len() > 0 then insert(fields, func(s:sub(last_find))) end
return fields
end
--- Return the ordinal suffix for a number.
-- @tparam number n
-- @tparam boolean prepend_number if the passed number should be pre-pended
-- @treturn string the ordinal suffix
function String.ordinal_suffix(n, prepend_number)
if tonumber(n) then
n = abs(n) % 100
local d = n % 10
if d == 1 and n ~= 11 then
return (prepend_number and n or '') .. 'st'
elseif d == 2 and n ~= 12 then
return (prepend_number and n or '') .. 'nd'
elseif d == 3 and n ~= 13 then
return (prepend_number and n or '') .. 'rd'
else
return (prepend_number and n or '') .. 'th'
end
end
return prepend_number and n
end
local exponent_multipliers = {
['y'] = 0.000000000000000000000001,
['z'] = 0.000000000000000000001,
['a'] = 0.000000000000000001,
['f'] = 0.000000000000001,
['p'] = 0.000000000001,
['n'] = 0.000000001,
['u'] = 0.000001,
['m'] = 0.001,
['c'] = 0.01,
['d'] = 0.1,
[' '] = 1,
['h'] = 100,
['k'] = 1000,
['M'] = 1000000,
['G'] = 1000000000,
['T'] = 1000000000000,
['P'] = 1000000000000000,
['E'] = 1000000000000000000,
['Z'] = 1000000000000000000000,
['Y'] = 1000000000000000000000000
}
--- Convert a metric string prefix to a number value.
-- @tparam string str
-- @treturn float
function String.exponent_number(str)
if type(str) == 'string' then
local value, exp = str:match('([%-+]?[0-9]*%.?[0-9]+)([yzafpnumcdhkMGTPEZY]?)') ---@diagnostic disable-line: spell-check
exp = exp or ' '
value = (value or 0) * (exponent_multipliers[exp] or 1)
return value
elseif type(str) == 'number' then
return str
end
return 0
end
return String

View File

@@ -0,0 +1,574 @@
--- Extends Lua 5.2 table.
-- @module Utils.table
-- @see table
-- @usage local table = require('__stdlib__/stdlib/utils/table')
local Table = {}
Table.remove = table.remove
Table.sort = table.sort
Table.pack = table.pack
Table.unpack = table.unpack
Table.insert = table.insert
Table.concat = table.concat
-- Import base lua table into Table
for k, v in pairs(table) do if not Table[k] then Table[k] = v end end
--- Given a mapping function, creates a transformed copy of the table
-- by calling the function for each element in the table, and using
-- the result as the new value for the key. Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.map(a, function(v) return v * 10 end) --produces: { 10, 20, 30, 40, 50 }
-- @usage a = {1, 2, 3, 4, 5}
-- table.map(a, function(v, k, x) return v * k + x end, 100) --produces { 101, 104, 109, 116, 125}
-- @tparam table tbl the table to be mapped to the transform
-- @tparam function func the function to transform values
-- @param[opt] ... additional arguments passed to the function
-- @treturn table a new table containing the keys and mapped values
function Table.map(tbl, func, ...)
local new_tbl = {}
for k, v in pairs(tbl) do new_tbl[k] = func(v, k, ...) end
return new_tbl
end
--- Given a filter function, creates a filtered copy of the table
-- by calling the function for each element in the table, and
-- filtering out any key-value pairs for non-true results. Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.filter(a, function(v) return v % 2 == 0 end) --produces: { 2, 4 }
-- @usage a = {1, 2, 3, 4, 5}
-- table.filter(a, function(v, k, x) return k % 2 == 1 end) --produces: { 1, 3, 5 }
-- @tparam table tbl the table to be filtered
-- @tparam function func the function to filter values
-- @param[opt] ... additional arguments passed to the function
-- @treturn table a new table containing the filtered key-value pairs
function Table.filter(tbl, func, ...)
local new_tbl = {}
local add = #tbl > 0
for k, v in pairs(tbl) do
if func(v, k, ...) then
if add then
Table.insert(new_tbl, v)
else
new_tbl[k] = v
end
end
end
return new_tbl
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns the first element the search function returned true.
-- Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.find(a, function(v) return v % 2 == 0 end) --produces: 2
-- @usage a = {1, 2, 3, 4, 5}
-- table.find(a, function(v, k, x) return k % 2 == 1 end) --produces: 1
-- @tparam table tbl the table to be searched
-- @tparam function func the function to use to search for any matching element
-- @param[opt] ... additional arguments passed to the function
-- @treturn ?|nil|Mixed the first found value, or nil if none was found
function Table.find(tbl, func, ...)
for k, v in pairs(tbl) do if func(v, k, ...) then return v, k end end
return nil
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns true if search function returned true.
-- Passes the index as second argument to the function.
-- @see table.find
-- @usage a= { 1, 2, 3, 4, 5}
-- table.any(a, function(v) return v % 2 == 0 end) --produces: true
-- @usage a = {1, 2, 3, 4, 5}
-- table.any(a, function(v, k, x) return k % 2 == 1 end) --produces: true
-- @tparam table tbl the table to be searched
-- @tparam function func the function to use to search for any matching element
-- @param[opt] ... additional arguments passed to the function
-- @treturn boolean true if an element was found, false if none was found
function Table.any(tbl, func, ...)
return Table.find(tbl, func, ...) ~= nil
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns true if search function returned true
-- for all items in the table.
-- Passes the index as second argument to the function.
-- @tparam table tbl the table to be searched
-- @tparam function func the function to used to search
-- @param[opt] ... additional arguments passed to the search function
-- @treturn boolean true if all elements in the table return truthy
function Table.all(tbl, func, ...)
for k, v in pairs(tbl) do if not func(v, k, ...) then return false end end
return true
end
--- Given a function, apply it to each element in the table.
-- Passes the index as the second argument to the function.
-- <p>Iteration is aborted if the applied function returns true for any element during iteration.
-- @usage
-- a = {10, 20, 30, 40}
-- table.each(a, function(v) game.print(v) end) --prints 10, 20, 30, 40, 50
-- @tparam table tbl the table to be iterated
-- @tparam function func the function to apply to elements
-- @param[opt] ... additional arguments passed to the function
-- @treturn table the table where the given function has been applied to its elements
function Table.each(tbl, func, ...)
for k, v in pairs(tbl) do if func(v, k, ...) then break end end
return tbl
end
--- Returns a new array that is a one-dimensional recursive flattening of the given array.
-- For every element that is an array, extract its elements into the new array.
-- <p>The optional level argument determines the level of recursion to flatten.
-- > This function flattens an integer-indexed array, but not an associative array.
-- @tparam array tbl the array to be flattened
-- @tparam[opt] uint level recursive levels, or no limit to recursion if not supplied
-- @treturn array a new array that represents the flattened contents of the given array
function Table.flatten(tbl, level)
local flattened = {}
Table.each(tbl, function(value)
if type(value) == 'table' and #value > 0 then
if level then
if level > 0 then
Table.merge(flattened, Table.flatten(value, level - 1), true)
else
Table.insert(flattened, value)
end
else
Table.merge(flattened, Table.flatten(value), true)
end
else
Table.insert(flattened, value)
end
end)
return flattened
end
--- Given an array, returns the first element or nil if no element exists.
-- @tparam array tbl the array
-- @treturn ?|nil|Mixed the first element
function Table.first(tbl)
return tbl[1]
end
--- Given an array, returns the last element or nil if no elements exist.
-- @tparam array tbl the array
-- @treturn ?|nil|Mixed the last element or nil
function Table.last(tbl)
local size = #tbl
if size == 0 then return nil end
return tbl[size]
end
--- Given an array of only numeric values, returns the minimum or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the minimum value
function Table.min(tbl)
if #tbl == 0 then return nil end
local min = tbl[1]
for _, num in pairs(tbl) do min = num < min and num or min end
return min
end
---Given an array of only numeric values, returns the maximum or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the maximum value
function Table.max(tbl)
if #tbl == 0 then return nil end
local max = tbl[1]
for _, num in pairs(tbl) do max = num > max and num or max end
return max
end
--- Given an array of only numeric values, return the sum of all values, or 0 for empty arrays.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn number the sum of the numbers or zero if the given array was empty
function Table.sum(tbl)
local sum = 0
for _, num in pairs(tbl) do sum = sum + num end
return sum
end
--- Given an array of only numeric values, returns the average or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the average value
function Table.avg(tbl)
local cnt = #tbl
return cnt ~= 0 and Table.sum(tbl) / cnt or nil
end
--- Return a new array slice.
-- @tparam array tbl the table to slice
-- @tparam[opt=1] number start
-- @tparam[opt=#tbl] number stop stop at this index, use negative to stop from end.
-- @usage local tab = { 10, 20, 30, 40, 50}
-- slice(tab, 2, -2) --returns { 20, 30, 40 }
function Table.slice(tbl, start, stop)
local res = {}
local n = #tbl
start = start or 1
stop = stop or n
stop = stop < 0 and (n + stop + 1) or stop
if start < 1 or start > n then return {} end
local k = 1
for i = start, stop do
res[k] = tbl[i]
k = k + 1
end
return res
end
--- Merges two tables, values from first get overwritten by the second.
-- @usage
-- function some_func(x, y, args)
-- args = table.merge({option1=false}, args)
-- if opts.option1 == true then return x else return y end
-- end
-- some_func(1,2) -- returns 2
-- some_func(1,2,{option1=true}) -- returns 1
-- @tparam table tblA first table
-- @tparam table tblB second table
-- @tparam[opt=false] boolean array_merge set to true to merge the tables as an array or false for an associative array
-- @tparam[opt=false] boolean raw use rawset for associated array
-- @treturn array|table an array or an associated array where tblA and tblB have been merged
function Table.merge(tblA, tblB, array_merge, raw)
if not tblB then return tblA end
if array_merge then
for _, v in pairs(tblB) do Table.insert(tblA, v) end
else
for k, v in pairs(tblB) do
if raw then
rawset(tblA, k, v)
else
tblA[k] = v
end
end
end
return tblA
end
function Table.array_combine(...)
local tables = { ... }
local new = {}
for _, tab in pairs(tables) do for _, v in pairs(tab) do Table.insert(new, v) end end
return new
end
function Table.dictionary_combine(...)
local tables = { ... }
local new = {}
for _, tab in pairs(tables) do for k, v in pairs(tab) do new[k] = v end end
return new
end
--- Creates a new merged dictionary, if the values in tbl_b are in tbl_a they are not overwritten.
-- @usage
-- local a = {one = A}
-- local b = {one = Z, two = B}
-- local merged = table.dictionary_merge(tbl_a, tbl_b)
-- --merged = {one = A, two = B}
-- @tparam table tbl_a
-- @tparam table tbl_b
-- @treturn table with a and b merged together
function Table.dictionary_merge(tbl_a, tbl_b)
local meta_a = getmetatable(tbl_a)
local meta_b = getmetatable(tbl_b)
setmetatable(tbl_a, nil)
setmetatable(tbl_b, nil)
local new_t = {}
for k, v in pairs(tbl_a) do new_t[k] = v end
for k, v in pairs(tbl_b or {}) do if not new_t[k] then new_t[k] = v end end
setmetatable(tbl_a, meta_a)
setmetatable(tbl_b, meta_b)
return new_t
end
--- Compares 2 tables for inner equality.
-- Modified from factorio/data/core/lualib/util.lua
-- @tparam table t1
-- @tparam table t2
-- @tparam[opt=false] boolean ignore_mt ignore eq metamethod
-- @treturn boolean if the tables are the same
-- @author Sparr, Nexela, luacode.org
function Table.deep_compare(t1, t2, ignore_mt)
local ty1, ty2 = type(t1), type(t2)
if ty1 ~= ty2 then return false end
-- non-table types can be directly compared
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
-- as well as tables which have the metamethod __eq
if not ignore_mt then
local mt = getmetatable(t1)
if mt and mt.__eq then return t1 == t2 end
end
for k1, v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not Table.deep_compare(v1, v2) then return false end
end
for k in pairs(t2) do if t1[k] == nil then return false end end
return true
end
Table.compare = Table.deep_compare
--- Creates a deep copy of table without copying Factorio objects.
-- copied from factorio/data/core/lualib/util.lua
-- @usage local copy = table.deep_copy[data.raw.["stone-furnace"]["stone-furnace"]]
-- -- returns a copy of the stone furnace entity
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.deep_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return lookup_table[inner]
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.deepcopy = Table.deep_copy
--- Creates a deep copy of a table without copying factorio objects
-- internal table refs are also deepcopy. The resulting table should
-- @usage local copy = table.fullcopy[data.raw.["stone-furnace"]["stone-furnace"]]
-- -- returns a deepcopy of the stone furnace entity with no internal table references.
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.full_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return _copy(lookup_table[inner])
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.fullcopy = Table.full_copy
--- Creates a flexible deep copy of an object, recursively copying sub-objects
-- @usage local copy = table.flexcopy(data.raw.["stone-furnace"]["stone-furnace"])
-- -- returns a copy of the stone furnace entity
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.flex_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return lookup_table[inner]
elseif type(inner._copy_with) == 'function' then
lookup_table[inner] = inner:_copy_with(_copy)
return lookup_table[inner]
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.flexcopy = Table.flex_copy
--- Returns a copy of all of the values in the table.
-- @tparam table tbl the table to copy the keys from, or an empty table if tbl is nil
-- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs()
-- @tparam[opt] boolean as_string whether to try and parse the values as strings, or leave them as their existing type
-- @treturn array an array with a copy of all the values in the table
function Table.values(tbl, sorted, as_string)
if not tbl then return {} end
local value_set = {}
local n = 0
if as_string then -- checking as_string /before/ looping is faster
for _, v in pairs(tbl) do
n = n + 1
value_set[n] = tostring(v)
end
else
for _, v in pairs(tbl) do
n = n + 1
value_set[n] = v
end
end
if sorted then
table.sort(value_set, function(x, y) -- sorts tables with mixed index types.
local tx = type(x) == 'number'
local ty = type(y) == 'number'
if tx == ty then
return x < y and true or false -- similar type can be compared
elseif tx == true then
return true -- only x is a number and goes first
else
return false -- only y is a number and goes first
end
end)
end
return value_set
end
--- Returns a copy of all of the keys in the table.
-- @tparam table tbl the table to copy the keys from, or an empty table if tbl is nil
-- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs()
-- @tparam[opt] boolean as_string whether to try and parse the keys as strings, or leave them as their existing type
-- @treturn array an array with a copy of all the keys in the table
function Table.keys(tbl, sorted, as_string)
if not tbl then return {} end
local key_set = {}
local n = 0
if as_string then -- checking as_string /before/ looping is faster
for k, _ in pairs(tbl) do
n = n + 1
key_set[n] = tostring(k)
end
else
for k, _ in pairs(tbl) do
n = n + 1
key_set[n] = k
end
end
if sorted then
table.sort(key_set, function(x, y) -- sorts tables with mixed index types.
local tx = type(x) == 'number'
local ty = type(y) == 'number'
if tx == ty then
return x < y and true or false -- similar type can be compared
elseif tx == true then
return true -- only x is a number and goes first
else
return false -- only y is a number and goes first
end
end)
end
return key_set
end
--- Removes keys from a table by setting the values associated with the keys to nil.
-- @usage local a = {1, 2, 3, 4}
-- table.remove_keys(a, {1,3}) --returns {nil, 2, nil, 4}
-- @usage local b = {k1 = 1, k2 = 'foo', old_key = 'bar'}
-- table.remove_keys(b, {'old_key'}) --returns {k1 = 1, k2 = 'foo'}
-- @tparam table tbl the table to remove the keys from
-- @tparam {Mixed,...} keys an array of keys that exist in the given table
-- @treturn table tbl without the specified keys
function Table.remove_keys(tbl, keys)
for i = 1, #keys do tbl[keys[i]] = nil end
return tbl
end
--- Returns the number of keys in a table, if func is passed only count keys when the function is true.
-- @tparam table tbl to count keys
-- @tparam[opt] function func to increment counter
-- @param[optchain] ... additional arguments passed to the function
-- @treturn number The number of keys matching the function or the number of all keys if func isn't passed
-- @treturn number The total number of keys
-- @usage local a = { 1, 2, 3, 4, 5}
-- table.count_keys(a) -- produces: 5, 5
-- @usage local a = {1, 2, 3, 4, 5}
-- table.count_keys(a, function(v, k) return k % 2 == 1 end) -- produces: 3, 5
function Table.count_keys(tbl, func, ...)
local count, total = 0, 0
if type(tbl) == 'table' then
for k, v in pairs(tbl) do
total = total + 1
if func then
if func(v, k, ...) then count = count + 1 end
else
count = count + 1
end
end
end
return count, total
end
--- Returns an inverted (***{[value] = key,...}***) copy of the given table. If the values are not unique,
-- the assigned key depends on the order of pairs().
-- @usage local a = {k1 = 'foo', k2 = 'bar'}
-- table.invert(a) --returns {'foo' = k1, 'bar' = k2}
-- @usage local b = {k1 = 'foo', k2 = 'bar', k3 = 'bar'}
-- table.invert(b) --returns {'foo' = k1, 'bar' = ?}
-- @tparam table tbl the table to invert
-- @treturn table a new table with inverted mapping
function Table.invert(tbl)
local inverted = {}
for k, v in pairs(tbl) do inverted[v] = k end
return inverted
end
local function _size(tbl)
local count = 0
for _ in pairs(tbl or {}) do count = count + 1 end
return count
end
--- Return the size of a table using the factorio built in table_size function
-- @function size
-- @tparam table table to use
-- @treturn int size of the table
Table.size = _ENV.table_size or _size
--- For all string or number values in an array map them to a value = value table
-- @usage local a = {"v1", "v2"}
-- table.array_to_bool(a) -- return {["v1"] = "v1", ["v2"]= "v2"}
-- @tparam table tbl the table to convert
-- @tparam[opt=false] boolean as_bool map to true instead of value
-- @treturn table the converted table
function Table.array_to_dictionary(tbl, as_bool)
local new_tbl = {}
for _, v in ipairs(tbl) do
if type(v) == 'string' or type(v) == 'number' then new_tbl[v] = as_bool and true or v end
end
return new_tbl
end
-- Returns an array of unique values from tbl
-- @tparam table tbl
-- @treturn table an array of unique values.
function Table.unique_values(tbl)
return Table.keys(Table.invert(tbl))
end
--- Does the table contain any elements
-- @tparam table tbl
-- @treturn boolean
function Table.is_empty(tbl)
return _ENV.table_size and _ENV.table_size(tbl) == 0 or next(tbl) == nil
end
--- Clear all elements in a table
-- @tparam table tbl the table to clear
-- @treturn table the cleared table
function Table.clear(tbl)
for k in pairs(tbl) do tbl[k] = nil end
return tbl
end
return Table

View File

@@ -0,0 +1,11 @@
local type = type
local Type = {
Table = function(param) return type(param) == 'table' end,
Function = function(param) return type(param) == 'function' or type((getmetatable(param) or {}).__call) == 'function' end,
Nil = function(param) return param == nil end,
String = function(param) return type(param) == 'string' end,
Number = function(param) return type(param) == 'number' end,
}
return Type