Implement version-aware sorting of file listings.
authorNick Bowler <nbowler@draconx.ca>
Wed, 17 Feb 2021 01:32:02 +0000 (20:32 -0500)
committerNick Bowler <nbowler@draconx.ca>
Wed, 17 Feb 2021 01:59:50 +0000 (20:59 -0500)
Based on glibc strverscmp, except with better awareness of version
components.  Now slotifier-1.1.tar.gz will appear after slotifier-1,
as expected.

layouts/listing.xhtml
lib/strverscmp.rb [new file with mode: 0644]

index 2e053d5e085aab11a4a2ce88ab9d23f6f94d2d5c..aad56081170f2d33949ca3db6614b4d6e1d6cfb7 100644 (file)
@@ -54,7 +54,7 @@ end
     <tr><th /><th>Name</th><th>Last Modified</th><th>Size</th></tr>
   </thead>
   <tbody>
-<% files.keys.sort.each do |key| %>
+<% files.keys.sort{ |a, b| strverscmp(a, b) }.each do |key| %>
     <tr>
       <td>
 <% if files[key][:type] %>
diff --git a/lib/strverscmp.rb b/lib/strverscmp.rb
new file mode 100644 (file)
index 0000000..1f90c62
--- /dev/null
@@ -0,0 +1,117 @@
+# Ruby implementation of glibc strverscmp-workalike, with some improvements.
+#
+# Copyright © 2021 Nick Bowler
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
+# This implementation is partially adapted from the GNU C Library, covered by
+# the following copyright and permission notice:
+#
+#   Copyright (C) 1997-2018 Free Software Foundation, Inc.
+#
+#   The GNU C Library is free software; you can redistribute it and/or
+#   modify it under the terms of the GNU Lesser General Public
+#   License as published by the Free Software Foundation; either
+#   version 2.1 of the License, or (at your option) any later version.
+
+module SVC
+    ZERO = "0".ord
+    NINE = "9".ord
+    A_UP = "A".ord
+    A_LO = "a".ord
+    Z_UP = "Z".ord
+    Z_LO = "z".ord
+    DOT  = ".".ord
+
+    def SVC.my_ord(s)
+        s ? s.ord : 0
+    end
+
+    def SVC.isdigit(x)
+        return x >= ZERO && x <= NINE
+    end
+
+    def SVC.isdigitnz(x)
+        return x > ZERO && x <= NINE
+    end
+
+    def SVC.isalpha(x)
+        return x >= A_UP && x <= A_LO ||
+               x >= A_LO && x <= Z_LO
+    end
+
+    def SVC.rank(x, xs, i)
+        return 3 if isdigit(x)
+        return 2 if x == DOT and isdigit(my_ord(xs[i+1]))
+        return 1 if isalpha(x)
+        return 0
+    end
+end
+
+def strverscmp(as, bs)
+    state = :normal
+    i = 0
+
+    # locate first difference, tracking context
+    while true
+        a, b = SVC.my_ord(as[i]), SVC.my_ord(bs[i])
+
+        break if a == 0 or a != b
+        i = i+1
+
+        if a == SVC::ZERO
+            state = :zeroes if state == :normal or state == :dot
+        elsif a > SVC::ZERO and a <= SVC::NINE
+            state = :integral if state == :normal or state == :dot
+            state = :fractional if state == :zeroes
+        elsif a == SVC::DOT
+            state = :dot unless state == :normal
+        else
+            state = :normal
+        end
+    end
+
+    # found first differing character
+    diff = a - b
+
+    case state
+    when :fractional
+        return diff
+    when :normal
+        return diff unless SVC.isdigitnz(a) and SVC.isdigitnz(b)
+    when :zeroes
+        ra, rb = SVC.rank(a, as, i), SVC.rank(b, bs, i)
+
+        if ( cmp = ra <=> rb ) != 0
+            return cmp if a == SVC::DOT and SVC.isalpha(b)
+            return cmp if b == SVC::DOT and SVC.isalpha(a)
+            return -cmp
+        end
+
+        return diff
+    when :integral
+        ra, rb = SVC.rank(a, as, i), SVC.rank(b, bs, i)
+        return ra <=> rb if ra != rb
+        return diff unless SVC.isdigit(b)
+    end
+
+    while SVC.isdigit(a)
+        return 1 unless SVC.isdigit(b)
+        i = i + 1
+        a, b = SVC.my_ord(as[i]), SVC.my_ord(bs[i])
+    end
+
+    return -1 if SVC.isdigit(b)
+    return diff
+end