diff --git a/.luacheckrc b/.luacheckrc index 53fc740e..82dab885 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -18,6 +18,10 @@ include_files = { ".luacheckrc", } +files["spec/**/*.lua"] = { + std = "+busted", +} + exclude_files = { "tests/*.lua", "tests/**/*.lua", diff --git a/CHANGELOG.md b/CHANGELOG.md index bf91bd4f..bbbbfbee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ deprecation policy. see [CONTRIBUTING.md](CONTRIBUTING.md#release-instructions-for-a-new-version) for release instructions +## 1.12.0 (unreleased) + - feat: `utils.npairs` added. An iterator with a range that honours the `n` field + [#387](https://github.com/lunarmodules/Penlight/pull/387) + ## 1.11.0 (2021-08-18) - fix: `stringx.strip` behaved badly with string lengths > 200 diff --git a/lua/pl/utils.lua b/lua/pl/utils.lua index 7a1bf972..66edda6d 100644 --- a/lua/pl/utils.lua +++ b/lua/pl/utils.lua @@ -50,6 +50,8 @@ utils.stdmt = { -- @return a table with field `n` set to the length -- @function utils.pack -- @see compat.pack +-- @see utils.npairs +-- @see utils.unpack utils.pack = table.pack -- added here to be symmetrical with unpack --- unpack a table and return its contents. @@ -62,6 +64,8 @@ utils.pack = table.pack -- added here to be symmetrical with unpack -- @return multiple return values from the table -- @function utils.unpack -- @see compat.unpack +-- @see utils.pack +-- @see utils.npairs -- @usage -- local t = table.pack(nil, nil, nil, 4) -- local a, b, c, d = table.unpack(t) -- this `unpack` is NOT nil-safe, so d == nil @@ -166,6 +170,56 @@ function utils.is_type (obj,tp) return tp == mt end + + +--- an iterator with indices, similar to `ipairs`, but with a range. +-- This is a nil-safe index based iterator that will return `nil` when there +-- is a hole in a list. To be safe ensure that table `t.n` contains the length. +-- @tparam table t the table to iterate over +-- @tparam[opt=1] integer i_start start index +-- @tparam[opt=t.n or #t] integer i_end end index +-- @tparam[opt=1] integer step step size +-- @treturn integer index +-- @treturn any value at index (which can be `nil`!) +-- @see utils.pack +-- @see utils.unpack +-- @usage +-- local t = utils.pack(nil, 123, nil) -- adds an `n` field when packing +-- +-- for i, v in utils.npairs(t, 2) do -- start at index 2 +-- t[i] = tostring(t[i]) +-- end +-- +-- -- t = { n = 3, [2] = "123", [3] = "nil" } +function utils.npairs(t, i_start, i_end, step) + step = step or 1 + if step == 0 then + error("iterator step-size cannot be 0", 2) + end + local i = (i_start or 1) - step + i_end = i_end or t.n or #t + if step < 0 then + return function() + i = i + step + if i < i_end then + return nil + end + return i, t[i] + end + + else + return function() + i = i + step + if i > i_end then + return nil + end + return i, t[i] + end + end +end + + + --- Error handling -- @section Error-handling diff --git a/spec/utils-npairs_spec.lua b/spec/utils-npairs_spec.lua new file mode 100644 index 00000000..61bf4edb --- /dev/null +++ b/spec/utils-npairs_spec.lua @@ -0,0 +1,105 @@ +local utils = require("pl.utils") + +describe("pl.utils", function () + + describe("npairs", function () + local npairs = utils.npairs + + it("start index defaults to 1", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, nil, 2) do t2[i] = v end + assert.are.same({ 1, 2 }, t2) + end) + + + it("end index defaults to `t.n`", function() + local t1 = { n = 2, 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1) do t2[i] = v end + assert.are.same({1, 2}, t2) + end) + + + it("step size defaults to 1", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1) do t2[i] = v end + assert.are.same({1, 2, 3}, t2) + end) + + + it("step size cannot be 0", function() + local t1 = { 1, 2, 3 } + assert.has.error(function() + for i, v in npairs(t1, nil, nil, 0) do end + end, "iterator step-size cannot be 0") + end) + + + it("end index defaults to `#t` if there is no `t.n`", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1) do t2[i] = v end + assert.are.same({1, 2, 3}, t2) + end) + + + it("returns nothing if start index is beyond end index", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, 5, 3) do t2[i] = v end + assert.are.same({}, t2) + end) + + + it("returns nothing if start index is beyond end index, with negative step size", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, 3, 1, -1) do t2[#t2+1] = v end + assert.are.same({ 3, 2, 1}, t2) + end) + + + it("returns 1 key/value if end == start index", function() + local t1 = { 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, 2, 2) do t2[i] = v end + assert.are.same({ [2] = 2 }, t2) + end) + + + it("returns negative to positive ranges", function() + local t1 = { [-5] = -5, [-4] = -4, [-3] = -3, [-2] = -2, [-1] = -1, [0] = 0, 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, -4, 1) do t2[i] = v end + assert.are.same({ [-4] = -4, [-3] = -3, [-2] = -2, [-1] = -1, [0] = 0, 1 }, t2) + end) + + + it("returns nil values with the range", function() + local t1 = { n = 3 } + local t2 = {} + for i, v in npairs(t1) do t2[i] = tostring(v) end + assert.are.same({ "nil", "nil", "nil" }, t2) + end) + + + it("honours positive step size", function() + local t1 = { [-5] = -5, [-4] = -4, [-3] = -3, [-2] = -2, [-1] = -1, [0] = 0, 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, -4, 1, 2) do t2[#t2+1] = v end + assert.are.same({ -4, -2, 0}, t2) + end) + + + it("honours negative step size", function() + local t1 = { [-5] = -5, [-4] = -4, [-3] = -3, [-2] = -2, [-1] = -1, [0] = 0, 1, 2, 3 } + local t2 = {} + for i, v in npairs(t1, 0, -5, -2) do t2[#t2+1] = v end + assert.are.same({ 0, -2, -4 }, t2) + end) + + end) + +end)