From 50e959d622e51a90c634f167204d2c75a58b6061 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Tue, 27 Apr 2021 00:17:11 +0200 Subject: [PATCH] chore(array2d) overhaul of module and tests (#377) closes #376 --- CHANGELOG.md | 20 ++ lua/pl/array2d.lua | 280 ++++++++++++++------- spec/array2d_spec.lua | 557 +++++++++++++++++++++++++++++++++++++++++ tests/test-array2d.lua | 77 ------ 4 files changed, 760 insertions(+), 174 deletions(-) create mode 100644 spec/array2d_spec.lua delete mode 100644 tests/test-array2d.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cafd5b..a1d4b27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,26 @@ see [CONTRIBUTING.md](CONTRIBUTING.md#release-instructions-for-a-new-version) fo [#373](https://github.com/lunarmodules/Penlight/pull/373) - fix: `dir.makepath` failed to create top-level directories [#372](https://github.com/lunarmodules/Penlight/pull/372) + - overhaul: `array2d` module was updated, got additional tests and several + documentation updates + [#377](https://github.com/lunarmodules/Penlight/pull/377) + - feat: `aray2d` now accepts negative indices + - feat: `array2d.row` added to align with `column` + - fix: bad error message in `array2d.map` + - fix: `array2d.flatten` now ensures to deliver a 'square' result if `nil` is + encountered + - feat: `array2d.transpose` added + - feat: `array2d.swap_rows` and `array2d.swap_cols` now return the array + - fix: `aray2d.range` correctly recognizes `R` column in spreadsheet format, was + mistaken for `R1C1` format. + - fix: `aray2d.range` correctly recognizes 2 char column in spreadsheet format + - feat: `array2d.default_range` added (previously private) + - feat: `array2d.set` if used with a function now passes `i,j` to the function + in line with the `new` implementation. + - fix: `array2d.iter` didn't properly iterate the indices + [#376](https://github.com/lunarmodules/Penlight/issues/376) + - feat: `array2d.columns` now returns a second value; the column index + - feat: `array2d.rows` added to be in line with `columns` ## 1.9.2 (2020-09-27) diff --git a/lua/pl/array2d.lua b/lua/pl/array2d.lua index 40b4ecab..db55b39e 100644 --- a/lua/pl/array2d.lua +++ b/lua/pl/array2d.lua @@ -1,11 +1,17 @@ --- Operations on two-dimensional arrays. -- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide} -- +-- The size of the arrays is determined by using the length operator `#` hence +-- the module is not `nil` safe, and the usual precautions apply. +-- +-- Note: all functions taking `i1,j1,i2,j2` as arguments will normalize the +-- arguments using `default_range`. +-- -- Dependencies: `pl.utils`, `pl.tablex`, `pl.types` -- @module pl.array2d -local tonumber,assert,tostring,io,ipairs,string,table = - _G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table +local tonumber,tostring,io,ipairs,string,table = + _G.tonumber,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table local setmetatable,getmetatable = setmetatable,getmetatable local tablex = require 'pl.tablex' @@ -33,29 +39,48 @@ local function makelist (res) return setmetatable(res, require('pl.List')) end - -local function index (t,k) - return t[k] +--- return the row and column size. +-- Size is calculated using the Lua length operator #, so usual precautions +-- regarding `nil` values apply. +-- @array2d a a 2d array +-- @treturn int number of rows (`#a`) +-- @treturn int number of cols (`#a[1]`) +function array2d.size (a) + assert_arg(1,a,'table') + return #a,#a[1] end ---- return the row and column size. --- @array2d t a 2d array --- @treturn int number of rows --- @treturn int number of cols -function array2d.size (t) - assert_arg(1,t,'table') - return #t,#t[1] +do + local function index (t,k) + return t[k] + end + + --- extract a column from the 2D array. + -- @array2d a 2d array + -- @param j column index + -- @return 1d array + function array2d.column (a,j) + assert_arg(1,a,'table') + return makelist(imap(index,a,j)) + end end +local column = array2d.column ---- extract a column from the 2D array. +--- extract a row from the 2D array. +-- Added in line with `column`, for read-only purposes directly +-- accessing a[i] is more performant. -- @array2d a 2d array --- @param key an index or key --- @return 1d array -function array2d.column (a,key) +-- @param i row index +-- @return 1d array (copy of the row) +function array2d.row(a,i) assert_arg(1,a,'table') - return makelist(imap(index,a,key)) + local row = a[i] + local r = {} + for n,v in ipairs(row) do + r[n] = v + end + return makelist(r) end -local column = array2d.column --- map a function over a 2D array -- @func f a function of at least one argument @@ -63,7 +88,7 @@ local column = array2d.column -- @param arg an optional extra argument to be passed to the function. -- @return 2d array function array2d.map (f,a,arg) - assert_arg(1,a,'table') + assert_arg(2,a,'table') f = utils.function_arg(1,f) return obj(a,imap(function(row) return imap(f,row,arg) end, a)) end @@ -101,8 +126,8 @@ end --- map a function over two arrays. -- They can be both or either 2D arrays -- @func f function of at least two arguments --- @int ad order of first array (1 or 2) --- @int bd order of second array (1 or 2) +-- @int ad order of first array (`1` if `a` is a list/array, `2` if it is a 2d array) +-- @int bd order of second array (`1` if `b` is a list/array, `2` if it is a 2d array) -- @tab a 1d or 2d array -- @tab b 1d or 2d array -- @param arg optional extra argument to pass to function @@ -138,9 +163,9 @@ function array2d.product (f,t1,t2) f = utils.function_arg(1,f) assert_arg(2,t1,'table') assert_arg(3,t2,'table') - local res, map = {}, tablex.map + local res = {} for i,v in ipairs(t2) do - res[i] = map(f,t1,v) + res[i] = tmap(f,t1,v) end return res end @@ -153,19 +178,21 @@ end function array2d.flatten (t) local res = {} local k = 1 - for _,a in ipairs(t) do -- for all rows - for i = 1,#a do - res[k] = a[i] + local rows, cols = array2d.size(t) + for r = 1, rows do + local row = t[r] + for c = 1, cols do + res[k] = row[c] k = k + 1 end end return makelist(res) end ---- reshape a 2D array. +--- reshape a 2D array. Reshape the aray by specifying a new nr of rows. -- @array2d t 2d array -- @int nrows new number of rows --- @bool co column-order (Fortran-style) (default false) +-- @bool co use column-order (Fortran-style) (default false) -- @return a new 2d array function array2d.reshape (t,nrows,co) local nr,nc = array2d.size(t) @@ -195,30 +222,43 @@ function array2d.reshape (t,nrows,co) return obj(t,res) end +--- transpose a 2D array. +-- @array2d t 2d array +-- @return a new 2d array +function array2d.transpose(t) + assert_arg(1,t,'table') + local _, c = array2d.size(t) + return array2d.reshape(t,c,true) +end + --- swap two rows of an array. -- @array2d t a 2d array -- @int i1 a row index -- @int i2 a row index +-- @return t (same, modified 2d array) function array2d.swap_rows (t,i1,i2) assert_arg(1,t,'table') t[i1],t[i2] = t[i2],t[i1] + return t end --- swap two columns of an array. -- @array2d t a 2d array -- @int j1 a column index -- @int j2 a column index +-- @return t (same, modified 2d array) function array2d.swap_cols (t,j1,j2) assert_arg(1,t,'table') - for i = 1,#t do - local row = t[i] + for _, row in ipairs(t) do row[j1],row[j2] = row[j2],row[j1] end + return t end --- extract the specified rows. -- @array2d t 2d array -- @tparam {int} ridx a table of row indices +-- @return a new 2d array with the extracted rows function array2d.extract_rows (t,ridx) return obj(t,index_by(t,ridx)) end @@ -226,6 +266,7 @@ end --- extract the specified columns. -- @array2d t 2d array -- @tparam {int} cidx a table of column indices +-- @return a new 2d array with the extracted colums function array2d.extract_cols (t,cidx) assert_arg(1,t,'table') local res = {} @@ -251,37 +292,44 @@ function array2d.remove_col (t,j) end end -local function _parse (s) - local c,r - if s:sub(1,1) == 'R' then - r,c = s:match 'R(%d+)C(%d+)' - r,c = tonumber(r),tonumber(c) - else - c,r = s:match '(.)(.)' - c = byte(c) - byte 'A' + 1 - r = tonumber(r) +do + local function _parse (s) + local r, c = s:match 'R(%d+)C(%d+)' + if r then + r,c = tonumber(r),tonumber(c) + return r,c + end + c,r = s:match '(%a+)(%d+)' + if c then + local cv = 0 + for i = 1, #c do + cv = cv * 26 + byte(c:sub(i,i)) - byte 'A' + 1 + end + return tonumber(r), cv + end + error('bad cell specifier: '..s) end - assert(c ~= nil and r ~= nil,'bad cell specifier: '..s) - return r,c -end ---- parse a spreadsheet range. --- The range can be specified either as 'A1:B2' or 'R1C1:R2C2'; --- a special case is a single element (e.g 'A1' or 'R1C1') --- @string s a range. --- @treturn int start col --- @treturn int start row --- @treturn int end col --- @treturn int end row -function array2d.parse_range (s) - if s:find ':' then - local start,finish = splitv(s,':') - local i1,j1 = _parse(start) - local i2,j2 = _parse(finish) - return i1,j1,i2,j2 - else -- single value - local i,j = _parse(s) - return i,j + --- parse a spreadsheet range. + -- The range can be specified either as 'A1:B2' or 'R1C1:R2C2'; + -- a special case is a single element (e.g 'A1' or 'R1C1') + -- @string s a range (case insensitive). + -- @treturn int start row + -- @treturn int start col + -- @treturn int end row + -- @treturn int end col + function array2d.parse_range (s) + assert_arg(1,s,'string') + s = s:upper() + if s:find ':' then + local start,finish = splitv(s,':') + local i1,j1 = _parse(start) + local i2,j2 = _parse(finish) + return i1,j1,i2,j2 + else -- single value + local i,j = _parse(s) + return i,j + end end end @@ -293,21 +341,38 @@ end -- @see array2d.slice function array2d.range (t,rstr) assert_arg(1,t,'table') - local i1,j1,i2,j2 = array2d.parse_range(rstr) - if i2 then - return array2d.slice(t,i1,j1,i2,j2) - else -- single value - return t[i1][j1] - end + return array2d.slice(t,array2d.parse_range(rstr)) end -local function default_range (t,i1,j1,i2,j2) - local nr, nc = array2d.size(t) - i1,j1 = i1 or 1, j1 or 1 - i2,j2 = i2 or nr, j2 or nc - if i2 < 0 then i2 = nr + i2 + 1 end - if j2 < 0 then j2 = nc + j2 + 1 end - return i1,j1,i2,j2 +local default_range do + local function norm_value(v, max) + if not v then return v end + if v < 0 then + v = max + v + 1 + end + if v < 1 then v = 1 end + if v > max then v = max end + return v + end + + --- normalizes coordinates to valid positive entries and defaults. + -- Negative indices will be counted from the end, too low, or too high + -- will be limited by the array sizes. + -- @array2d t a 2D array + -- @int i1 start row (default 1) + -- @int j1 start col (default 1) + -- @int i2 end row (default N) + -- @int j2 end col (default M) + -- return i1, j1, i2, j2 + function array2d.default_range (t,i1,j1,i2,j2) + local nr, nc = array2d.size(t) + i1 = norm_value(i1 or 1, nr) + j1 = norm_value(j1 or 1, nc) + i2 = norm_value(i2 or nr, nr) + j2 = norm_value(j2 or nc, nc) + return i1,j1,i2,j2 + end + default_range = array2d.default_range end --- get a slice of a 2D array. Note that if the specified range has @@ -341,7 +406,7 @@ end --- set a specified range of an array to a value. -- @array2d t a 2D array --- @param value the value (may be a function) +-- @param value the value (may be a function, called as `val(i,j)`) -- @int i1 start row (default 1) -- @int j1 start col (default 1) -- @int i2 end row (default N) @@ -349,8 +414,16 @@ end -- @see tablex.set function array2d.set (t,value,i1,j1,i2,j2) i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) - for i = i1,i2 do + local i = i1 + if types.is_callable(value) then + local old_f = value + value = function(j) + return old_f(i,j) + end + end + while i <= i2 do tset(t[i],value,j1,j2) + i = i + 1 end end @@ -379,8 +452,8 @@ end --- perform an operation for all values in a 2D array. -- @array2d t 2D array --- @func row_op function to call on each value --- @func end_row_op function to call at end of each row +-- @func row_op function to call on each value; `row_op(row,j)` +-- @func end_row_op function to call at end of each row; `end_row_op(i)` -- @int i1 start row (default 1) -- @int j1 start col (default 1) -- @int i2 end row (default N) @@ -429,22 +502,21 @@ end -- @int j1 start col (default 1) -- @int i2 end row (default N) -- @int j2 end col (default M) --- @return either value or i,j,value depending on indices -function array2d.iter (a,indices,i1,j1,i2,j2) +-- @return either `value` or `i,j,value` depending on the value of `indices` +function array2d.iter(a,indices,i1,j1,i2,j2) assert_arg(1,a,'table') - local norowset = not (i2 and j2) i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2) - local i,j = i1-1,j1-1 - local row,nr = nil,0 - local onr = j2 - j1 + 1 + local i,j = i1,j1-1 + local row = a[i] return function() j = j + 1 - if j > nr then + if j > j2 then j = j1 i = i + 1 - if i > i2 then return nil end row = a[i] - nr = norowset and #row or onr + if i > i2 then + return nil + end end if indices then return i,j,row[j] @@ -456,16 +528,32 @@ end --- iterate over all columns. -- @array2d a a 2D array --- @return each column in turn -function array2d.columns (a) - assert_arg(1,a,'table') - local n = a[1][1] - local i = 0 - return function() - i = i + 1 - if i > n then return nil end - return column(a,i) - end +-- @return column, column-index +function array2d.columns(a) + assert_arg(1,a,'table') + local n = #a[1] + local i = 0 + return function() + i = i + 1 + if i > n then return nil end + return column(a,i), i + end +end + +--- iterate over all rows. +-- Returns a copy of the row, for read-only purrposes directly iterating +-- is more performant; `ipairs(a)` +-- @array2d a a 2D array +-- @return row, row-index +function array2d.rows(a) + assert_arg(1,a,'table') + local n = #a + local i = 0 + return function() + i = i + 1 + if i > n then return nil end + return array2d.row(a,i), i + end end --- new array of specified dimensions @@ -489,5 +577,3 @@ function array2d.new(rows,cols,val) end return array2d - - diff --git a/spec/array2d_spec.lua b/spec/array2d_spec.lua new file mode 100644 index 00000000..3fd0085a --- /dev/null +++ b/spec/array2d_spec.lua @@ -0,0 +1,557 @@ +local array2d = require("pl.array2d") + +describe("pl.array2d", function() + + describe("new()", function() + it("creates an empty 2d array", function() + assert.same({{},{},{}}, array2d.new(3,3,nil)) + end) + + it("creates a value-filled 2d array", function() + assert.same({{99,99,99}, + {99,99,99}, + {99,99,99}}, array2d.new(3,3,99)) + end) + + it("creates a function-filled 2d array", function() + assert.same({{2,3,4}, + {3,4,5}, + {4,5,6}}, array2d.new(3,3,function(i,j) return i+j end)) + end) + end) + + describe("size()", function() + it("returns array size", function() + local a = array2d.new(3,5,99) + assert.same({3,5}, {array2d.size(a)}) + end) + + it("returns 0 columns for nil arrays", function() + local a = array2d.new(3,5,nil) + assert.same({3,0}, {array2d.size(a)}) + end) + end) + + describe("column()", function() + it("returns a column copy", function() + local a = {{1,2}, + {3,4}, + {5,6}} + assert.same({1,3,5}, array2d.column(a,1)) + assert.same({2,4,6}, array2d.column(a,2)) + end) + end) + + describe("row()", function() + it("returns a row copy", function() + local a = {{1,2}, + {3,4}, + {5,6}} + assert.same({1,2}, array2d.row(a,1)) + -- next test: need to remove the metatable to prevent comparison by + -- metamethods in Lua 5.3 and 5.4 + assert.not_equal(a[1], setmetatable(array2d.row(a,1),nil)) + assert.same({3,4}, array2d.row(a,2)) + assert.same({5,6}, array2d.row(a,3)) + end) + end) + + describe("map()", function() + it("maps a function on an array", function() + local a1 = array2d.new(2,3,function(i,j) return i+j end) + local a2 = array2d.map(function(a,b) return a .. b end, a1, "x") + assert.same({{"2x","3x","4x"}, + {"3x","4x","5x"}}, a2) + end) + end) + + describe("reduce_rows()", function() + it("reduces rows", function() + local a = {{ 1, 2, 3, 4}, + { 10, 20, 30, 40}, + { 100, 200, 300, 400}, + {1000,2000,3000,4000}} + assert.same({10,100,1000,10000},array2d.reduce_rows('+',a)) + end) + end) + + describe("reduce_cols()", function() + it("reduces columns", function() + local a = {{ 1, 2, 3, 4}, + { 10, 20, 30, 40}, + { 100, 200, 300, 400}, + {1000,2000,3000,4000}} + assert.same({1111,2222,3333,4444},array2d.reduce_cols('+',a)) + end) + end) + + describe("reduce2()", function() + it("recuces array to scalar", function() + local a = {{1,10}, + {2,10}, + {3,10}} + assert.same(60, array2d.reduce2('+','*',a)) + end) + end) + + describe("map2()", function() + it("maps over 2 arrays", function() + local b = {{10,20}, + {30,40}} + local a = {{1,2}, + {3,4}} + -- 2 2d arrays + assert.same({{11,22},{33,44}}, array2d.map2('+',2,2,a,b)) + -- 1d, 2d + assert.same({{11,102},{13,104}}, array2d.map2('+',1,2,{10,100},a)) + -- 2d, 1d + assert.same({{1,-2},{3,-4}},array2d.map2('*',2,1,a,{1,-1})) + end) + end) + + describe("product()", function() + it("creates a product array", function() + local a = array2d.product('..',{1,2,3},{'a','b','c'}) + assert.same({{'1a','2a','3a'},{'1b','2b','3b'},{'1c','2c','3c'}}, a) + + local a = array2d.product('{}',{1,2},{'a','b','c'}) + assert.same({{{1,'a'},{2,'a'}},{{1,'b'},{2,'b'}},{{1,'c'},{2,'c'}}}, a) + end) + end) + + describe("flatten()", function() + it("flattens a 2darray", function() + local a = {{1,2}, + {3,4}, + {5,6}} + assert.same( {1,2,3,4,5,6}, array2d.flatten(a)) + end) + + it("keeps a nil-array 'square'", function() + local a = {{ 1,2}, + {nil,4}, + {nil,6}} + assert.same( {1,2,nil,4,nil,6}, array2d.flatten(a)) + end) + end) + + describe("reshape()", function() + it("reshapes array in new nr of rows", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}, + {10,11,12}} + local b = array2d.reshape(a, 2, false) + assert.same({{ 1, 2, 3, 4, 5, 6}, + { 7, 8, 9,10,11,12}}, b) + local c = array2d.reshape(b, 4, false) + assert.same(a, c) + end) + it("reshapes array in new nr of rows, column order", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}} + local b = array2d.reshape(a, 3, true) + assert.same({{ 1, 4, 7}, + { 2, 5, 8}, + { 3, 6, 9}}, b) + end) + end) + + describe("transpose()", function() + it("transposes a 2d array", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}} + local b = array2d.transpose(a) + assert.same({{ 1, 4, 7}, + { 2, 5, 8}, + { 3, 6, 9}}, b) + + local a = {{ 1, 2, 3, 4, 5}, + { 6, 7, 8, 9, 10}} + local b = array2d.transpose(a) + assert.same({{ 1, 6}, + { 2, 7}, + { 3, 8}, + { 4, 9}, + { 5,10}}, b) + end) + end) + + describe("swap_rows()", function() + it("swaps 2 rows, in-place", function() + local a = {{1,2}, + {3,4}, + {5,6}} + local b = array2d.swap_rows(a, 1, 3) + assert.same({{5,6}, + {3,4}, + {1,2}}, b) + assert.equal(a, b) + end) + end) + + describe("swap_cols()", function() + it("swaps 2 columns, in-place", function() + local a = {{1,2,3}, + {4,5,6}, + {7,8,9}} + local b = array2d.swap_cols(a, 1, 3) + assert.same({{3,2,1}, + {6,5,4}, + {9,8,7}}, b) + assert.equal(a, b) + end) + end) + + describe("extract_rows()", function() + it("extracts rows", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}, + {10,11,12}} + local b = array2d.extract_rows(a, {1, 3}) + assert.same({{1,2,3}, + {7,8,9}}, b) + end) + end) + + describe("extract_cols()", function() + it("extracts colums", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}, + {10,11,12}} + local b = array2d.extract_cols(a, {1, 2}) + assert.same({{ 1, 2}, + { 4, 5}, + { 7, 8}, + {10,11}}, b) + end) + end) + + describe("remove_row()", function() + it("removes a row", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}, + {10,11,12}} + array2d.remove_row(a, 2) + assert.same({{ 1, 2, 3}, + { 7, 8, 9}, + {10,11,12}}, a) + end) + end) + + describe("remove_col()", function() + it("removes a colum", function() + local a = {{ 1, 2, 3}, + { 4, 5, 6}, + { 7, 8, 9}, + {10,11,12}} + array2d.remove_col(a, 2) + assert.same({{ 1, 3}, + { 4, 6}, + { 7, 9}, + {10,12}}, a) + end) + end) + + describe("parse_range()", function() + it("parses A1:B2 format", function() + assert.same({4,11,7,12},{array2d.parse_range("K4:L7")}) + assert.same({4,28,7,54},{array2d.parse_range("AB4:BB7")}) + -- test Col R since it might be mixed up with RxCx format + assert.same({4,18,7,18},{array2d.parse_range("R4:R7")}) + end) + + it("parses A1 format", function() + assert.same({4,11},{array2d.parse_range("K4")}) + -- test Col R since it might be mixed up with RxCx format + assert.same({4,18},{array2d.parse_range("R4")}) + end) + + it("parses R1C1:R2C2 format", function() + assert.same({4,11,7,12},{array2d.parse_range("R4C11:R7C12")}) + end) + + it("parses R1C1 format", function() + assert.same({4,11},{array2d.parse_range("R4C11")}) + end) + end) + + describe("range()", function() + it("returns a range", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local b = array2d.range(a, "B3:C4") + assert.same({{ 8, 9}, + {11,12}}, b) + end) + end) + + describe("default_range()", function() + it("returns the default range", function() + local a = array2d.new(4,6,1) + assert.same({1,1,4,6}, {array2d.default_range(a, nil, nil, nil, nil)}) + end) + + it("accepts negative indices", function() + local a = array2d.new(4,6,1) + assert.same({2,2,3,5}, {array2d.default_range(a, -3, -5, -2, -2)}) + end) + + it("corrects out of bounds indices", function() + local a = array2d.new(4,6,1) + assert.same({1,1,4,6}, {array2d.default_range(a, -100, -100, 100, 100)}) + end) + end) + + describe("slice()", function() + it("returns a slice", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local b = array2d.slice(a,3,2,4,3) + assert.same({{ 8, 9}, + {11,12}}, b) + end) + + it("returns a single row if rows are equal", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local b = array2d.slice(a,4,1,4,3) + assert.same({10,11,12}, b) + end) + + it("returns a single column if columns are equal", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local b = array2d.slice(a,1,3,4,3) + assert.same({3,6,9,12}, b) + end) + + it("returns a single value if rows and columns are equal", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local b = array2d.slice(a,2,2,2,2) + assert.same(5, b) + end) + end) + + describe("set()", function() + it("sets a range to a value", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + array2d.set(a,0,2,2,3,3) + assert.same({{1 ,2 ,3}, + {4 ,0 ,0}, + {7 ,0 ,0}, + {10,11,12}}, a) + end) + + it("sets a range to a function value", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local x = 10 + local args = {} + local f = function(r,c) + args[#args+1] = {r,c} + x = x + 1 + return x + end + array2d.set(a,f,3,1,4,3) + assert.same({{1 ,2 ,3}, + {4 ,5 ,6}, + {11,12,13}, + {14,15,16}}, a) + -- validate args used to call the function + assert.same({{3,1},{3,2},{3,3},{4,1},{4,2},{4,3}}, args) + end) + end) + + describe("write()", function() + it("writes array to a file", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local f = setmetatable({}, { + __index = { + write = function(self,str) + self[#self+1] = str + end + } + }) + array2d.write(a,f,"(%s)") + f = table.concat(f) + assert.equal([[(1)(2)(3) +(4)(5)(6) +(7)(8)(9) +(10)(11)(12) +]],f) + end) + + it("writes partial array to a file", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local f = setmetatable({}, { + __index = { + write = function(self,str) + self[#self+1] = str + end + } + }) + array2d.write(a,f,"(%s)", 1,1,2,2) + f = table.concat(f) + assert.equal([[(1)(2) +(4)(5) +]],f) + end) + end) + + describe("forall()", function() + it("runs all value and row functions", function() + local r = {} + local t = 0 + local fval = function(row, j) t = t + row[j] end + local frow = function(i) r[#r+1] = t; t = 0 end + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + array2d.forall(a, fval, frow) + assert.same({6, 15, 24, 33}, r) + r = {} + array2d.forall(a, fval, frow, 2,2,4,3) + assert.same({11, 17, 23}, r) + end) + + end) + + describe("move()", function() + it("moves block to destination aray", function() + local a = array2d.new(4,4,0) + local b = array2d.new(3,3,1) + array2d.move(a,2,2,b) + assert.same({{0,0,0,0}, + {0,1,1,1}, + {0,1,1,1}, + {0,1,1,1}}, a) + end) + end) + + describe("iter()", function() + it("iterates all values", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + for v, i, j in array2d.iter(a) do + r[#r+1] = v + assert.is_nil(i) + assert.is_nil(j) + end + assert.same({1,2,3,4,5,6,7,8,9,10,11,12}, r) + end) + + it("iterates all values and indices", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + local ri = {} + local rj = {} + for i, j, v in array2d.iter(a,true) do + r[#r+1] = v + ri[#ri+1] = i + rj[#rj+1] = j + end + assert.same({1,2,3,4,5,6,7,8,9,10,11,12}, r) + assert.same({1,1,1,2,2,2,3,3,3,4,4,4}, ri) + assert.same({1,2,3,1,2,3,1,2,3,1,2,3}, rj) + end) + + it("iterates all values of a 2d array part", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + for v, i, j in array2d.iter(a,false,2,2,4,3) do + r[#r+1] = v + assert.is_nil(i) + assert.is_nil(j) + end + assert.same({5,6,8,9,11,12}, r) + end) + + it("iterates all values and indices of a 2d array part", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + local ri = {} + local rj = {} + for i, j, v in array2d.iter(a,true,2,2,4,3) do + r[#r+1] = v + ri[#ri+1] = i + rj[#rj+1] = j + end + assert.same({5,6,8,9,11,12}, r) + assert.same({2,2,3,3,4,4}, ri) + assert.same({2,3,2,3,2,3}, rj) + end) + end) + + describe("columns()", function() + it("iterates all columns", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + for col, idx in array2d.columns(a) do + r[#r+1] = col + col.idx = idx + end + assert.same({{1,4,7,10, idx=1},{2,5,8,11, idx=2},{3,6,9,12, idx=3}}, r) + end) + end) + + describe("rows()", function() + it("iterates all columns", function() + local a = {{1 ,2 ,3}, + {4 ,5 ,6}, + {7 ,8 ,9}, + {10,11,12}} + local r = {} + for row, idx in array2d.rows(a) do + r[#r+1] = row + row.idx = idx + end + assert.same({{1,2,3, idx=1},{4,5,6, idx=2}, + {7,8,9, idx=3},{10,11,12, idx=4}}, r) + end) + end) + +end) diff --git a/tests/test-array2d.lua b/tests/test-array2d.lua deleted file mode 100644 index ab8f41ee..00000000 --- a/tests/test-array2d.lua +++ /dev/null @@ -1,77 +0,0 @@ -local array = require 'pl.array2d' -local asserteq = require('pl.test').asserteq -local L = require 'pl.utils'. string_lambda - -local A = { - {1,2,3,4}, - {10,20,30,40}, - {100,200,300,400}, - {1000,2000,3000,4000}, -} - -asserteq(array.column(A,2),{2,20,200,2000}) -asserteq(array.reduce_rows('+',A),{10,100,1000,10000}) -asserteq(array.reduce_cols('+',A),{1111,2222,3333,4444}) - ---array.write(A) - -local dump = require 'pl.pretty'.dump - -asserteq(array.range(A,'A1:B1'),{1,2}) - -asserteq(array.range(A,'A1:B2'),{{1,2},{10,20}}) - -asserteq( - array.product('..',{1,2,3},{'a','b','c'}), - {{'1a','2a','3a'},{'1b','2b','3b'},{'1c','2c','3c'}} -) - -asserteq( - array.product('{}',{1,2},{'a','b','c'}), - {{{1,'a'},{2,'a'}},{{1,'b'},{2,'b'}},{{1,'c'},{2,'c'}}} -) - -asserteq( - array.flatten {{1,2},{3,4},{5,6}}, - {1,2,3,4,5,6} -) - - -A = {{1,2,3},{4,5,6}} - --- flatten in column order! -asserteq( - array.reshape(A,1,true), - {{1,4,2,5,3,6}} -) - --- regular row-order reshape -asserteq( - array.reshape(A,3), - {{1,2},{3,4},{5,6}} -) - -asserteq( - array.new(3,3,0), - {{0,0,0},{0,0,0},{0,0,0}} -) - -asserteq( - array.new(3,3,L'|i,j| i==j and 1 or 0'), - {{1,0,0},{0,1,0},{0,0,1}} -) - -asserteq( - array.reduce2('+','*',{{1,10},{2,10},{3,10}}), - 60 -- i.e. 1*10 + 2*10 + 3*10 -) - -A = array.new(4,4,0) -B = array.new(3,3,1) -array.move(A,2,2,B) -asserteq(A,{{0,0,0,0},{0,1,1,1},{0,1,1,1},{0,1,1,1}}) - - - - -