diff --git a/oo.lua b/class.lua similarity index 96% rename from oo.lua rename to class.lua index 89491eb..58a2314 100644 --- a/oo.lua +++ b/class.lua @@ -5,7 +5,7 @@ todo: collect some stats on classes/optional global class registry ]] -function class(inherits) +return local function class(inherits) local c = {} c.__mt = {__index = c} --handle single inheritence diff --git a/colour.lua b/colour.lua new file mode 100644 index 0000000..9860120 --- /dev/null +++ b/colour.lua @@ -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 \ No newline at end of file diff --git a/functional.lua b/functional.lua index 085900e..b1e2012 100644 --- a/functional.lua +++ b/functional.lua @@ -1,29 +1,25 @@ --[[ 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 -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 +local _table = BATTERIES_FUNCTIONAL_MODULE or table --simple sequential iteration, f is called for all elements of t --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 local r = f(v, i) 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 -- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3) -- (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 o = f(o, v) end @@ -44,7 +40,7 @@ end --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) -function table.map(t, f) +function _table.map(t, f) local r = {} for i,v in ipairs(t) do local mapped = f(v, i) @@ -55,8 +51,25 @@ function table.map(t, f) return r 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 -function table.filter(t, f) +function _table.filter(t, f) local r = {} for i,v in ipairs(t) do if f(v, i) then @@ -67,7 +80,7 @@ function table.filter(t, f) end --partitions a sequence based on filter criteria -function table.partition(t, f) +function _table.partition(t, f) local a = {} local b = {} for i,v in ipairs(t) do @@ -84,7 +97,7 @@ end --iteration limited by min(#t1, #t2) --function receives arguments (t1, t2, i) --nil results ignored -function table.zip(t1, t2, f) +function _table.zip(t1, t2, f) local ret = {} local limit = math.min(#t2, #t2) for i=1, limit do @@ -101,9 +114,9 @@ end --return a copy of a sequence with all duplicates removed -- causes a little "extra" gc churn; one table and one closure -- as well as the copied deduped table -function table.dedupe(t) +function _table.dedupe(t) local seen = {} - return table.filter(t, function(v) + return _table.filter(t, function(v) if seen[v] then return false end @@ -113,15 +126,15 @@ function table.dedupe(t) end --append sequence t2 into t1, modifying t1 -function table.append_inplace(t1, t2) - table.foreach(t2, function(v) +function _table.append_inplace(t1, t2) + for i,v in ipairs(t2) do table.insert(t1, v) - end) + end return t1 end --return a new sequence with the elements of both t1 and t2 -function table.append(t1, t2) +function _table.append(t1, t2) local r = {} append_inplace(r, t1) append_inplace(r, t2) @@ -133,7 +146,7 @@ end ----------------------------------------------------------- --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 if f(v) then return true @@ -143,7 +156,7 @@ function table.any(t, f) end --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 if f(v) then return false @@ -153,7 +166,7 @@ function table.none(t, f) end --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 if not f(v) then return false @@ -163,7 +176,7 @@ function table.all(t, f) end --counts the elements of t that match f -function table.count(t, f) +function _table.count(t, f) local c = 0 for i,v in ipairs(t) do if f(v) then @@ -174,7 +187,7 @@ function table.count(t, f) end --true if the table contains element e -function table.contains(t, e) +function _table.contains(t, e) for i, v in ipairs(t) do if v == e then return true @@ -184,48 +197,53 @@ function table.contains(t, e) end --return the numeric sum of all elements of t -function table.sum(t) - return table.reduce(t, function(a, b) +function _table.sum(t) + return _table.reduce(t, function(a, b) return a + b end, 0) end --return the numeric mean of all elements of t -function table.mean(t) +function _table.mean(t) local len = #t if len == 0 then return 0 end - return table.sum(t) / len + return _table.sum(t) / len end --return the minimum and maximum of t in one pass -function table.minmax(t) - local a = table.reduce(t, function(a, b) - a.min = a.min and math.min(a.min, b) or b - a.max = a.max and math.max(a.max, b) or b - return a - end, {}) - if a.min == nil then - a.min = 0 - a.max = 0 +--or zero for both if t is empty +-- (would perhaps more correctly be math.huge, -math.huge +-- but that tends to be surprising/annoying in practice) +function _table.minmax(t) + local max, min + for i,v in ipairs(t) do + min = not min and v or math.min(min, v) + max = not max and v or math.max(max, v) end - return a.min, a.max + if min == nil then + min = 0 + max = 0 + end + return min, max end -function table.max(t) - local min, max = table.minmax(t) +--return the maximum element of t or zero if t is empty +function _table.max(t) + local min, max = _table.minmax(t) return max end -function table.min(t) - local min, max = table.minmax(t) +--return the minimum element of t or zero if t is empty +function _table.min(t) + local min, max = _table.minmax(t) return min end --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 table.find_best(t, f) +function _table.find_best(t, f) local current = nil local current_best = -math.huge for k,e in pairs(t) do @@ -239,14 +257,15 @@ function table.find_best(t, f) end --return the element of the table that results in the value nearest to the passed value -function table.find_nearest(t, f, v) - return table.find_best(t, function(e) +--todo: optimise as this generates a closure each time +function _table.find_nearest(t, f, v) + return _table.find_best(t, function(e) return -math.abs(f(e) - v) end) end --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 if f(v) then return v @@ -254,3 +273,5 @@ function table.find_match(t, f) end return nil end + +return _table diff --git a/init.lua b/init.lua index ce20613..3551045 100644 --- a/init.lua +++ b/init.lua @@ -1,35 +1,92 @@ --[[ - core modules + batteries for lua 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 + + 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 function relative_file(p) - return table.concat({path, p}, ".") +local function require_relative(p) + return require(table.concat({path, p}, ".")) 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")) -require(relative_file("stable_sort")) +local _math = require_relative("math") -require(relative_file("functional")) -sequence = require(relative_file("sequence")) -unique_mapping = require(relative_file("unique_mapping")) +local _table = require_relative("table") +local _stable_sort = require_relative("stable_sort") -vec2 = require(relative_file("vec2")) -vec3 = require(relative_file("vec3")) -intersect = require(relative_file("intersect")) +local _functional = require_relative("functional") +local _sequence = require_relative("sequence") -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, +} diff --git a/math.lua b/math.lua index 3ad0a1a..7c9def4 100644 --- a/math.lua +++ b/math.lua @@ -1,11 +1,15 @@ --[[ 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) -function math.wrap(v, lo, hi) +function _math.wrap(v, lo, hi) local range = hi - lo local relative = v - lo local relative_wrapped = relative % range @@ -15,51 +19,51 @@ function math.wrap(v, lo, hi) end --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)) end -function math.clamp01(v) - return math.clamp(v, 0, 1) +function _math.clamp01(v) + return _math.clamp(v, 0, 1) end --round v to nearest whole -function math.round(v) +function _math.round(v) return math.floor(v + 0.5) end --round v to one-in x -- (eg x = 2, v rounded to increments of 0.5) -function math.to_one_in(v, x) - return math.round(v * x) / x +function _math.to_one_in(v, x) + return _math.round(v * x) / x end --round v to a given decimal precision -function math.to_precision(v, decimal_points) - return math.to_one_in(v, math.pow(10, decimal_points)) +function _math.to_precision(v, decimal_points) + return _math.to_one_in(v, math.pow(10, decimal_points)) end --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 return 0 end --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 end --classic smoothstep --(only "safe" for 0-1 range) -function math.smoothstep(v) +function _math.smoothstep(v) return v * v * (3 - 2 * v) end --classic smootherstep; zero 2nd order derivatives at 0 and 1 --(only safe for 0-1 range) -function math.smootherstep(v) +function _math.smootherstep(v) return v * v * v * (v * (v * 6 - 15) + 10) end @@ -125,7 +129,7 @@ local sparse_primes_1k = { 6689, 7039, 7307, 7559, 7573, 7919, } -function math.first_above(v, t) +function _math.first_above(v, t) for _,p in ipairs(t) do if p > v then return p @@ -134,79 +138,30 @@ function math.first_above(v, t) return t[#t] end -function math.next_prime_1k(v) - return math.first_above(v, primes_1k) +function _math.next_prime_1k(v) + return _math.first_above(v, primes_1k) end -function math.next_prime_1k_sparse(v) - 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 +function _math.next_prime_1k_sparse(v) + return _math.first_above(v, sparse_primes_1k) end --angle handling stuff -function math.normalise_angle(a) - return math.wrap(a, -math.pi, math.pi) +function _math.normalise_angle(a) + return _math.wrap(a, -math.pi, math.pi) end -function math.relative_angle(a1, a2) - a1 = math.normalise_angle(a1) - a2 = math.normalise_angle(a2) - return math.normalise_angle(a1 - a2) +function _math.relative_angle(a1, a2) + a1 = _math.normalise_angle(a1) + a2 = _math.normalise_angle(a2) + return _math.normalise_angle(a1 - a2) end --geometric rotation multi-return -function math.rotate(x, y, r) +function _math.rotate(x, y, r) local s = math.sin(r) local c = math.cos(r) return c * x - s * y, s * x + c * y end + +return _math diff --git a/readme.md b/readme.md index 6b4b51b..112d677 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,38 @@ -# lua-core +# batteries 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 :) diff --git a/sequence.lua b/sequence.lua index e24afb1..80fc8d6 100644 --- a/sequence.lua +++ b/sequence.lua @@ -7,6 +7,7 @@ in that case, you can still use the table methods that accept a table first as method calls. ]] + local sequence = {} sequence.mt = {__index = sequence} @@ -20,52 +21,59 @@ function sequence:new(t) return setmetatable(t or {}, sequence.mt) end ---import table functions to sequence as-is -sequence.join = table.concat --alias +--alias +sequence.join = table.concat --sorting default to stable if present 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() - return sequence:new(table.keys(self)) + return sequence:new(_func.keys(self)) end function sequence:values() - return sequence:new(table.values(self)) + return sequence:new(_func.values(self)) end function sequence:foreach(f) - return table.foreach(self, f) + return _func.foreach(self, f) end function sequence:reduce(f, o) - return table.foreach(self, f, o) + return _func.foreach(self, f, o) end 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 function sequence:filter(f) - return sequence:new(table.filter(self, f)) + return sequence:new(_func.filter(self, f)) end 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) end function sequence:zip(other, f) - return sequence:new(table.zip(self, other, f)) + return sequence:new(_func.zip(self, other, f)) end function sequence:dedupe() - return table.dedupe(self) + return _func.dedupe(self) end function sequence:append_inplace(other) - return table.append_inplace(self, other) + return _func.append_inplace(self, other) end function sequence:append(other) @@ -73,7 +81,7 @@ function sequence:append(other) end function sequence:copy(deep) - return sequence:new(table.copy(self, deep)) + return sequence:new(_func.copy(self, deep)) end return sequence \ No newline at end of file diff --git a/stable_sort.lua b/stable_sort.lua index 09e9a8d..b0ac601 100644 --- a/stable_sort.lua +++ b/stable_sort.lua @@ -1,6 +1,6 @@ -- 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. -- -- table.stable_sort @@ -10,6 +10,11 @@ -- table.insertion_sort -- 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 --license as follows: @@ -36,14 +41,15 @@ DEALINGS IN THE SOFTWARE. ]] --- (modifications by Max Cahill 2018) +-- (modifications by Max Cahill 2018, 2020) local _sort_core = {} ---tunable size for -_sort_core.max_chunk_size = 24 +--tunable size for insertion sort "bottom out" +_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 local k = first local v = array[i] @@ -59,7 +65,8 @@ function _sort_core.insertion_sort_impl(array, first, last, less) 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 i = 1 -- copy first half of array to auxiliary array @@ -91,24 +98,25 @@ function _sort_core.merge(array, workspace, low, middle, high, less) end end - -function _sort_core.merge_sort_impl(array, workspace, low, high, less) +--implementation for the merge sort +function _sort_core._merge_sort_impl(array, workspace, low, high, less) 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 local middle = math.floor((low + high) / 2) - _sort_core.merge_sort_impl(array, workspace, low, middle, less) - _sort_core.merge_sort_impl(array, workspace, middle + 1, high, less) - _sort_core.merge(array, workspace, low, middle, high, 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(array, workspace, low, middle, high, less) end end -function default_less(a, b) +--default comparison; hoisted for clarity +local function default_less(a, b) return a < b end --inline common setup stuff -function _sort_core.sort_setup(array, less) +function _sort_core._sort_setup(array, less) --default less less = less or default_less -- @@ -127,28 +135,35 @@ end function _sort_core.stable_sort(array, less) --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 --temp storage; allocate ahead of time local workspace = {} local middle = math.ceil(n / 2) workspace[middle] = array[1] --dive in - _sort_core.merge_sort_impl( array, workspace, 1, n, less ) + _sort_core._merge_sort_impl( array, workspace, 1, n, less ) end return array end function _sort_core.insertion_sort(array, less) --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 - _sort_core.insertion_sort_impl(array, 1, n, less) + _sort_core._insertion_sort_impl(array, 1, n, less) end return array end +_sort_core.unstable_sort = table.sort + --export sort core -table.insertion_sort = _sort_core.insertion_sort -table.stable_sort = _sort_core.stable_sort -table.unstable_sort = table.sort + +local _table = BATTERIES_SORT_MODULE or BATTERIES_TABLE_MODULE or table + +_table.insertion_sort = _sort_core.insertion_sort +_table.stable_sort = _sort_core.stable_sort +_table.unstable_sort = _sort_core.unstable_sort + +return _sort_core diff --git a/table.lua b/table.lua index 4f279b8..0ebedae 100644 --- a/table.lua +++ b/table.lua @@ -1,36 +1,52 @@ --[[ 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 -function table.back(t) +function _table.back(t) return t[#t] end --remove the back element of a table and return it -function table.pop(t) +function _table.pop(t) return table.remove(t) end --insert to the back of a table -function table.push(t, v) +function _table.push(t, v) return table.insert(t, v) end --remove the front element of a table and return it -function table.shift(t) +function _table.shift(t) return table.remove(t, 1) end --insert to the front of a table -function table.unshift(t, v) +function _table.unshift(t, v) return table.insert(t, 1, v) end --find the index in a sequential table that a resides at --or nil if nothing was found --(todo: consider pairs version?) -function table.index_of(t, a) +function _table.index_of(t, a) if a == nil then return nil end for i,b in ipairs(t) do if a == b then @@ -42,8 +58,8 @@ end --remove the first instance of value from a table (linear search) --returns true if the value was removed, else false -function table.remove_value(t, a) - local i = table.index_of(t, a) +function _table.remove_value(t, a) + local i = _table.index_of(t, a) if i then table.remove(t, i) return true @@ -53,8 +69,8 @@ end --add a value to a table if it doesn't already exist (linear search) --returns true if the value was added, else false -function table.add_value(t, a) - local i = table.index_of(t, a) +function _table.add_value(t, a) + local i = _table.index_of(t, a) if not i then table.insert(t, a) return true @@ -70,7 +86,7 @@ local function _random(min, max, r) end --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 return nil end @@ -78,7 +94,7 @@ function table.pick_random(t, r) end --shuffle the order of a table -function table.shuffle(t, r) +function _table.shuffle(t, r) for i = 1, #t do local j = _random(1, #t, r) t[i], t[j] = t[j], t[i] @@ -87,7 +103,7 @@ function table.shuffle(t, r) end --reverse the order of a table -function table.reverse(t) +function _table.reverse(t) for i = 1, #t / 2 do local j = #t - i + 1 t[i], t[j] = t[j], t[i] @@ -95,28 +111,57 @@ function table.reverse(t) return t 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) -if table.clear == nil then - --destructively clear a numerically keyed table - --useful when multiple references are floating around - --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") - while t[1] ~= nil do - table.remove(t) +if _table.clear == nil then + if _table ~= table and table.clear then + --import from global if it exists + _table.clear = table.clear + else + --remove all values from a table + --useful when multiple references are floating around + --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 ---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 +--note: +-- copies and overlays are currently not satisfactory +-- +-- i feel that copy especially tries to do too much and +-- probably they should be split into separate functions +-- to be both more explicit and performant, ie +-- +-- shallow_copy, deep_copy, shallow_overlay, deep_overlay +-- +-- input is welcome on this :) --copy a table -- deep_or_into is either: @@ -128,7 +173,7 @@ end -- if into specified, copies into that table -- but doesn't clear anything out -- (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") local is_bool = type(deep_or_into) == "boolean" 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 v = v:copy() else - v = table.copy(v, deep) + v = _table.copy(v, deep) end end into[k] = v @@ -148,3 +193,45 @@ function table.copy(t, deep_or_into) return into 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 diff --git a/vec2.lua b/vec2.lua index 48d99a9..b7df938 100644 --- a/vec2.lua +++ b/vec2.lua @@ -5,7 +5,7 @@ --[[ 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 diff --git a/vec3.lua b/vec3.lua index f66de1e..5371dbe 100644 --- a/vec3.lua +++ b/vec3.lua @@ -5,7 +5,7 @@ --[[ 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