From 817cbbd184b3aa6cdf41e5b373343ce6bb48a563 Mon Sep 17 00:00:00 2001 From: "Ben L. Titzer" Date: Tue, 24 Sep 2024 06:58:23 -0400 Subject: [PATCH] [util] Add Ranges.quicksort (#277) --- lib/util/Ranges.v3 | 44 +++++++++++- lib/util/StringBuilder.v3 | 9 +++ test/lib/RangesTest.v3 | 147 ++++++++++++++++++++++++++++++++++---- 3 files changed, 186 insertions(+), 14 deletions(-) diff --git a/lib/util/Ranges.v3 b/lib/util/Ranges.v3 index 05373f195..5555230bf 100644 --- a/lib/util/Ranges.v3 +++ b/lib/util/Ranges.v3 @@ -3,7 +3,22 @@ // Utility methods for Ranges. component Ranges { - // Make a copy of a range (as an array). + // Compute a hash of {r}, given a {hash} function for each element. + def hash(h0: int, r: Range, hash: T -> int) -> int { + for (e in r) h0 = h0 * 33 + hash(e); + return h0; + } + // Map {func} over an input {array}, returning a new array of the results. + def map(r: Range, func: A -> B) -> Array { + var result = Array.new(r.length); + for (i < r.length) result[i] = func(r[i]); + return result; + } + // Map {func} over an input {src}, storing the results in the {dest}. + def mapInto(src: Range, func: A -> B, dest: Range) { + for (i < src.length) dest[i] = func(src[i]); + } + // Make a copy of a range as an array. def dup(r: Range) -> Array { var result = Array.new(r.length); for (i < r.length) result[i] = r[i]; @@ -34,4 +49,31 @@ component Ranges { } return range; } + // Sort the range {r} in place using Quicksort using the {cmp} function. + def quicksort(r: Range, cmp: (T, T) -> bool) { + if (r.length <= 1) return; + if (r.length == 2) { + var a = r[0], b = r[1]; + if (cmp(b, a)) { r[0] = b; r[1] = a; } + return; + } + def pi = partition(r, cmp); + if (pi > 1) quicksort(r[0 ... pi], cmp); + if (pi < r.length) quicksort(r[pi ...], cmp); + } + private def partition(r: Range, cmp: (T, T) -> bool) -> int { + var i = 0, j = r.length - 1; + def pivot = r[j]; + while (i <= j) { + var a = r[i]; + if (cmp(a, pivot)) { + i++; + } else { + r[i] = r[j]; + r[j] = a; + j--; + } + } + return i; + } } diff --git a/lib/util/StringBuilder.v3 b/lib/util/StringBuilder.v3 index 80997ec68..8494134a0 100644 --- a/lib/util/StringBuilder.v3 +++ b/lib/util/StringBuilder.v3 @@ -184,6 +184,15 @@ class StringBuilder { var l = Utf8.encode(codepoint, buf, p); length += l; } + // Append a range of values as [v1, v2, ...] with each element rendered with the format specifier {fmt}. + def putRange(fmt: string, r: Range) -> this { + putc('['); + for (i < r.length) { + if (i > 0) csp(); + put1(fmt, r[i]); + } + putc(']'); + } // Append a line terminator. def ln() -> this { putc('\n'); diff --git a/test/lib/RangesTest.v3 b/test/lib/RangesTest.v3 index e4946f776..a875973f8 100644 --- a/test/lib/RangesTest.v3 +++ b/test/lib/RangesTest.v3 @@ -5,36 +5,157 @@ def T = LibTests.register("Ranges", _, _); def X = [ T("reverse_int", test_reverse_int), T("reverse_string", test_reverse_string), + T("sort_ints", test_sort_ints), + T("sort_floats", test_sort_floats), + T("sort_strings", test_sort_strings), + T("sort_perm3", test_sort_perm3), + T("sort_perm5", test_sort_perm5), () ]; -def assert_reverse(t: LibTest, expected: Range, input: Range) { - Ranges.reverse(input); +def putall(buf: StringBuilder, r: Range) -> StringBuilder { + buf.puts("["); + for (i < r.length) { + if (i > 0) buf.csp(); + var v = r[i]; + if (string.?(v)) buf.putsq(string.!(v)); + else buf.putd(v); + } + return buf.puts("]"); +} + +def report(t: LibTest, expected: Range, got: Range) { + var buf = StringBuilder.new().puts("expected: "); + putall(buf, expected); + buf.puts(", got "); + putall(buf, got); + t.fail(buf.toString()); +} + +def assert_func_in_place(t: LibTest, func: Range -> void, expected: Range, input: Range) { + func(input); var got = input; // reverse is done in place if (got.length != expected.length) return t.fail("wrong result length"); - for (i < expected.length) t.asserteq(expected[i], got[i]); + for (i < expected.length) if (expected[i] != got[i]) return report(t, expected, got); } def test_reverse_int(t: LibTest) { - def p = assert_reverse(t, _, _); + def test = assert_func_in_place(t, Ranges.reverse, _, _); - p([], []); - p([1], [1]); - p([3, 4], [4, 3]); - p([-44, -55, -66], [-66, -55, -44]); + test([], []); + test([1], [1]); + test([3, 4], [4, 3]); + test([-44, -55, -66], [-66, -55, -44]); } def test_reverse_string(t: LibTest) { - def p = assert_reverse(t, _, _); + def test = assert_func_in_place(t, Ranges.reverse, _, _); def foo = "foo", bar = "bar"; def ganz = "ganz", geschichte = "geschichte", pflanz = "pflanz", gebaut = "gebaut", danke = "danke", dir = "dir", sehr = "sehr"; - p([], []); - p([bar], [bar]); - p([foo, bar], [bar, foo]); - p( + test([], []); + test([bar], [bar]); + test([foo, bar], [bar, foo]); + test( [ganz, geschichte, pflanz, gebaut, danke, dir, sehr], [sehr, dir, danke, gebaut, pflanz, geschichte, ganz] ); } + +def test_sort_ints(t: LibTest) { + def test = assert_func_in_place(t, Ranges.quicksort(_, int.<), _, _); + + test([], []); + test([1], [1]); + test([-1, 1], [1, -1]); + test([-1, 1], [-1, 1]); + test([-3, -2], [-3, -2]); + test([-3, -2], [-2, -3]); + test([0, 1, 2], [2, 0, 1]); + test([0, 1, 2], [1, 2, 0]); + test([0, 1, 2, 3], [3, 2, 0, 1]); + test([0, 1, 2, 3, 4], [3, 2, 0, 1, 4]); + test([0, 1, 2, 3, 4, 5], [5, 3, 2, 0, 1, 4]); +} + +def test_sort_floats(t: LibTest) { + def test = assert_func_in_place(t, Ranges.quicksort(_, float.<), _, _); + + test([], []); + test([1], [1]); + test([1, 1.1f], [1, 1.1f]); + test([-1.1f, 1], [-1.1f, 1]); + test([1, float.infinity], [1, float.infinity]); + test([1, float.infinity], [float.infinity, 1]); + + test([0, 1, 2], [2, 0, 1]); + test([0, 1, 2], [1, 2, 0]); + test([0, 1, 2, 3], [3, 2, 0, 1]); + test([0, 1, 2, 3, 4], [3, 2, 0, 1, 4]); + test([0, 1, 2, 3, 4, 5], [5, 3, 2, 0, 1, 4]); +} + +def test_sort_strings(t: LibTest) { + def test = assert_func_in_place(t, Ranges.quicksort(_, Strings.asciiLt), _, _); + def foo = "foo", bar = "bar"; + def ganz = "ganz", geschichte = "geschichte", pflanz = "pflanz", gebaut = "gebaut", danke = "danke", dir = "dir", sehr = "sehr"; + + test([], []); + test([foo], [foo]); + test([bar, foo], [foo, bar]); + test([bar, danke, dir, ganz, gebaut, geschichte, pflanz], + [gebaut, bar, dir, ganz, geschichte, pflanz, danke]); +} + +def test_sort_perm3(t: LibTest) { + def test = assert_func_in_place(t, Ranges.quicksort(_, i64.<), _, _); + def X = 0L, Y = 0x1_0000_0000L, Z = 0x1000_0000_0000_0000L; + def expected = [X, Y, Z]; + + test(expected, [X, Y, Z]); + test(expected, [X, Z, Y]); + test(expected, [Y, X, Z]); + test(expected, [Y, Z, X]); + test(expected, [Z, X, Y]); + test(expected, [Z, Y, X]); +} + +def test_sort_perm5(t: LibTest) { + def sorted = [0uL, 1uL, 2uL, 3uL, 4uL]; + def sort = Ranges.quicksort(_, u64.<); + def check = assert_func_in_place(t, sort, sorted, _); + Permuter.new(sorted).genDupTo(check); +} + +class Permuter(set: Range) { + private def storage = Array.new(set.length); + + def genAll() -> Array> { + var vec = Vector>.new(); + enumRec(0, 0, dupToA(_, vec.put)); + return vec.extract(); + } + def genTo(f: Range -> R) { + enumRec(0, 0, f); + } + def genDupTo(f: Range -> R) { + enumRec(0, 0, dupTo(_, f)); + } + private def enumRec(pos: int, mask: int, f: Range -> R) { + if (pos == set.length) return void(f(storage)); + for (i < set.length) { + var bit = 1 << u5.!(i); + if ((mask & bit) == 0) { + storage[pos] = set[i]; + enumRec(pos + 1, mask | bit, f); + } + } + } + private def dupToA(r: Range, f: Array -> R) { + f(Ranges.dup(r)); + } + private def dupTo(r: Range, f: Range -> R) { + f(Ranges.dup(r)); + } +} \ No newline at end of file