hslua-list-1.1.1/0000755000000000000000000000000007346545000011770 5ustar0000000000000000hslua-list-1.1.1/CHANGELOG.md0000644000000000000000000000126307346545000013603 0ustar0000000000000000# Changelog `hslua-list` uses [PVP Versioning](https://pvp.haskell.org). ## hslua-list-1.1.1 Released 2023-03-17. - Conversion to strings: added a `__tostring` that lists all elements separated by commas and a space, surrounded by braces and prefixed with the metatable's name. ## hslua-list-1.1.0.1 Released 2023-01-23. - Ensure that `test/test-list.lua` is included in the release tarball. ## hslua-list-1.1.0 Released 2023-03-13. - Removed `pushPandocList`. The function was a left-over from pandoc-lua-marshal, the place where this package originated. ## hslua-list-1.0.0 Released 2022-10-10. - To boldly go where no Haskell library has gone before. hslua-list-1.1.1/LICENSE0000644000000000000000000000205007346545000012772 0ustar0000000000000000Copyright © 2021-2023 Albert Krewinkel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hslua-list-1.1.1/README.md0000644000000000000000000000030707346545000013247 0ustar0000000000000000hslua-list ========== Opinionated, fast, and extensible implementation of a Lua *List* type. The type re-uses some parts of Lua's base library, so the *base* library must be loaded before this one. hslua-list-1.1.1/cbits/0000755000000000000000000000000007346545000013074 5ustar0000000000000000hslua-list-1.1.1/cbits/listmod.c0000644000000000000000000002302707346545000014717 0ustar0000000000000000#include #include "lua.h" #include "lauxlib.h" #include "lualib.h" #define LIST_T "List" /* compatibility with older Lua versions, which did not define this in the * header. */ #ifndef LUA_LOADED_TABLE /* key, in the registry, for table of loaded modules */ #define LUA_LOADED_TABLE "_LOADED" #endif /* ** Placeholder function. */ static int missing (lua_State *L) { return luaL_error(L, "Function should have been overwritten with one from the table module." ); } /* Translate a relative table position: negative means back from end */ static lua_Integer posrelat (lua_Integer pos, size_t len) { if (pos >= 0) return pos; else if (0u - (size_t)pos > len) return 0; else return (lua_Integer)len + pos + 1; } /* ** Check that 'arg' is either a function or a different callable object. */ static void checkcallable (lua_State *L, int arg) { if (lua_type(L, arg) != LUA_TFUNCTION) { /* is it not a function? */ if (luaL_getmetafield(L, arg, "__call")) lua_pop(L, 1); /* pop metamethod */ else luaL_checktype(L, arg, LUA_TFUNCTION); /* force an error */ } } /* ** Creates a List from a table; uses a fresh, empty table if none is ** given. */ static int list_new (lua_State *L) { lua_settop(L, 2); if (lua_isnoneornil(L, 2)) { lua_newtable(L); lua_remove(L, 2); } else { luaL_checktype(L, 2, LUA_TTABLE); } lua_pushvalue(L, 1); lua_setmetatable(L, 2); return 1; } /* ** Creates a shallow clone of the given list; the clone will contain ** only the list elements, not any other elements that might have been ** present. */ static int list_clone (lua_State *L) { lua_settop(L, 1); luaL_checktype(L, 1, LUA_TTABLE); lua_Integer len = luaL_len(L, 1); lua_createtable(L, len, 0); /* create new table */ lua_getmetatable(L, 1); lua_setmetatable(L, 2); for (lua_Integer i = 1; i <= len; i++) { lua_geti(L, 1, i); lua_seti(L, 2, i); } return 1; } /* ** Creates a new list that is the concatenation of its two arguments. ** The result has the same metatable as the first operand. */ static int list_concat (lua_State *L) { lua_settop(L, 2); luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 2, LUA_TTABLE); lua_Integer len1 = luaL_len(L, 1); lua_Integer len2 = luaL_len(L, 2); lua_createtable(L, len1 + len2, 0); /* result table */ if (lua_getmetatable(L, 1)) { lua_setmetatable(L, 3); } for (lua_Integer i = 1; i <= len1; i++) { lua_geti(L, 1, i); lua_seti(L, 3, i); } for (lua_Integer i = 1; i <= len2; i++) { lua_geti(L, 2, i); lua_seti(L, 3, len1 + i); } return 1; } /* ** Checks equality. Two lists are equal if and only if they have the same ** metatable and if all items are equal. */ static int list_eq (lua_State *L) { lua_settop(L, 2); /* compare meta tables */ if (!(lua_getmetatable(L, 1) && lua_getmetatable(L, 2) && lua_rawequal(L, -1, -2))) { lua_pushboolean(L, 0); return 1; }; lua_pop(L, 2); /* remove metatables */ /* ensure both lists have the same length */ lua_Integer len1 = luaL_len(L, 1); lua_Integer len2 = luaL_len(L, 2); if (len1 != len2) { lua_pushboolean(L, 0); return 1; } /* check element-wise equality */ for (lua_Integer i = 1; i <= len1; i++) { lua_geti(L, 1, i); lua_geti(L, 2, i); if (!lua_compare(L, -1, -2, LUA_OPEQ)) { lua_pushboolean(L, 0); return 1; } } lua_pushboolean(L, 1); return 1; } /* ** Appends the second list to the first. */ static int list_extend (lua_State *L) { lua_settop(L, 2); luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 2, LUA_TTABLE); lua_Integer len1 = luaL_len(L, 1); lua_Integer len2 = luaL_len(L, 2); for (lua_Integer i = 1; i <= len2; i++) { lua_geti(L, 2, i); lua_seti(L, 1, len1 + i); } return 1; } /* ** Removes elements that do not have the desired property. */ static int list_filter (lua_State *L) { lua_settop(L, 2); luaL_checktype(L, 1, LUA_TTABLE); checkcallable(L, 2); luaL_checkstack(L, 4, NULL); lua_Integer len = luaL_len(L, 1); lua_createtable(L, len, 0); /* create new table */ lua_getmetatable(L, 1); lua_setmetatable(L, 3); for (lua_Integer i = 1, j = 0; i <= len; i++) { lua_pushvalue(L, 2); /* push predicate function */ lua_geti(L, 1, i); lua_pushinteger(L, i); lua_call(L, 2, 1); if (lua_toboolean(L, -1)) { lua_geti(L, 1, i); lua_seti(L, 3, ++j); } lua_pop(L, 1); /* remove predicate call result */ } return 1; } /* ** Returns the first element that is equal to `needle`, along with that ** element's index, or `nil` if no such element exists. */ static int list_find (lua_State *L) { luaL_checkstack(L, 2, "List.find"); lua_settop(L, 3); luaL_checktype(L, 1, LUA_TTABLE); lua_Integer len = luaL_len(L, 1); lua_Integer start = posrelat(luaL_optinteger(L, 3, 1), len); for (lua_Integer i = start; i <= len; i++) { lua_geti(L, 1, i); if (lua_compare(L, 2, -1, LUA_OPEQ)) { lua_pushinteger(L, i); return 2; } lua_pop(L, 1); /* remove list element result */ } lua_pushnil(L); return 1; } /* ** Returns the first element after the given start index for which the ** predicate function returns a truthy value, along with that element's ** index; returns `nil` if no such element exists. */ static int list_find_if (lua_State *L) { lua_settop(L, 3); luaL_checktype(L, 1, LUA_TTABLE); checkcallable(L, 2); lua_Integer len = luaL_len(L, 1); lua_Integer start = posrelat(luaL_optinteger(L, 3, 1), len); for (lua_Integer i = start; i <= len; i++) { lua_pushvalue(L, 2); /* predicate function */ lua_geti(L, 1, i); lua_pushinteger(L, i); lua_call(L, 2, 1); if (lua_toboolean(L, -1)) { lua_geti(L, 1, i); lua_pushinteger(L, i); return 2; } lua_pop(L, 1); /* remove predicate call result */ } lua_pushnil(L); return 1; } /* ** Returns a boolean value indicating whether or not the element exists ** in the given list. */ static int list_includes(lua_State *L) { lua_settop(L, 3); lua_pushcfunction(L, list_find); lua_insert(L, 1); lua_call(L, 3, 1); luaL_checkstack(L, 1, "List.includes"); lua_pushboolean(L, lua_toboolean(L, -1)); return 1; } /* ** Returns a copy of the current list by applying the given function to ** all elements. */ static int list_map(lua_State *L) { lua_settop(L, 2); luaL_checktype(L, 1, LUA_TTABLE); checkcallable(L, 2); lua_Integer len = luaL_len(L, 1); lua_createtable(L, len, 0); /* create new table */ luaL_getmetatable(L, LIST_T); /* make result a generic list */ lua_setmetatable(L, 3); for (lua_Integer i = 1; i <= len; i++) { lua_pushvalue(L, 2); /* map function */ lua_geti(L, 1, i); lua_pushinteger(L, i); lua_call(L, 2, 1); lua_seti(L, 3, i); } return 1; } static void addfield (lua_State *L, luaL_Buffer *b, lua_Integer i) { lua_geti(L, 1, i); luaL_tolstring(L, -1, NULL); lua_remove(L, -2); /* element */ luaL_addvalue(b); } /* ** Convert the list to a string. */ static int list_tostring(lua_State *L) { luaL_Buffer b; lua_Integer len = luaL_len(L, 1); luaL_buffinit(L, &b); /* Prefix the string with name from metatable */ if (luaL_getmetafield(L, 1, "__name") != LUA_TNIL) { luaL_addvalue(&b); luaL_addchar(&b, ' '); } luaL_addchar(&b, '{'); lua_Integer i = 1; for (; i < len; i++) { addfield(L, &b, i); luaL_addstring(&b, ", "); } if (i == len) /* add last value (if interval was not empty) */ addfield(L, &b, i); luaL_addchar(&b, '}'); luaL_pushresult(&b); return 1; } /* ** Pushes the standard `table` module to the stack. */ static void pushtablemodule (lua_State *L) { lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); if (!lua_getfield(L, -1, LUA_TABLIBNAME)) { /* apparently it's not been loaded yes. So open it here (but don't * 'load' it). */ lua_pushcfunction(L, luaopen_table); lua_pushliteral(L, LUA_TABLIBNAME); lua_call(L, 1, 1); } lua_remove(L, -2); /* remove LOADED table */ } /* ** Fields to copy from standard `table` package. */ static const char *tablelib_functions[] = { "insert", "remove", "sort", NULL }; /* ** Copy fields from standard `table` module to the table at the given ** index. */ static void copyfromtablelib (lua_State *L, int idx) { int absidx = lua_absindex(L, idx); pushtablemodule(L); for (const char **name = tablelib_functions; *name != NULL; *name++) { if (lua_getfield(L, -1, *name)) { lua_setfield(L, absidx, *name); } else { lua_pop(L, 1); } } lua_pop(L, 1); /* remove table module */ } static const luaL_Reg list_funcs[] = { {"__concat", list_concat}, {"__eq", list_eq}, {"__tostring", list_tostring}, {"clone", list_clone}, {"extend", list_extend}, {"filter", list_filter}, {"find", list_find}, {"find_if", list_find_if}, {"includes", list_includes}, {"insert", missing}, {"map", list_map}, {"new", list_new}, {"remove", missing}, {"sort", missing}, {NULL, NULL} }; static const luaL_Reg metareg[] = { {"__call", list_new}, {NULL, NULL} }; /* ** Creates a new metatable for a new List-like type. */ int lualist_newmetatable (lua_State *L, const char *name) { if (luaL_newmetatable(L, name)) { luaL_setfuncs(L, list_funcs, 0); /* use functions from standard table module. */ copyfromtablelib(L, -1); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); return 1; } return 0; } int luaopen_list (lua_State *L) { luaL_checkversion(L); lualist_newmetatable(L, LIST_T); lua_newtable(L); luaL_setfuncs(L, metareg, 0); lua_setmetatable(L, -2); return 1; } hslua-list-1.1.1/hslua-list.cabal0000644000000000000000000000454707346545000015053 0ustar0000000000000000cabal-version: 2.2 name: hslua-list version: 1.1.1 synopsis: Opinionated, but extensible Lua list type. description: List type for Lua, with a Haskell interface. homepage: https://hslua.org/ bug-reports: https://github.com/hslua/hslua/issues license: MIT license-file: LICENSE author: Albert Krewinkel maintainer: Albert Krewinkel copyright: © 2022-2023 Albert Krewinkel category: Foreign build-type: Simple extra-doc-files: README.md CHANGELOG.md extra-source-files: test/test-list.lua tested-with: GHC == 8.4.4 , GHC == 8.6.5 , GHC == 8.8.4 , GHC == 8.10.7 , GHC == 9.0.2 , GHC == 9.2.5 , GHC == 9.4.4 source-repository head type: git location: https://github.com/hslua/hslua subdir: hslua-list common common-options build-depends: base >= 4.9.1 && < 5 , hslua-core >= 2.2 && < 2.4 ghc-options: -Wall -Wcompat -Widentities -Wincomplete-uni-patterns -Wincomplete-record-updates if impl(ghc >= 8.0) ghc-options: -Wredundant-constraints if impl(ghc >= 8.2) ghc-options: -fhide-source-paths if impl(ghc >= 8.4) ghc-options: -Wmissing-export-lists -Wpartial-fields if impl(ghc >= 8.10) ghc-options: -Wunused-packages if impl(ghc >= 9.0) ghc-options: -Winvalid-haddock default-language: Haskell2010 library import: common-options hs-source-dirs: src c-sources: cbits/listmod.c exposed-modules: HsLua.List build-depends: bytestring test-suite hslua-list-test import: common-options type: exitcode-stdio-1.0 hs-source-dirs: test main-is: test-hslua-list.hs build-depends: hslua-list , tasty >= 0.11 , tasty-lua >= 1.0 ghc-options: -threaded -rtsopts -with-rtsopts=-N hslua-list-1.1.1/src/HsLua/0000755000000000000000000000000007346545000013573 5ustar0000000000000000hslua-list-1.1.1/src/HsLua/List.hs0000644000000000000000000000262207346545000015044 0ustar0000000000000000{-# LANGUAGE LambdaCase #-} {- | Copyright : © 2021-2023 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Lua lists with additional methods. -} module HsLua.List ( luaopen_list_ptr , pushListModule , newListMetatable ) where import Data.ByteString (useAsCString) import Foreign.C import HsLua.Core -- | Pointer to the function that opens the List module and pushes it to the -- stack. foreign import ccall unsafe "listmod.c &luaopen_list" luaopen_list_ptr :: CFunction -- | Opens the List module and pushes it to the stack. pushListModule :: LuaError e => LuaE e () pushListModule = do pushcfunction luaopen_list_ptr call 0 1 -- | Creates a new list metatable with the given name. foreign import ccall "listmod.c lualist_newmetatable" lualist_newmetatable :: State -> CString -> IO CInt -- | Pushes the metatable of the given List type, creating it if -- necessary. The @setup@ operation is run when the metatable did not -- exists, was created, and is then at the top of the stack. The -- operation may modify the table but must be balanced, and must leave -- the stack as it found it. newListMetatable :: Name -> LuaE e () {-^ setup -} -> LuaE e () newListMetatable (Name name) setup = do l <- state liftIO (useAsCString name (lualist_newmetatable l)) >>= \case 0 -> pure () -- metatable already registered; no need to setup again _ -> setup hslua-list-1.1.1/test/0000755000000000000000000000000007346545000012747 5ustar0000000000000000hslua-list-1.1.1/test/test-hslua-list.hs0000644000000000000000000000162207346545000016346 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-| Module : Main Copyright : © 2021-2023 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Tests for the list type. -} module Main (main) where import HsLua.Core as Lua import HsLua.List import Test.Tasty (defaultMain, testGroup) import Test.Tasty.Lua (translateResultsFromFile) main :: IO () main = do listTests <- run @Lua.Exception $ do openlibs -- Init default List module pushListModule *> setglobal "List" -- Create a custom List type with constructor "CustomList" pushHaskellFunction $ do settop 1 newListMetatable "CustomList" (pure ()) setmetatable (nthBottom 1) return 1 setglobal "CustomList" translateResultsFromFile "test/test-list.lua" defaultMain $ testGroup "hslua-list" [listTests] hslua-list-1.1.1/test/test-list.lua0000644000000000000000000002564607346545000015417 0ustar0000000000000000-- -- Tests for the list type -- local tasty = require 'tasty' local group = tasty.test_group local test = tasty.test_case local assert = tasty.assert return { group 'List' { test('is a table', function () assert.are_equal(type(List), 'table') end), group 'constructor' { test('returns a new list if called without args', function () assert.are_same(List(), {}) end), }, group 'clone' { test('changing the clone does not affect original', function () local orig = List:new {23, 42} local copy = orig:clone() copy[1] = 5 assert.are_same({23, 42}, orig) assert.are_same({5, 42}, copy) end), test('result is a list', function () local orig = List:new {23, 42} assert.are_equal(List, getmetatable(orig:clone())) end), }, group 'extend' { test('extends list with other list', function () local primes = List:new {2, 3, 5, 7} primes:extend {11, 13, 17} assert.are_same({2, 3, 5, 7, 11, 13, 17}, primes) end) }, group 'filter' { test('keep elements for which property is truthy', function () local is_small_prime = function (x) return x == 2 or x == 3 or x == 5 or x == 7 end local numbers = List:new {4, 7, 2, 9, 5, 11} assert.are_same(List{7, 2, 5}, numbers:filter(is_small_prime)) end), test('predecate function gets current index as second arg', function () local args = List() local collect_args = function (...) args[#args+1] = {...} return false end (List{0, 1, 1, 2, 3, 5, 8}):filter(collect_args) assert.are_same( args, List{{0, 1}, {1, 2}, {1, 3}, {2, 4}, {3, 5}, {5, 6}, {8, 7}} ) end), test('accepts callable table as function', function () local always_true = function (t, x) return true end local callable = setmetatable({}, {__call = always_true}) assert.are_same( List{0}, List{0}:filter(callable) ) end), test('fails on non-callable table', function () assert.error_matches( function () List{1}:filter({}) end, 'bad argument %#1 to \'filter\' %(function expected, got table%)' ) end), }, group 'find' { test('returns element and index if found', function () local list = List:new {5, 23, 71} local elem, idx = list:find(71) assert.are_same(71, elem) assert.are_same(3, idx) end), test('respects start index', function () local list = List:new {19, 23, 29, 71} assert.are_equal(23, list:find(23, 1)) assert.are_equal(23, list:find(23, 2)) assert.are_equal(23, list:find(23, -4)) assert.is_nil(list:find(23, 3)) assert.is_nil(list:find(23, -2)) end), test('returns nil if element not found', function () assert.is_nil((List:new {18, 20, 22, 0, 24}):find('0')) end), test('fails if start index is not an integer', function () assert.error_matches( function () List:new{}:find(0, 'NaN') end, 'number expected, got string' ) end), }, group 'find_if' { test('returns element and index if found', function () local perm_prime = List:new {2, 3, 5, 7, 11, 13, 17, 31, 37, 71} local elem, idx = perm_prime:find_if(function (x) return x >= 10 end) assert.are_same(11, elem) assert.are_same(5, idx) end), test('gets current index as second arg', function () assert.are_equal( 5, (List{9, 8, 7, 6, 5, 4, 3, 2, 1}):find_if( function (x, i) return x == i end ) ) end), test('returns nil if element not found', function () local is_zero = function (n) return n == 0 end assert.is_nil((List:new {18, 20, 22, 24, 27}):find_if(is_zero)) end), test('respects start index', function () local list = List:new {9, 29, 3, 71} assert.are_equal(71, list:find_if(function(n) return n > 10 end, 3)) assert.are_equal(29, list:find_if(function(n) return n > 10 end, -3)) end), test('fails if start index is not an integer', function () assert.error_matches( function () List:new{}:find(0, 'NaN') end, 'number expected, got string' ) end), test('accepts callable table', function () local always_true = function (t, x) return true end local callable = setmetatable({}, {__call = always_true}) assert.are_equal(List{1}:find_if(callable), 1) end), test('fails on non-callable table', function () assert.error_matches( function () List():find_if({}) end, 'bad argument %#1 to \'find_if\' %(function expected, got table%)' ) end), }, group 'includes' { test('finds elements in list', function () local lst = List:new {'one', 'two', 'three'} assert.is_truthy(lst:includes('one')) assert.is_truthy(lst:includes('two')) assert.is_truthy(lst:includes('three')) assert.is_falsy(lst:includes('four')) end), test('doesn\'t crash with long lists', function () local lst = List:new() for i = 1, 1000 do lst[#lst + 1] = tostring(i) end assert.is_truthy(lst:includes '999') end) }, group 'insert' { test('is a function', function () assert.are_equal(type(List.insert), 'function') end), test('insert value at end of list.', function () local count_norsk = List {'en', 'to', 'tre'} count_norsk:insert('fire') assert.are_same({'en', 'to', 'tre', 'fire'}, count_norsk) end), test('insert value in the middle of list.', function () local count_norsk = List {'fem', 'syv'} count_norsk:insert(2, 'seks') assert.are_same({'fem', 'seks', 'syv'}, count_norsk) end) }, group 'map' { test('applies function to elements', function () local primes = List:new {2, 3, 5, 7} local squares = primes:map(function (x) return x^2 end) assert.are_same({4, 9, 25, 49}, squares) end), test('leaves original list unchanged', function () local primes = List:new {2, 3, 5, 7} local squares = primes:map(function (x) return x^2 end) assert.are_same({2, 3, 5, 7}, primes) end), test('map function gets index as second argument', function () local primes = List:new {2, 3, 5, 7} local indices = primes:map(function (x, i) return i end) assert.are_same(List{1, 2, 3, 4}, indices) end), test('map returns a generic list', function () local custom = CustomList{'α', 'β'} assert.are_equal(debug.getmetatable(custom).__name, 'CustomList') assert.are_same( debug.getmetatable(custom:map(tostring)).__name, 'List' ) end), test('accepts callable table', function () local plus_length = function (t, x) return x + #t end local callable = setmetatable({1, 2}, {__call = plus_length}) assert.are_equal(List{1, 3}:map(callable), List{3, 5}) end), test('fails on non-callable table', function () assert.error_matches( function () List{1}:map({}) end, 'bad argument %#1 to \'map\' %(function expected, got table%)' ) end), }, group 'new' { test('keeps elements in list', function () local test = {1, 1, 2, 3, 5} assert.are_same(List:new(test), test) end), test('return empty list if no argument is given', function () assert.are_same({}, List:new()) end), test('metatable of result is List', function () local test = List:new{5} assert.are_equal(List, getmetatable(test)) end) }, group 'remove' { test('is a function', function () assert.are_equal(type(List.remove), 'function') end), test('remove value at end of list.', function () local understand = List {'jeg', 'forstår', 'ikke'} local norsk_not = understand:remove() assert.are_same({'jeg', 'forstår'}, understand) assert.are_equal('ikke', norsk_not) end), test('remove value at beginning of list.', function () local count_norsk = List {'en', 'to', 'tre'} count_norsk:remove(1) assert.are_same({'to', 'tre'}, count_norsk) end) }, group 'sort' { test('is a function', function () assert.are_equal(type(List.sort), 'function') end), test('sort numeric list', function () local numbers = List {71, 5, -1, 42, 23, 0, 1} numbers:sort() assert.are_same({-1, 0, 1, 5, 23, 42, 71}, numbers) end), test('reverse-sort numeric', function () local numbers = List {71, 5, -1, 42, 23, 0, 1} numbers:sort(function (x, y) return x > y end) assert.are_same({71, 42, 23, 5, 1, 0, -1}, numbers) end) }, }, group 'Operations' { group 'concatenation' { test('yields a concatenated list', function () assert.are_same(List {3, 4, 5, 6}, List{3, 4} .. List {5, 6}) end), test('does not modify its operands', function () local a = List {54, 74} local b = List {90, 2014} local result = a .. b assert.are_same(a, List{54, 74}) assert.are_same(b, List{90, 2014}) end), test('sets metatable of first operand on result', function () local result = {1, 4} .. List{9, 16} assert.are_equal(nil, getmetatable(result)) result = List{1, 4} .. {9, 16} assert.are_equal(List, getmetatable(result)) end), }, group 'equality' { test('lists are equal if all elements are equal', function () assert.are_equal( List {5, 6, 7, 8}, List {5, 6, 7, 8} ) end), test('lists are not equal if their metatables are different', function () assert.is_truthy( List {18, 20, 2, 0, 24} ~= CustomList {18, 20, 2, 0, 24} ) end), test('lists are not equal if one is a plain table', function () assert.is_truthy( List {18, 20, 2, 0, 24} ~= {18, 20, 2, 0, 24} ) end), test('lists are not equal if an element differs', function () assert.is_truthy( List {18, 20, 22, 23, 24} ~= List {18, 20, 22, 0, 24} ) end), test('can compare to a string', function () assert.is_truthy( List {'a', 'b', 'c'} ~= "abc" ) end), }, group 'tostring' { test('lists all elements', function () assert.are_equal( tostring(List {5, 23, 42}), 'List {5, 23, 42}' ) end), test('empty list', function () assert.are_equal( tostring(List {}), 'List {}' ) end), } }, }