From edf48f6f11b1a8ea773cb91ebda92fd006a4b1e7 Mon Sep 17 00:00:00 2001
From: Yorhel <git@yorhel.nl>
Date: Thu, 3 Feb 2022 10:59:42 +0100
Subject: [PATCH] Use natsort when sorting by name

Fixes #181, now also for Zig.
---
 src/browser.zig |   5 +--
 src/util.zig    | 106 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/src/browser.zig b/src/browser.zig
index 631f908..44fda3f 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -103,11 +103,10 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
         },
     }
 
-    // TODO: Unicode-aware sorting might be nice (and slow)
     const an = a.name();
     const bn = b.name();
-    return if (main.config.sort_order == .asc) std.mem.lessThan(u8, an, bn)
-           else std.mem.lessThan(u8, bn, an);
+    return if (main.config.sort_order == .asc) util.strnatcmp(an, bn) == .lt
+           else util.strnatcmp(bn, an) == .lt;
 }
 
 // Should be called when:
diff --git a/src/util.zig b/src/util.zig
index 7655cae..3d7ba3c 100644
--- a/src/util.zig
+++ b/src/util.zig
@@ -36,3 +36,109 @@ pub fn arrayListBufZ(buf: *std.ArrayList(u8)) [:0]const u8 {
     defer buf.items.len -= 1;
     return buf.items[0..buf.items.len-1:0];
 }
+
+// Straightforward Zig port of strnatcmp() from https://github.com/sourcefrog/natsort/
+// (Requiring nul-terminated strings is ugly, but we've got them anyway and it does simplify the code)
+pub fn strnatcmp(a: [:0]const u8, b: [:0]const u8) std.math.Order {
+    var ai: usize = 0;
+    var bi: usize = 0;
+    const isDigit = std.ascii.isDigit;
+    while (true) {
+        while (std.ascii.isSpace(a[ai])) ai += 1;
+        while (std.ascii.isSpace(b[bi])) bi += 1;
+
+        if (isDigit(a[ai]) and isDigit(b[bi])) {
+            if (a[ai] == '0' or b[bi] == '0') { // compare_left
+                while (true) {
+                    if (!isDigit(a[ai]) and !isDigit(b[bi])) break;
+                    if (!isDigit(a[ai])) return .lt;
+                    if (!isDigit(b[bi])) return .gt;
+                    if (a[ai] < b[bi]) return .lt;
+                    if (a[ai] > b[bi]) return .gt;
+                    ai += 1;
+                    bi += 1;
+                }
+            } else { // compare_right - for right-aligned numbers
+                var bias = std.math.Order.eq;
+                while (true) {
+                    if (!isDigit(a[ai]) and !isDigit(b[bi])) {
+                        if (bias != .eq or (a[ai] == 0 and b[bi] == 0)) return bias
+                        else break;
+                    }
+                    if (!isDigit(a[ai])) return .lt;
+                    if (!isDigit(b[bi])) return .gt;
+                    if (bias == .eq) {
+                        if (a[ai] < b[bi]) bias = .lt;
+                        if (a[ai] > b[bi]) bias = .gt;
+                    }
+                    ai += 1;
+                    bi += 1;
+                }
+            }
+        }
+        if (a[ai] == 0 and b[bi] == 0) return .eq;
+        if (a[ai] < b[bi]) return .lt;
+        if (a[ai] > b[bi]) return .gt;
+        ai += 1;
+        bi += 1;
+    }
+}
+
+test "strnatcmp" {
+    // Test strings from https://github.com/sourcefrog/natsort/
+    // Includes sorted-words, sorted-dates and sorted-fractions.
+    const w = [_][:0]const u8{
+        "1-02",
+        "1-2",
+        "1-20",
+        "1.002.01",
+        "1.002.03",
+        "1.002.08",
+        "1.009.02",
+        "1.009.10",
+        "1.009.20",
+        "1.010.12",
+        "1.011.02",
+        "10-20",
+        "1999-3-3",
+        "1999-12-25",
+        "2000-1-2",
+        "2000-1-10",
+        "2000-3-23",
+        "fred",
+        "jane",
+        "pic01",
+        "pic02",
+        "pic02a",
+        "pic02000",
+        "pic05",
+        "pic2",
+        "pic3",
+        "pic4",
+        "pic 4 else",
+        "pic 5",
+        "pic 5 ",
+        "pic 5 something",
+        "pic 6",
+        "pic   7",
+        "pic100",
+        "pic100a",
+        "pic120",
+        "pic121",
+        "tom",
+        "x2-g8",
+        "x2-y08",
+        "x2-y7",
+        "x8-y8",
+    };
+    // Test each string against each other string, simple and thorough.
+    const eq = std.testing.expectEqual;
+    var i: usize = 0;
+    while (i < w.len) : (i += 1) {
+        var j: usize = 0;
+        try eq(strnatcmp(w[i], w[i]), .eq);
+        while (j < i) : (j += 1) try eq(strnatcmp(w[i], w[j]), .gt);
+        j += 1;
+        while (j < w.len) : (j += 1) try eq(strnatcmp(w[i], w[j]), .lt);
+    }
+}
-- 
GitLab