Commit 68f3f8f3 authored by Tobia Tesan's avatar Tobia Tesan Committed by Jim Nelson

Natural sorting of photo titles: Bug #717960

parent ed856388
......@@ -61,6 +61,7 @@ UNUNITIZED_SRC_FILES = \
main.vala \
AppWindow.vala \
CollectionPage.vala \
NaturalCollate.vala \
Thumbnail.vala \
ThumbnailCache.vala \
CheckerboardLayout.vala \
......
......@@ -9,6 +9,7 @@ public class MediaSourceItem : CheckerboardItem {
private static Gdk.Pixbuf current_sprocket_pixbuf = null;
private bool enable_sprockets = false;
private string? natural_collation_key = null;
// preserve the same constructor arguments and semantics as CheckerboardItem so that we're
// a drop-in replacement
......@@ -93,6 +94,19 @@ public class MediaSourceItem : CheckerboardItem {
public void set_enable_sprockets(bool enable_sprockets) {
this.enable_sprockets = enable_sprockets;
}
public new void set_title(string text, bool marked_up = false,
Pango.Alignment alignment = Pango.Alignment.LEFT) {
base.set_title(text, marked_up, alignment);
this.natural_collation_key = null;
}
public string get_natural_collation_key() {
if (this.natural_collation_key == null) {
this.natural_collation_key = NaturalCollate.collate_key(this.get_title());
}
return this.natural_collation_key;
}
}
public abstract class MediaPage : CheckerboardPage {
......
/**
* NaturalCollate
* Simple helper class for natural sorting in Vala.
*
* (c) Tobia Tesan <tobia.tesan@gmail.com>, 2014
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Lesser GNU General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; see the file COPYING. If not,
* see <http://www.gnu.org/licenses/>.
*/
namespace NaturalCollate {
private const unichar SUPERDIGIT = ':';
private const unichar NUM_SENTINEL = 0x2; // glib uses these, so do we
private const string COLLATION_SENTINEL = "\x01\x01\x01";
private static int read_number(owned string s, ref int byte_index) {
/*
* Given a string in the form [numerals]*[everythingelse]*
* returns the int value of the first block and increments index
* by its length as a side effect.
* Notice that "numerals" is not just 0-9 but everything else
* Unicode considers a numeral (see: string::isdigit())
*/
int number = 0;
while (s.length != 0 && s.get_char(0).isdigit()) {
number = number*10;
number += s.get_char(0).digit_value();
int second_char = s.index_of_nth_char(1);
s = s.substring(second_char);
byte_index += second_char;
}
return number;
}
public static int compare(string str1, string str2) {
return strcmp(collate_key(str1), collate_key(str2));
}
public static string collate_key(owned string str) {
/*
* Computes a collate key.
* Has roughly the same effect as g_utf8_collate_key_for_file, except that it doesn't
* handle the dot as a special char.
*/
assert (str.validate());
string result = "";
bool eos = (str.length == 0);
while (!eos) {
assert(str.validate());
int position = 0;
while (!(str.get_char(position).to_string() in "0123456789")) {
// We only care about plain old 0123456789, aping what g_utf8_collate_key_for_filename does
position++;
}
// (0... position( is a bunch of non-numerical chars, so we compute and append the collate key...
result = result + (str.substring(0, position).collate_key());
// ...then throw them away
str = str.substring(position);
eos = (str.length == 0);
position = 0;
if (!eos) {
// We have some numbers to handle in front of us
int number = read_number(str, ref position);
str = str.substring(position);
int number_of_superdigits = number.to_string().length;
string to_append = "";
for (int i = 1; i < number_of_superdigits; i++) {
// We append n - 1 superdigits where n is the number of digits
to_append = to_append + SUPERDIGIT.to_string();
}
to_append = to_append + (number.to_string()); // We append the actual number
result = result +
COLLATION_SENTINEL +
NUM_SENTINEL.to_string() +
to_append;
}
eos = (str.length == 0);
}
result = result + NUM_SENTINEL.to_string();
// No specific reason except that glib does it
return result;
}
}
......@@ -161,8 +161,7 @@ public class Thumbnail : MediaSourceItem {
}
public static int64 title_ascending_comparator(void *a, void *b) {
int64 result = strcmp(((Thumbnail *) a)->media.get_name(), ((Thumbnail *) b)->media.get_name());
int64 result = strcmp(((Thumbnail *) a)->get_natural_collation_key(), ((Thumbnail *) b)->get_natural_collation_key());
return (result != 0) ? result : photo_id_ascending_comparator(a, b);
}
......
NaturalCollate-Test
test: NaturalCollate-Test.vala ../src/NaturalCollate.vala
valac NaturalCollate-Test.vala ../src/NaturalCollate.vala && ./NaturalCollate-Test
clean:
rm NaturalCollate-Test
void add_trailing_numbers_tests () {
Test.add_func ("/vala/test", () => {
string a = "100foo";
string b = "100bar";
string coll_a = NaturalCollate.collate_key(a);
string coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) > 0);
assert(strcmp(a,b) > 0);
assert(NaturalCollate.compare(a,b) == strcmp(coll_a, coll_b));
string atrail = "00100foo";
string btrail = "0100bar";
string coll_atrail = NaturalCollate.collate_key(a);
string coll_btrail = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_atrail) == 0);
assert(strcmp(coll_b, coll_btrail) == 0);
assert(strcmp(coll_atrail, coll_btrail) > 0);
assert(strcmp(atrail,btrail) < 0);
assert(NaturalCollate.compare(atrail,btrail) == strcmp(coll_atrail, coll_btrail));
});
}
void add_numbers_tail_tests () {
Test.add_func ("/vala/test", () => {
string a = "aaa00100";
string b = "aaa02";
string coll_a = NaturalCollate.collate_key(a);
string coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) > 0);
assert(strcmp(a,b) < 0);
assert(NaturalCollate.compare(a,b) == strcmp(coll_a, coll_b));
});
}
void add_dots_tests () {
Test.add_func ("/vala/test", () => {
string sa = "Foo01.jpg";
string sb = "Foo2.jpg";
string sc = "Foo3.jpg";
string sd = "Foo10.jpg";
assert (strcmp(sa, sd) < 0);
assert (strcmp(sd, sb) < 0);
assert (strcmp(sb, sc) < 0);
string coll_sa = NaturalCollate.collate_key(sa);
string coll_sb = NaturalCollate.collate_key(sb);
string coll_sc = NaturalCollate.collate_key(sc);
string coll_sd = NaturalCollate.collate_key(sd);
assert (strcmp(coll_sa, coll_sb) < 0);
assert (strcmp(coll_sb, coll_sc) < 0);
assert (strcmp(coll_sc, coll_sd) < 0);
});
}
void add_bigger_as_strcmp_tests () {
Test.add_func ("/vala/test", () => {
string a = "foo";
string b = "bar";
string coll_a = NaturalCollate.collate_key(a);
string coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a,coll_b) > 0);
assert(strcmp(a,b) > 0);
assert(NaturalCollate.compare(a,b) == strcmp(coll_a, coll_b));
a = "foo0001";
b = "bar0000";
coll_a = NaturalCollate.collate_key(a);
coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a,coll_b) > 0);
assert(strcmp(a,b) > 0);
assert(NaturalCollate.compare(a,b) == strcmp(coll_a, coll_b));
a = "bar010";
b = "bar01";
coll_a = NaturalCollate.collate_key(a);
coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a,coll_b) > 0);
assert(strcmp(a,b) > 0);
assert(NaturalCollate.compare(a,b) == strcmp(coll_a, coll_b));
});
}
void add_numbers_tests() {
Test.add_func ("/vala/test", () => {
string a = "0";
string b = "1";
string coll_a = NaturalCollate.collate_key(a);
string coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) < 0);
a = "100";
b = "101";
coll_a = NaturalCollate.collate_key(a);
coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) < 0);
a = "2";
b = "10";
coll_a = NaturalCollate.collate_key(a);
coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) < 0);
a = "b20";
b = "b100";
coll_a = NaturalCollate.collate_key(a);
coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a, coll_b) < 0);
});
}
void add_ignore_leading_zeros_tests () {
Test.add_func ("/vala/test", () => {
string a = "bar0000010";
string b = "bar10";
string coll_a = NaturalCollate.collate_key(a);
string coll_b = NaturalCollate.collate_key(b);
assert(strcmp(coll_a,coll_b) == 0);
});
}
void main (string[] args) {
GLib.Intl.setlocale(GLib.LocaleCategory.ALL, "");
Test.init (ref args);
add_trailing_numbers_tests();
add_numbers_tail_tests();
add_bigger_as_strcmp_tests();
add_ignore_leading_zeros_tests();
add_numbers_tests();
add_dots_tests();
Test.run();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment