[modified] renamed to batteries, added global export suppression and more documentation

This commit is contained in:
Max Cahill 2020-03-15 20:28:50 +11:00
parent 0dd92b2c2e
commit 66a9f41474
11 changed files with 462 additions and 224 deletions

View File

@ -5,7 +5,7 @@
todo: collect some stats on classes/optional global class registry todo: collect some stats on classes/optional global class registry
]] ]]
function class(inherits) return local function class(inherits)
local c = {} local c = {}
c.__mt = {__index = c} c.__mt = {__index = c}
--handle single inheritence --handle single inheritence

62
colour.lua Normal file
View File

@ -0,0 +1,62 @@
--[[
colour handling stuff
feel free to alias to colour
]]
local bit = require("bit")
local band, bor = bit.band, bit.bor
local lshift, rshift = bit.lshift, bit.rshift
local colour = {}
function colour.packRGB(r, g, b)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb )
end
function colour.packARGB(r, g, b, a)
local ba = lshift(band(0xff, a * 255), 24)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb, ba )
end
function colour.packRGBA(r, g, b, a)
local br = lshift(band(0xff, r * 255), 24)
local bg = lshift(band(0xff, g * 255), 16)
local bb = lshift(band(0xff, b * 255), 8)
local ba = lshift(band(0xff, a * 255), 0)
return bor( br, bg, bb, ba )
end
function colour.unpackARGB(argb)
local r = rshift(band(argb, 0x00ff0000), 16) / 255.0
local g = rshift(band(argb, 0x0000ff00), 8) / 255.0
local b = rshift(band(argb, 0x000000ff), 0) / 255.0
local a = rshift(band(argb, 0xff000000), 24) / 255.0
return r, g, b, a
end
function colour.unpackRGBA(rgba)
local r = rshift(band(rgba, 0xff000000), 24) / 255.0
local g = rshift(band(rgba, 0x00ff0000), 16) / 255.0
local b = rshift(band(rgba, 0x0000ff00), 8) / 255.0
local a = rshift(band(rgba, 0x000000ff), 0) / 255.0
return r, g, b, a
end
function colour.unpackRGB(rgb)
local r = rshift(band(rgb, 0x00ff0000), 16) / 255.0
local g = rshift(band(rgb, 0x0000ff00), 8) / 255.0
local b = rshift(band(rgb, 0x000000ff), 0) / 255.0
local a = 1.0
return r, g, b, a
end
--todo: hsl, hsv, other colour spaces
return colour

View File

@ -1,29 +1,25 @@
--[[ --[[
functional programming facilities functional programming facilities
notes:
be careful about creating closures in hot loops.
this is this module's achilles heel - there's no special
syntax for closures so it's not apparent that you're suddenly
allocating at every call
reduce has a similar problem, but at least arguments
there are clear!
optional:
set BATTERIES_FUNCTIONAL_MODULE to a table before requiring
if you don't want this to modify the global `table` table
]] ]]
--collect all keys of a table into a sequence local _table = BATTERIES_FUNCTIONAL_MODULE or table
function table.keys(t)
local r = {}
for k,v in pairs(t) do
table.insert(r, k)
end
return r
end
--collect all values of a table into a sequence
--(shallow copy if it's already a sequence)
function table.values(t)
local r = sequence:new()
for k,v in pairs(t) do
table.insert(r, v)
end
return r
end
--simple sequential iteration, f is called for all elements of t --simple sequential iteration, f is called for all elements of t
--f can return non-nil to break the loop (and return the value) --f can return non-nil to break the loop (and return the value)
function table.foreach(t, f) function _table.foreach(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
local r = f(v, i) local r = f(v, i)
if r ~= nil then if r ~= nil then
@ -35,7 +31,7 @@ end
--performs a left to right reduction of t using f, with o as the initial value --performs a left to right reduction of t using f, with o as the initial value
-- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3) -- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3)
-- (but performed iteratively, so no stack smashing) -- (but performed iteratively, so no stack smashing)
function table.reduce(t, f, o) function _table.reduce(t, f, o)
for i,v in ipairs(t) do for i,v in ipairs(t) do
o = f(o, v) o = f(o, v)
end end
@ -44,7 +40,7 @@ end
--maps a sequence {a, b, c} -> {f(a), f(b), f(c)} --maps a sequence {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter) -- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter)
function table.map(t, f) function _table.map(t, f)
local r = {} local r = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
local mapped = f(v, i) local mapped = f(v, i)
@ -55,8 +51,25 @@ function table.map(t, f)
return r return r
end end
--maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils, which can be used to simultaneously map and filter,
-- but this results in a linear table.remove so "careful" for big working sets)
function _table.remap(t, f)
local i = 1
while i <= #t do
local mapped = f(t[i])
if mapped ~= nil then
t[i] = mapped
i = i + 1
else
table.remove(t, i)
end
end
return t
end
--filters a sequence --filters a sequence
function table.filter(t, f) function _table.filter(t, f)
local r = {} local r = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v, i) then if f(v, i) then
@ -67,7 +80,7 @@ function table.filter(t, f)
end end
--partitions a sequence based on filter criteria --partitions a sequence based on filter criteria
function table.partition(t, f) function _table.partition(t, f)
local a = {} local a = {}
local b = {} local b = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
@ -84,7 +97,7 @@ end
--iteration limited by min(#t1, #t2) --iteration limited by min(#t1, #t2)
--function receives arguments (t1, t2, i) --function receives arguments (t1, t2, i)
--nil results ignored --nil results ignored
function table.zip(t1, t2, f) function _table.zip(t1, t2, f)
local ret = {} local ret = {}
local limit = math.min(#t2, #t2) local limit = math.min(#t2, #t2)
for i=1, limit do for i=1, limit do
@ -101,9 +114,9 @@ end
--return a copy of a sequence with all duplicates removed --return a copy of a sequence with all duplicates removed
-- causes a little "extra" gc churn; one table and one closure -- causes a little "extra" gc churn; one table and one closure
-- as well as the copied deduped table -- as well as the copied deduped table
function table.dedupe(t) function _table.dedupe(t)
local seen = {} local seen = {}
return table.filter(t, function(v) return _table.filter(t, function(v)
if seen[v] then if seen[v] then
return false return false
end end
@ -113,15 +126,15 @@ function table.dedupe(t)
end end
--append sequence t2 into t1, modifying t1 --append sequence t2 into t1, modifying t1
function table.append_inplace(t1, t2) function _table.append_inplace(t1, t2)
table.foreach(t2, function(v) for i,v in ipairs(t2) do
table.insert(t1, v) table.insert(t1, v)
end) end
return t1 return t1
end end
--return a new sequence with the elements of both t1 and t2 --return a new sequence with the elements of both t1 and t2
function table.append(t1, t2) function _table.append(t1, t2)
local r = {} local r = {}
append_inplace(r, t1) append_inplace(r, t1)
append_inplace(r, t2) append_inplace(r, t2)
@ -133,7 +146,7 @@ end
----------------------------------------------------------- -----------------------------------------------------------
--true if any element of the table matches f --true if any element of the table matches f
function table.any(t, f) function _table.any(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return true return true
@ -143,7 +156,7 @@ function table.any(t, f)
end end
--true if no element of the table matches f --true if no element of the table matches f
function table.none(t, f) function _table.none(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return false return false
@ -153,7 +166,7 @@ function table.none(t, f)
end end
--true if all elements of the table match f --true if all elements of the table match f
function table.all(t, f) function _table.all(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if not f(v) then if not f(v) then
return false return false
@ -163,7 +176,7 @@ function table.all(t, f)
end end
--counts the elements of t that match f --counts the elements of t that match f
function table.count(t, f) function _table.count(t, f)
local c = 0 local c = 0
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
@ -174,7 +187,7 @@ function table.count(t, f)
end end
--true if the table contains element e --true if the table contains element e
function table.contains(t, e) function _table.contains(t, e)
for i, v in ipairs(t) do for i, v in ipairs(t) do
if v == e then if v == e then
return true return true
@ -184,48 +197,53 @@ function table.contains(t, e)
end end
--return the numeric sum of all elements of t --return the numeric sum of all elements of t
function table.sum(t) function _table.sum(t)
return table.reduce(t, function(a, b) return _table.reduce(t, function(a, b)
return a + b return a + b
end, 0) end, 0)
end end
--return the numeric mean of all elements of t --return the numeric mean of all elements of t
function table.mean(t) function _table.mean(t)
local len = #t local len = #t
if len == 0 then if len == 0 then
return 0 return 0
end end
return table.sum(t) / len return _table.sum(t) / len
end end
--return the minimum and maximum of t in one pass --return the minimum and maximum of t in one pass
function table.minmax(t) --or zero for both if t is empty
local a = table.reduce(t, function(a, b) -- (would perhaps more correctly be math.huge, -math.huge
a.min = a.min and math.min(a.min, b) or b -- but that tends to be surprising/annoying in practice)
a.max = a.max and math.max(a.max, b) or b function _table.minmax(t)
return a local max, min
end, {}) for i,v in ipairs(t) do
if a.min == nil then min = not min and v or math.min(min, v)
a.min = 0 max = not max and v or math.max(max, v)
a.max = 0
end end
return a.min, a.max if min == nil then
min = 0
max = 0
end
return min, max
end end
function table.max(t) --return the maximum element of t or zero if t is empty
local min, max = table.minmax(t) function _table.max(t)
local min, max = _table.minmax(t)
return max return max
end end
function table.min(t) --return the minimum element of t or zero if t is empty
local min, max = table.minmax(t) function _table.min(t)
local min, max = _table.minmax(t)
return min return min
end end
--return the element of the table that results in the greatest numeric value --return the element of the table that results in the greatest numeric value
--(function receives element and key respectively, table evaluated in pairs order) --(function receives element and key respectively, table evaluated in pairs order)
function table.find_best(t, f) function _table.find_best(t, f)
local current = nil local current = nil
local current_best = -math.huge local current_best = -math.huge
for k,e in pairs(t) do for k,e in pairs(t) do
@ -239,14 +257,15 @@ function table.find_best(t, f)
end end
--return the element of the table that results in the value nearest to the passed value --return the element of the table that results in the value nearest to the passed value
function table.find_nearest(t, f, v) --todo: optimise as this generates a closure each time
return table.find_best(t, function(e) function _table.find_nearest(t, f, v)
return _table.find_best(t, function(e)
return -math.abs(f(e) - v) return -math.abs(f(e) - v)
end) end)
end end
--return the first element of the table that results in a true filter --return the first element of the table that results in a true filter
function table.find_match(t, f) function _table.find_match(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return v return v
@ -254,3 +273,5 @@ function table.find_match(t, f)
end end
return nil return nil
end end
return _table

View File

@ -1,35 +1,92 @@
--[[ --[[
core modules batteries for lua
if required as the "entire library" (ie by this file), puts everything into if required as the "entire library" (ie by this file), puts everything into
global namespace as it'll presumably be commonly used global namespace by default as it'll presumably be commonly used
if not, several of the modules work as "normal" modules and return a table if not, several of the modules work as normal lua modules and return a table
for local-friendly use for local-friendly use
the others that modify some global table can be talked into behaving as normal
lua modules as well by setting appropriate globals prior to inclusion
you can avoid modifying any global namespace by setting
BATTERIES_NO_GLOBALS = true
before requiring, then everything can be accessed as eg
batteries.table.stable_sort
]] ]]
local path = ... local path = ...
local function relative_file(p) local function require_relative(p)
return table.concat({path, p}, ".") return require(table.concat({path, p}, "."))
end end
require(relative_file("oo")) if BATTERIES_NO_GLOBALS then
--define local tables for everything to go into
BATTERIES_MATH_MODULE = {}
BATTERIES_TABLE_MODULE = {}
BATTERIES_FUNCTIONAL_MODULE = {}
end
require(relative_file("math")) local _class = require_relative("class")
require(relative_file("table")) local _math = require_relative("math")
require(relative_file("stable_sort"))
require(relative_file("functional")) local _table = require_relative("table")
sequence = require(relative_file("sequence")) local _stable_sort = require_relative("stable_sort")
unique_mapping = require(relative_file("unique_mapping"))
vec2 = require(relative_file("vec2")) local _functional = require_relative("functional")
vec3 = require(relative_file("vec3")) local _sequence = require_relative("sequence")
intersect = require(relative_file("intersect"))
state_machine = require(relative_file("state_machine")) local _vec2 = require_relative("vec2")
local _vec3 = require_relative("vec3")
local _intersect = require_relative("intersect")
async = require(relative_file("async")) local _unique_mapping = require_relative("unique_mapping")
local _state_machine = require_relative("state_machine")
manual_gc = require(relative_file("manual_gc")) local _async = require_relative("async")
local _manual_gc = require_relative("manual_gc")
local _colour = require_relative("colour")
--export globally if required
if not BATTERIES_NO_GLOBALS then
class = _class
sequence = _sequence
vec2 = _vec2
vec3 = _vec3
intersect = _intersect
unique_mapping = _unique_mapping
state_machine = _state_machine
async = _async
manual_gc = _manual_gc
--support both spellings
colour = _colour
color = _colour
end
--either way, export to package registry
return {
class = _class,
math = _math,
table = _table,
stable_sort = _stable_sort,
functional = _functional,
sequence = _sequence,
vec2 = _vec2,
vec3 = _vec3,
intersect = _intersect,
unique_mapping = _unique_mapping,
state_machine = _state_machine,
async = _async,
manual_gc = _manual_gc,
colour = _colour,
}

109
math.lua
View File

@ -1,11 +1,15 @@
--[[ --[[
extra mathematical functions extra mathematical functions
optional:
set BATTERIES_MATH_MODULE to a table before requiring
if you don't want this to modify the global `math` table
]] ]]
local bit = require("bit") local _math = BATTERIES_MATH_MODULE or math
--wrap v around range [lo, hi) --wrap v around range [lo, hi)
function math.wrap(v, lo, hi) function _math.wrap(v, lo, hi)
local range = hi - lo local range = hi - lo
local relative = v - lo local relative = v - lo
local relative_wrapped = relative % range local relative_wrapped = relative % range
@ -15,51 +19,51 @@ function math.wrap(v, lo, hi)
end end
--clamp v to range [lo, hi] --clamp v to range [lo, hi]
function math.clamp(v, lo, hi) function _math.clamp(v, lo, hi)
return math.max(lo, math.min(v, hi)) return math.max(lo, math.min(v, hi))
end end
function math.clamp01(v) function _math.clamp01(v)
return math.clamp(v, 0, 1) return _math.clamp(v, 0, 1)
end end
--round v to nearest whole --round v to nearest whole
function math.round(v) function _math.round(v)
return math.floor(v + 0.5) return math.floor(v + 0.5)
end end
--round v to one-in x --round v to one-in x
-- (eg x = 2, v rounded to increments of 0.5) -- (eg x = 2, v rounded to increments of 0.5)
function math.to_one_in(v, x) function _math.to_one_in(v, x)
return math.round(v * x) / x return _math.round(v * x) / x
end end
--round v to a given decimal precision --round v to a given decimal precision
function math.to_precision(v, decimal_points) function _math.to_precision(v, decimal_points)
return math.to_one_in(v, math.pow(10, decimal_points)) return _math.to_one_in(v, math.pow(10, decimal_points))
end end
--0, 1, -1 sign of a scalar --0, 1, -1 sign of a scalar
function math.sign(v) function _math.sign(v)
if v < 0 then return -1 end if v < 0 then return -1 end
if v > 0 then return 1 end if v > 0 then return 1 end
return 0 return 0
end end
--linear interpolation between a and b --linear interpolation between a and b
function math.lerp(a, b, t) function _math.lerp(a, b, t)
return a * (1.0 - t) + b * t return a * (1.0 - t) + b * t
end end
--classic smoothstep --classic smoothstep
--(only "safe" for 0-1 range) --(only "safe" for 0-1 range)
function math.smoothstep(v) function _math.smoothstep(v)
return v * v * (3 - 2 * v) return v * v * (3 - 2 * v)
end end
--classic smootherstep; zero 2nd order derivatives at 0 and 1 --classic smootherstep; zero 2nd order derivatives at 0 and 1
--(only safe for 0-1 range) --(only safe for 0-1 range)
function math.smootherstep(v) function _math.smootherstep(v)
return v * v * v * (v * (v * 6 - 15) + 10) return v * v * v * (v * (v * 6 - 15) + 10)
end end
@ -125,7 +129,7 @@ local sparse_primes_1k = {
6689, 7039, 7307, 7559, 7573, 7919, 6689, 7039, 7307, 7559, 7573, 7919,
} }
function math.first_above(v, t) function _math.first_above(v, t)
for _,p in ipairs(t) do for _,p in ipairs(t) do
if p > v then if p > v then
return p return p
@ -134,79 +138,30 @@ function math.first_above(v, t)
return t[#t] return t[#t]
end end
function math.next_prime_1k(v) function _math.next_prime_1k(v)
return math.first_above(v, primes_1k) return _math.first_above(v, primes_1k)
end end
function math.next_prime_1k_sparse(v) function _math.next_prime_1k_sparse(v)
return math.first_above(v, sparse_primes_1k) return _math.first_above(v, sparse_primes_1k)
end
--color handling stuff
local band, bor = bit.band, bit.bor
local lshift, rshift = bit.lshift, bit.rshift
function math.colorToRGB(r, g, b)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb )
end
function math.colorToARGB(r, g, b, a)
local ba = lshift(band(0xff, a * 255), 24)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb, ba )
end
function math.colorToRGBA(r, g, b, a)
local br = lshift(band(0xff, r * 255), 24)
local bg = lshift(band(0xff, g * 255), 16)
local bb = lshift(band(0xff, b * 255), 8)
local ba = lshift(band(0xff, a * 255), 0)
return bor( br, bg, bb, ba )
end
function math.ARGBToColor(argb)
local r = rshift(band(argb, 0x00ff0000), 16) / 255.0
local g = rshift(band(argb, 0x0000ff00), 8) / 255.0
local b = rshift(band(argb, 0x000000ff), 0) / 255.0
local a = rshift(band(argb, 0xff000000), 24) / 255.0
return r, g, b, a
end
function math.RGBAToColor(rgba)
local r = rshift(band(rgba, 0xff000000), 24) / 255.0
local g = rshift(band(rgba, 0x00ff0000), 16) / 255.0
local b = rshift(band(rgba, 0x0000ff00), 8) / 255.0
local a = rshift(band(rgba, 0x000000ff), 0) / 255.0
return r, g, b, a
end
function math.RGBToColor(rgb)
local r = rshift(band(rgb, 0x00ff0000), 16) / 255.0
local g = rshift(band(rgb, 0x0000ff00), 8) / 255.0
local b = rshift(band(rgb, 0x000000ff), 0) / 255.0
local a = 1.0
return r, g, b, a
end end
--angle handling stuff --angle handling stuff
function math.normalise_angle(a) function _math.normalise_angle(a)
return math.wrap(a, -math.pi, math.pi) return _math.wrap(a, -math.pi, math.pi)
end end
function math.relative_angle(a1, a2) function _math.relative_angle(a1, a2)
a1 = math.normalise_angle(a1) a1 = _math.normalise_angle(a1)
a2 = math.normalise_angle(a2) a2 = _math.normalise_angle(a2)
return math.normalise_angle(a1 - a2) return _math.normalise_angle(a1 - a2)
end end
--geometric rotation multi-return --geometric rotation multi-return
function math.rotate(x, y, r) function _math.rotate(x, y, r)
local s = math.sin(r) local s = math.sin(r)
local c = math.cos(r) local c = math.cos(r)
return c * x - s * y, s * x + c * y return c * x - s * y, s * x + c * y
end end
return _math

View File

@ -1,5 +1,38 @@
# lua-core # batteries
Core dependencies for making games with lua, especially with [love](https://love2d.org). Core dependencies for making games with lua, especially with [love](https://love2d.org).
Definitely needs a catchier name, but does a lot of useful stuff to get projects off the ground faster. Does a lot to get projects off the ground faster, filling out lua's standard library and providing implementations of common algorithms and data structures useful for games.
# Module Overview
- `class` - single-inheritance oo in a single function
- `math` - mathematical extensions
- `table` - table handling extensions
- `stable_sort` - a stable sorting algorithm that is also faster than table.sort under luajit
- `functional` - functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`...
- `sequence` - an oo wrapper on sequential tables so you can do `t:insert(i, v)` instead of `table.insert(t, i, v)`. Also supports the functional interfance.
- `vec2` - 2d vectors with method chaining, garbage saving interface
- `vec3` - 3d vectors as above
- `intersect` - 2d intersection routines, a bit sparse at the moment
- `unique_mapping` - generate a unique mapping from arbitrary lua values to numeric keys - essentially making up a consistent ordering for unordered data. niche, but useful for optimising draw batches for example, as you can't sort on textures without it.
- `state_machine` - finite state machine implementation with state transitions and all the rest. useful for game states, ai, cutscenes...
- `async` - async operations as coroutines.
- `manual_gc` - get gc out of your update/draw calls. really good when trying to get accurate profiling information. requires you to think a bit about your garbage budgets though.
- `colour` - colour conversion routines. can also be spelled `color` if you're into that.
# PRs
Pull requests are welcome. If you have something "big" to add please get in touch before starting work, but I'm quite open minded!
# Globals?
By default `batteries` will modify builtin lua tables (such as `table` and `math`) as it sees fit, as this eases consumption later on - you don't have to remember if say, `table.remove_value` is built in to lua or not. As the intended use case is fairly pervasive, required early, and "fire and forget", this sits with me just fine.
If however, you'd prefer to require things locally, you can (rather ironically) set a few globals at boot time as documented in each module to change this behaviour, or set `BATTERIES_NO_GLOBALS = true` to make none of them modify anything global. If you really want to you can undefine the behaviour-changing globals after the module is required, as the results are cached.
I'd strongly recommend that if you find yourself defining the above, stop and think why/if you really want to avoid globals for a library intended to be commonly used across your entire codebase!
Some folks will have good reasons, which is why the functionality is present!
Others may wish to reconsider and save themselves typing batteries a few hundred times :)

View File

@ -7,6 +7,7 @@
in that case, you can still use the table methods that accept a table in that case, you can still use the table methods that accept a table
first as method calls. first as method calls.
]] ]]
local sequence = {} local sequence = {}
sequence.mt = {__index = sequence} sequence.mt = {__index = sequence}
@ -20,52 +21,59 @@ function sequence:new(t)
return setmetatable(t or {}, sequence.mt) return setmetatable(t or {}, sequence.mt)
end end
--import table functions to sequence as-is --alias
sequence.join = table.concat --alias sequence.join = table.concat
--sorting default to stable if present --sorting default to stable if present
sequence.sort = table.stable_sort or table.sort sequence.sort = table.stable_sort or table.sort
--import functional interface to sequence in a sequence preserving way --(handle functional module delegation correctly)
local _func = BATTERIES_FUNCTIONAL_MODULE or table
--import functional interface to sequence in a type preserving way
function sequence:keys() function sequence:keys()
return sequence:new(table.keys(self)) return sequence:new(_func.keys(self))
end end
function sequence:values() function sequence:values()
return sequence:new(table.values(self)) return sequence:new(_func.values(self))
end end
function sequence:foreach(f) function sequence:foreach(f)
return table.foreach(self, f) return _func.foreach(self, f)
end end
function sequence:reduce(f, o) function sequence:reduce(f, o)
return table.foreach(self, f, o) return _func.foreach(self, f, o)
end end
function sequence:map(f) function sequence:map(f)
return sequence:new(table.map(self, f)) return sequence:new(_func.map(self, f))
end
function sequence:remap(f)
return _func.remap(self, f)
end end
function sequence:filter(f) function sequence:filter(f)
return sequence:new(table.filter(self, f)) return sequence:new(_func.filter(self, f))
end end
function sequence:partition(f) function sequence:partition(f)
local a, b = table.partition(self, f) local a, b = _func.partition(self, f)
return sequence:new(a), sequence:new(b) return sequence:new(a), sequence:new(b)
end end
function sequence:zip(other, f) function sequence:zip(other, f)
return sequence:new(table.zip(self, other, f)) return sequence:new(_func.zip(self, other, f))
end end
function sequence:dedupe() function sequence:dedupe()
return table.dedupe(self) return _func.dedupe(self)
end end
function sequence:append_inplace(other) function sequence:append_inplace(other)
return table.append_inplace(self, other) return _func.append_inplace(self, other)
end end
function sequence:append(other) function sequence:append(other)
@ -73,7 +81,7 @@ function sequence:append(other)
end end
function sequence:copy(deep) function sequence:copy(deep)
return sequence:new(table.copy(self, deep)) return sequence:new(_func.copy(self, deep))
end end
return sequence return sequence

View File

@ -1,6 +1,6 @@
-- stable sorting routines for lua -- stable sorting routines for lua
-- --
-- modifies the global table namespace so you don't have -- by default modifies the global table namespace so you don't have
-- to re-require it everywhere. -- to re-require it everywhere.
-- --
-- table.stable_sort -- table.stable_sort
@ -10,6 +10,11 @@
-- table.insertion_sort -- table.insertion_sort
-- an insertion sort, should you prefer it -- an insertion sort, should you prefer it
-- --
-- alternatively, adds them to either BATTERIES_SORT_MODULE or
-- BATTERIES_TABLE_MODULE if defined
--
-- also returns just the interface above as a module either way
--
--this is based on MIT licensed code from Dirk Laurie and Steve Fisher --this is based on MIT licensed code from Dirk Laurie and Steve Fisher
--license as follows: --license as follows:
@ -36,14 +41,15 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
]] ]]
-- (modifications by Max Cahill 2018) -- (modifications by Max Cahill 2018, 2020)
local _sort_core = {} local _sort_core = {}
--tunable size for --tunable size for insertion sort "bottom out"
_sort_core.max_chunk_size = 24 _sort_core.max_chunk_size = 32
function _sort_core.insertion_sort_impl(array, first, last, less) --insertion sort on a section of array
function _sort_core._insertion_sort_impl(array, first, last, less)
for i = first + 1, last do for i = first + 1, last do
local k = first local k = first
local v = array[i] local v = array[i]
@ -59,7 +65,8 @@ function _sort_core.insertion_sort_impl(array, first, last, less)
end end
end end
function _sort_core.merge(array, workspace, low, middle, high, less) --merge sorted adjacent sections of array
function _sort_core._merge(array, workspace, low, middle, high, less)
local i, j, k local i, j, k
i = 1 i = 1
-- copy first half of array to auxiliary array -- copy first half of array to auxiliary array
@ -91,24 +98,25 @@ function _sort_core.merge(array, workspace, low, middle, high, less)
end end
end end
--implementation for the merge sort
function _sort_core.merge_sort_impl(array, workspace, low, high, less) function _sort_core._merge_sort_impl(array, workspace, low, high, less)
if high - low <= _sort_core.max_chunk_size then if high - low <= _sort_core.max_chunk_size then
_sort_core.insertion_sort_impl(array, low, high, less) _sort_core._insertion_sort_impl(array, low, high, less)
else else
local middle = math.floor((low + high) / 2) local middle = math.floor((low + high) / 2)
_sort_core.merge_sort_impl(array, workspace, low, middle, less) _sort_core._merge_sort_impl(array, workspace, low, middle, less)
_sort_core.merge_sort_impl(array, workspace, middle + 1, high, less) _sort_core._merge_sort_impl(array, workspace, middle + 1, high, less)
_sort_core.merge(array, workspace, low, middle, high, less) _sort_core._merge(array, workspace, low, middle, high, less)
end end
end end
function default_less(a, b) --default comparison; hoisted for clarity
local function default_less(a, b)
return a < b return a < b
end end
--inline common setup stuff --inline common setup stuff
function _sort_core.sort_setup(array, less) function _sort_core._sort_setup(array, less)
--default less --default less
less = less or default_less less = less or default_less
-- --
@ -127,28 +135,35 @@ end
function _sort_core.stable_sort(array, less) function _sort_core.stable_sort(array, less)
--setup --setup
local trivial, n, less = _sort_core.sort_setup(array, less) local trivial, n, less = _sort_core._sort_setup(array, less)
if not trivial then if not trivial then
--temp storage; allocate ahead of time --temp storage; allocate ahead of time
local workspace = {} local workspace = {}
local middle = math.ceil(n / 2) local middle = math.ceil(n / 2)
workspace[middle] = array[1] workspace[middle] = array[1]
--dive in --dive in
_sort_core.merge_sort_impl( array, workspace, 1, n, less ) _sort_core._merge_sort_impl( array, workspace, 1, n, less )
end end
return array return array
end end
function _sort_core.insertion_sort(array, less) function _sort_core.insertion_sort(array, less)
--setup --setup
local trivial, n, less = _sort_core.sort_setup(array, less) local trivial, n, less = _sort_core._sort_setup(array, less)
if not trivial then if not trivial then
_sort_core.insertion_sort_impl(array, 1, n, less) _sort_core._insertion_sort_impl(array, 1, n, less)
end end
return array return array
end end
_sort_core.unstable_sort = table.sort
--export sort core --export sort core
table.insertion_sort = _sort_core.insertion_sort
table.stable_sort = _sort_core.stable_sort local _table = BATTERIES_SORT_MODULE or BATTERIES_TABLE_MODULE or table
table.unstable_sort = table.sort
_table.insertion_sort = _sort_core.insertion_sort
_table.stable_sort = _sort_core.stable_sort
_table.unstable_sort = _sort_core.unstable_sort
return _sort_core

151
table.lua
View File

@ -1,36 +1,52 @@
--[[ --[[
extra table routines extra table routines
optional:
set BATTERIES_TABLE_MODULE to a table before requiring
if you don't want this to modify the global `table` table
]] ]]
local _table = BATTERIES_TABLE_MODULE or table
--apply prototype to module if it isn't the global table
if BATTERIES_TABLE_MODULE ~= table then
setmetatable(BATTERIES_TABLE_MODULE, {
__index = table,
})
end
--alias
_table.join = _table.concat
--return the back element of a table --return the back element of a table
function table.back(t) function _table.back(t)
return t[#t] return t[#t]
end end
--remove the back element of a table and return it --remove the back element of a table and return it
function table.pop(t) function _table.pop(t)
return table.remove(t) return table.remove(t)
end end
--insert to the back of a table --insert to the back of a table
function table.push(t, v) function _table.push(t, v)
return table.insert(t, v) return table.insert(t, v)
end end
--remove the front element of a table and return it --remove the front element of a table and return it
function table.shift(t) function _table.shift(t)
return table.remove(t, 1) return table.remove(t, 1)
end end
--insert to the front of a table --insert to the front of a table
function table.unshift(t, v) function _table.unshift(t, v)
return table.insert(t, 1, v) return table.insert(t, 1, v)
end end
--find the index in a sequential table that a resides at --find the index in a sequential table that a resides at
--or nil if nothing was found --or nil if nothing was found
--(todo: consider pairs version?) --(todo: consider pairs version?)
function table.index_of(t, a) function _table.index_of(t, a)
if a == nil then return nil end if a == nil then return nil end
for i,b in ipairs(t) do for i,b in ipairs(t) do
if a == b then if a == b then
@ -42,8 +58,8 @@ end
--remove the first instance of value from a table (linear search) --remove the first instance of value from a table (linear search)
--returns true if the value was removed, else false --returns true if the value was removed, else false
function table.remove_value(t, a) function _table.remove_value(t, a)
local i = table.index_of(t, a) local i = _table.index_of(t, a)
if i then if i then
table.remove(t, i) table.remove(t, i)
return true return true
@ -53,8 +69,8 @@ end
--add a value to a table if it doesn't already exist (linear search) --add a value to a table if it doesn't already exist (linear search)
--returns true if the value was added, else false --returns true if the value was added, else false
function table.add_value(t, a) function _table.add_value(t, a)
local i = table.index_of(t, a) local i = _table.index_of(t, a)
if not i then if not i then
table.insert(t, a) table.insert(t, a)
return true return true
@ -70,7 +86,7 @@ local function _random(min, max, r)
end end
--pick a random value from a table (or nil if it's empty) --pick a random value from a table (or nil if it's empty)
function table.pick_random(t, r) function _table.pick_random(t, r)
if #t == 0 then if #t == 0 then
return nil return nil
end end
@ -78,7 +94,7 @@ function table.pick_random(t, r)
end end
--shuffle the order of a table --shuffle the order of a table
function table.shuffle(t, r) function _table.shuffle(t, r)
for i = 1, #t do for i = 1, #t do
local j = _random(1, #t, r) local j = _random(1, #t, r)
t[i], t[j] = t[j], t[i] t[i], t[j] = t[j], t[i]
@ -87,7 +103,7 @@ function table.shuffle(t, r)
end end
--reverse the order of a table --reverse the order of a table
function table.reverse(t) function _table.reverse(t)
for i = 1, #t / 2 do for i = 1, #t / 2 do
local j = #t - i + 1 local j = #t - i + 1
t[i], t[j] = t[j], t[i] t[i], t[j] = t[j], t[i]
@ -95,28 +111,57 @@ function table.reverse(t)
return t return t
end end
--collect all keys of a table into a sequential table
--(useful if you need to iterate non-changing keys often and want an nyi tradeoff;
-- this call will be slow but then following iterations can use ipairs)
function _table.keys(t)
local r = {}
for k,v in pairs(t) do
table.insert(r, k)
end
return r
end
--collect all values of a keyed table into a sequential table
--(shallow copy if it's already sequential)
function _table.values(t)
local r = {}
for k,v in pairs(t) do
table.insert(r, v)
end
return r
end
--(might already exist depending on luajit) --(might already exist depending on luajit)
if table.clear == nil then if _table.clear == nil then
--destructively clear a numerically keyed table if _table ~= table and table.clear then
--useful when multiple references are floating around --import from global if it exists
--so you cannot just pop a new table out of nowhere _table.clear = table.clear
function table.clear(t) else
assert(type(to) == "table", "table.clear - argument 't' must be a table") --remove all values from a table
while t[1] ~= nil do --useful when multiple references are floating around
table.remove(t) --so you cannot just pop a new table out of nowhere
function _table.clear(t)
assert(type(to) == "table", "table.clear - argument 't' must be a table")
local k = next(t)
while k ~= nil do
t[k] = nil
k = next(t)
end
end end
end end
end end
--overlay one table directly onto another, shallow only --note:
function table.overlay(to, from) -- copies and overlays are currently not satisfactory
assert(type(to) == "table", "table.overlay - argument 'to' must be a table") --
assert(type(from) == "table", "table.overlay - argument 'from' must be a table") -- i feel that copy especially tries to do too much and
for k,v in pairs(from) do -- probably they should be split into separate functions
to[k] = v -- to be both more explicit and performant, ie
end --
return to -- shallow_copy, deep_copy, shallow_overlay, deep_overlay
end --
-- input is welcome on this :)
--copy a table --copy a table
-- deep_or_into is either: -- deep_or_into is either:
@ -128,7 +173,7 @@ end
-- if into specified, copies into that table -- if into specified, copies into that table
-- but doesn't clear anything out -- but doesn't clear anything out
-- (useful for deep overlays and avoiding garbage) -- (useful for deep overlays and avoiding garbage)
function table.copy(t, deep_or_into) function _table.copy(t, deep_or_into)
assert(type(t) == "table", "table.copy - argument 't' must be a table") assert(type(t) == "table", "table.copy - argument 't' must be a table")
local is_bool = type(deep_or_into) == "boolean" local is_bool = type(deep_or_into) == "boolean"
local is_table = type(deep_or_into) == "table" local is_table = type(deep_or_into) == "table"
@ -140,7 +185,7 @@ function table.copy(t, deep_or_into)
if type(v.copy) == "function" then if type(v.copy) == "function" then
v = v:copy() v = v:copy()
else else
v = table.copy(v, deep) v = _table.copy(v, deep)
end end
end end
into[k] = v into[k] = v
@ -148,3 +193,45 @@ function table.copy(t, deep_or_into)
return into return into
end end
--overlay one table directly onto another, shallow only
function _table.overlay(to, from)
assert(type(to) == "table", "table.overlay - argument 'to' must be a table")
assert(type(from) == "table", "table.overlay - argument 'from' must be a table")
for k,v in pairs(from) do
to[k] = v
end
return to
end
--faster unpacking for known-length tables up to 8
--gets around nyi in luajit
--note: you can use a larger unpack than you need as the rest
-- can be discarded, but it "feels dirty" :)
function _table.unpack2(t)
return t[1], t[2],
end
function _table.unpack3(t)
return t[1], t[2], t[3]
end
function _table.unpack4(t)
return t[1], t[2], t[3], t[4]
end
function _table.unpack5(t)
return t[1], t[2], t[3], t[4], t[5]
end
function _table.unpack6(t)
return t[1], t[2], t[3], t[4], t[5], t[6]
end
function _table.unpack7(t)
return t[1], t[2], t[3], t[4], t[5], t[6], t[7]
end
function _table.unpack8(t)
return t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8]
end

View File

@ -5,7 +5,7 @@
--[[ --[[
notes: notes:
depends on a class() function as in oo.lua depends on a class() function as in batteries class.lua
some methods depend on math library extensions some methods depend on math library extensions

View File

@ -5,7 +5,7 @@
--[[ --[[
notes: notes:
depends on a class() function as in oo.lua depends on a class() function as in batteries class.lua
some methods depend on math library extensions some methods depend on math library extensions