lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Aug 24, 2012 6:59 PM, "Jeff Smith" <spammealot1@live.co.uk> wrote:
>
> Hi
>  
> I am trying to sort a big list of arbitrary keyname strings in a table. I am using the standard table sort with a custom sort function. The little snag I hit was with strings like this
>  
> ABC_1
> ABC_2
> ABC_19
> ABC_20
>  
> I want to do a lexical sort on my strings but for strings like those above, sort in that order, but of course I get
>  
> ABC_1
> ABC_19
> ABC_2
> ABC_20
>  
> I cant think of a concise Lua way of doing a lexical sort of strings but still manage to get these numeric type examples correct too. Any suggestions please ?
>  
> Regards Geoff
>  
>  
>  

I wrote such a function a while ago as part of a yet-unreleased module. Released into public domain. (Sorry if this sends twice. Gmail is being dippy)

-- Splits a string into numeric and non-numeric segments. Used by
-- comparenatural below. (Private function)
-- @param str String to split.
-- @param [number='%d'] character class to be considered numbers.
-- @param [tonum=tonumber] function to convert strings to numbers.
-- @return Table of segments.
local function splitatnumbers(str, number, tonum)
local piece, start = {}, 1
number = number or '%d'
tonum = tonum or tonumber
local pattern = '%f[' .. number .. ']' .. number .. '+%f[^' .. number .. ']'
while true do
--use fronteir pattern ( http://lua-users.org/wiki/FrontierPattern ) to
--split string into numeric and non-numeric portions
local sp, ep = str:find(pattern, start)
if not sp then break end

--insert leading portion if not empty
if (sp-1) > start then piece[#piece+1] = str:sub(start, sp-1) end
piece[#piece+1] = tonum(str:sub(sp, ep))
start = ep+1
end

--insert trailing portion if not empty
if start < #str then piece[#piece+1] = str:sub(start) end
return piece
end

--- Compares two strings, preserving ordering when the strings contain
-- embedded numbers. (e.g. 'foo2' < 'foo10')
-- @param str1 string to compare.
-- @param str2 string to compare.
-- @param number character class to be considered numbers. (default '%d')
-- @param tonum function to convert strings to numbers. (default tonumber)
-- @return -1 if str1 < str2, 0 if str1 == str2, 1 if str1 > str2.
function rena_string.comparenatural(str1, str2, number, tonum)
number, tonum = (number or '%d'), (tonum or tonumber)
local part1 = splitatnumbers(str1, number, tonum)
local part2 = splitatnumbers(str2, number, tonum)

--compare portions
local len1, len2 = #part1, #part2
for i = 1, math.min(len1, len2) do
local num1 = (type(part1[i]) == 'number')
local num2 = (type(part2[i]) == 'number')

if     num1 and not num2 then return -1 --number < string
elseif num2 and not num1 then return  1
elseif part1[i] < part2[i] then return -1
elseif part1[i] > part2[i] then return  1
end
end
if     len1 < len2 then return -1
elseif len2 < len1 then return  1
else return 0
end
end