Skip to content

Commit

Permalink
feat(utils) add npairs nil-safe numeric iterator
Browse files Browse the repository at this point in the history
It honours the `table.n` size field.
  • Loading branch information
Tieske committed Aug 30, 2021
1 parent e3712f0 commit aec5e57
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ include_files = {
".luacheckrc",
}

files["spec/**/*.lua"] = {
std = "+busted",
}

exclude_files = {
"tests/*.lua",
"tests/**/*.lua",
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions lua/pl/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
106 changes: 106 additions & 0 deletions spec/utils-npairs_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 }
local t2 = {}
assert.has.error(function()
for i, v in npairs(t1, nil, nil, 0) do t2[i] = v 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)

0 comments on commit aec5e57

Please sign in to comment.