diff --git a/core/utilities/sorting.lua b/core/utilities/sorting.lua index 13792cf55..f2a72c586 100644 --- a/core/utilities/sorting.lua +++ b/core/utilities/sorting.lua @@ -1,21 +1,30 @@ --- Table sorting with language-dependent collation. -- @module SU.sorting +-- local icu = require("justenoughicu") local collatedSort = { -- No ICU for language "und", fallback to 'natural' table.sort - und = function (t, _) - table.sort(t) + und = function (t, _, comparator) + if comparator then + table.sort(t, function (e1, e2) + return comparator(e1, e2, function (s1, s2) + return s1 < s2 and -1 or s1 > s2 and 1 or 0 + end) + end) + else + table.sort(t) + end end, } setmetatable(collatedSort, { - __call = function (self, t, options) + __call = function (self, t, options, comparator) local lang = SILE.settings:get("document.language") if self[lang] and type(self[lang]) == "function" then -- Allow overriding ICU for some languages, typically "und" - return self[lang](t, options) + return self[lang](t, options, comparator) end if self[lang] and type(self[lang]) == "table" then @@ -25,8 +34,17 @@ setmetatable(collatedSort, { -- Be efficient: create the collator once before sorting. -- I don't think we need to cache it, still. local collator = icu.collation_create(lang, options or {}) - table.sort(t, function (s1, s2) + + local stringCompareClosure = function (s1, s2) return icu.compare(collator, s1, s2) + end + table.sort(t, function (e1, e2) + -- Allow custom comparison function, notably for complex objects + -- Pass the stringCompare function so that it can be used. + if comparator then + return comparator(e1, e2, stringCompareClosure) + end + return stringCompareClosure(e1, e2) < 0 end) icu.collation_destroy(collator) end, diff --git a/justenough/justenoughicu.c b/justenough/justenoughicu.c index 9b305a37d..be309753f 100644 --- a/justenough/justenoughicu.c +++ b/justenough/justenoughicu.c @@ -483,7 +483,7 @@ int je_icu_compare(lua_State *L) { return luaL_error(L, "Internal failure to perform comparison"); } - lua_pushboolean(L, result == UCOL_LESS); + lua_pushinteger(L, result); // UCOL_EQUAL(0), UCOL_GREATER(1), UCOL_LESS(-1) return 1; // IMPLEMENTATION NOTE FOR PORTABILITY // Good news, ucol_strcollUTF8 was introduced in ICU 50. diff --git a/spec/utilities_spec.lua b/spec/utilities_spec.lua index be38561cd..74b22d8b0 100644 --- a/spec/utilities_spec.lua +++ b/spec/utilities_spec.lua @@ -307,6 +307,44 @@ describe("SILE.utilities", function () "Jean-Paul", }, sortme) end) + it("should sort complex tables with callback comparison function", function () + local sortme = { + { name = "Jean", age = 30 }, + { name = "Charlie", age = 25 }, + { name = "Bob", age = 30 }, + { name = "Alice", age = 25 }, + } + SU.collatedSort(sortme, nil, function (a, b, stringCompare) + -- Sort by ascending age then ascending name + if a.age < b.age then return true end + if a.age > b.age then return false end + return stringCompare(a.name, b.name) < 0 + end) + assert.is.same({ + { name = "Alice", age = 25 }, + { name = "Charlie", age = 25 }, + { name = "Bob", age = 30 }, + { name = "Jean", age = 30 }, + }, sortme) + local namesAndYears = { + { name = "Alice", year = 2005 }, + { name = "Charlie", year = 1995 }, + { name = "Bob", year = 1990 }, + { name = "Alice", year = 1995 } + } + SU.collatedSort(namesAndYears, nil, function (a, b, stringCompare) + local nameCompare = stringCompare(a.name, b.name) + if nameCompare < 0 then return true end + if nameCompare > 0 then return false end + return a.year < b.year + end) + assert.is.same({ + { name = "Alice", year = 1995 }, + { name = "Alice", year = 2005 }, + { name = "Bob", year = 1990 }, + { name = "Charlie", year = 1995 }, + }, namesAndYears) + end) end) end) end)