Skip to content

Commit 30def83

Browse files
committed
feat: add mathematical sorting for pure numeric lists
Pure number lists (only numeric values, including negatives, decimals, and scientific notation) now sort mathematically when natural sort is enabled. Mixed content continues using natural sort. Empty segments are treated as 0.
1 parent aaf9937 commit 30def83

File tree

5 files changed

+598
-5
lines changed

5 files changed

+598
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Mathematical sorting for pure numeric lists. When natural sort is enabled and all items are numbers (integers, decimals, scientific notation), sorting uses numeric comparison. `-90, -10, 5` now sorts correctly instead of lexicographically.
13+
1014
### Fixed
1115

1216
- Natural sorting now treats dashes as separators, not negative signs. `item-10, item-2` sorts as `item-2, item-10` instead of reversed (#18).

lua/sort/sort.lua

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ local M = {}
88
--- @param a string
99
--- @param b string
1010
--- @param options SortOptions
11+
--- @param use_math_sort? boolean
1112
--- @return boolean
12-
local function compare_strings(a, b, options)
13+
local function compare_strings(a, b, options, use_math_sort)
14+
if use_math_sort then
15+
return utils.math_compare(a, b)
16+
end
17+
1318
-- Use natural sorting if enabled.
1419
if options.natural then
1520
return utils.natural_compare(a, b, options.ignore_case)
@@ -129,12 +134,14 @@ M.delimiter_sort = function(text, options)
129134
original_order[i] = item.original_position
130135
end
131136

137+
local use_math_sort = options.natural and utils.all_pure_numbers(items)
138+
132139
-- Sort items by their trimmed content.
133140
table.sort(items, function(a, b)
134141
if options.reverse then
135-
return compare_strings(b.trimmed, a.trimmed, options)
142+
return compare_strings(b.trimmed, a.trimmed, options, use_math_sort)
136143
else
137-
return compare_strings(a.trimmed, b.trimmed, options)
144+
return compare_strings(a.trimmed, b.trimmed, options, use_math_sort)
138145
end
139146
end)
140147

@@ -302,12 +309,15 @@ M.line_sort_text = function(text, options)
302309
items_to_sort = line_items
303310
end
304311

312+
local use_math_sort = options.natural
313+
and utils.all_pure_numbers(items_to_sort)
314+
305315
-- Sort lines using the same comparison logic as delimiter sorting but on trimmed content.
306316
table.sort(items_to_sort, function(a, b)
307317
if options.reverse then
308-
return compare_strings(b.trimmed, a.trimmed, options)
318+
return compare_strings(b.trimmed, a.trimmed, options, use_math_sort)
309319
else
310-
return compare_strings(a.trimmed, b.trimmed, options)
320+
return compare_strings(a.trimmed, b.trimmed, options, use_math_sort)
311321
end
312322
end)
313323

lua/sort/utils.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,45 @@ M.normalize_whitespace = function(
354354
return dominant_pattern
355355
end
356356

357+
--- Rejects '.5', accepts '0.5' and '5.'.
358+
--- @param str string
359+
--- @return boolean
360+
M.is_pure_number = function(str)
361+
if str == nil or str == '' then
362+
return false
363+
end
364+
local has_basic_number = string.match(str, '^[%+%-]?%d+%.?%d*$') ~= nil
365+
local has_scientific = string.match(str, '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+$')
366+
~= nil
367+
return (has_basic_number or has_scientific) and tonumber(str) ~= nil
368+
end
369+
370+
--- Check if all non-empty items in a list are pure numbers.
371+
--- Returns true for empty arrays or arrays with only empty trimmed values.
372+
--- This triggers mathematical sorting for pure number lists and empty segments.
373+
--- @param items table[] Array of items with trimmed field
374+
--- @return boolean
375+
M.all_pure_numbers = function(items)
376+
for _, item in ipairs(items) do
377+
if item.trimmed ~= '' and not M.is_pure_number(item.trimmed) then
378+
return false
379+
end
380+
end
381+
return true
382+
end
383+
384+
--- Falls back to string comparison if tonumber fails (should not occur when
385+
--- called via the sorting pipeline which pre-validates with all_pure_numbers).
386+
--- @param a string
387+
--- @param b string
388+
--- @return boolean
389+
M.math_compare = function(a, b)
390+
local na = a == '' and 0 or tonumber(a)
391+
local nb = b == '' and 0 or tonumber(b)
392+
if na and nb then
393+
return na < nb
394+
end
395+
return a < b
396+
end
397+
357398
return M

0 commit comments

Comments
 (0)