Skip to content

Commit

Permalink
Add Array.sort_stable and Array.sort_custom_stable
Browse files Browse the repository at this point in the history
Implemented stable sorting for Arrays using merge sort/insertion sort
  • Loading branch information
aaronp64 committed Aug 25, 2024
1 parent 568589c commit a71da05
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 3 deletions.
36 changes: 36 additions & 0 deletions core/templates/sort_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,42 @@ class SortArray {
}
introselect(p_first, p_nth, p_last, p_array, bitlog(p_last - p_first) * 2);
}

inline void merge(T *p_src, int64_t p_mid, int64_t p_len, T *p_dst) {
int64_t i1 = 0, i2 = p_mid, idst = 0;
while (idst < p_len) {
if (i1 == p_mid) {
p_dst[idst++] = p_src[i2++];
} else if (i2 == p_len) {
p_dst[idst++] = p_src[i1++];
} else if (compare(p_src[i2], p_src[i1])) {
p_dst[idst++] = p_src[i2++];
} else {
p_dst[idst++] = p_src[i1++];
}
}
}

inline void merge_sort(T *p_src, T *p_dst, int64_t p_len) {
if (p_len > 1) {
int64_t len1 = p_len / 2;
int64_t len2 = p_len - len1;
merge_sort(p_dst, p_src, len1);
merge_sort(p_dst + len1, p_src + len1, len2);
merge(p_src, len1, p_len, p_dst);
}
}

inline void merge_sort(T *p_array, int64_t p_len) {
if (p_len > 1) {
T *copy = memnew_arr(T, p_len);
for (int i = 0; i < p_len; i++) {
copy[i] = p_array[i];
}
merge_sort(copy, p_array, p_len);
memdelete_arr(copy);
}
}
};

#endif // SORT_ARRAY_H
20 changes: 20 additions & 0 deletions core/variant/array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,31 @@ void Array::sort() {
_p->array.sort_custom<_ArrayVariantSort>();
}

void Array::sort_stable() {
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
SortArray<Variant, _ArrayVariantSort> sort{ _ArrayVariantSort() };
if (size() < 10) {
sort.insertion_sort(0, size(), _p->array.ptrw());
} else {
sort.merge_sort(_p->array.ptrw(), size());
}
}

void Array::sort_custom(const Callable &p_callable) {
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
_p->array.sort_custom<CallableComparator, true>(p_callable);
}

void Array::sort_custom_stable(const Callable &p_callable) {
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
SortArray<Variant, CallableComparator, true> sort{ CallableComparator{ p_callable } };
if (size() < 10) {
sort.insertion_sort(0, size(), _p->array.ptrw());
} else {
sort.merge_sort(_p->array.ptrw(), size());
}
}

void Array::shuffle() {
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
const int n = _p->array.size();
Expand Down
2 changes: 2 additions & 0 deletions core/variant/array.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ class Array {
Variant pick_random() const;

void sort();
void sort_stable();
void sort_custom(const Callable &p_callable);
void sort_custom_stable(const Callable &p_callable);
void shuffle();
int bsearch(const Variant &p_value, bool p_before = true) const;
int bsearch_custom(const Variant &p_value, const Callable &p_callable, bool p_before = true) const;
Expand Down
2 changes: 2 additions & 0 deletions core/variant/variant_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2297,7 +2297,9 @@ static void _register_variant_builtin_methods_array() {
bind_method(Array, pop_front, sarray(), varray());
bind_method(Array, pop_at, sarray("position"), varray());
bind_method(Array, sort, sarray(), varray());
bind_method(Array, sort_stable, sarray(), varray());
bind_method(Array, sort_custom, sarray("func"), varray());
bind_method(Array, sort_custom_stable, sarray("func"), varray());
bind_method(Array, shuffle, sarray(), varray());
bind_method(Array, bsearch, sarray("value", "before"), varray(true));
bind_method(Array, bsearch_custom, sarray("value", "func", "before"), varray(true));
Expand Down
54 changes: 51 additions & 3 deletions doc/classes/Array.xml
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@
GD.Print(numbers); // Prints [2.5, 5, 8, 10]
[/csharp]
[/codeblocks]
[b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that equivalent elements (such as [code]2[/code] and [code]2.0[/code]) may have their order changed when calling [method sort].
[b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that equivalent elements (such as [code]2[/code] and [code]2.0[/code]) may have their order changed when calling [method sort]. See also [method sort_stable]
</description>
</method>
<method name="sort_custom">
Expand All @@ -728,7 +728,7 @@
print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9]]

# Sort descending, using a lambda function.
my_items.sort_custom(func(a, b): return a[0] &gt; b[0])
my_items.sort_custom(func(a, b): return a[1] &gt; b[1])
print(my_items) # Prints [["Apple", 9], ["Tomato", 5], ["Rice", 4]]
[/codeblock]
It may also be necessary to use this method to sort strings by natural order, with [method String.naturalnocasecmp_to], as in the following example:
Expand All @@ -738,10 +738,58 @@
print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"]
[/codeblock]
[b]Note:[/b] In C#, this method is not supported.
[b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when calling this method.
[b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when calling this method. See also [method sort_custom_stable]
[b]Note:[/b] You should not randomize the return value of [param func], as the heapsort algorithm expects a consistent result. Randomizing the return value will result in unexpected behavior.
</description>
</method>
<method name="sort_custom_stable">
<return type="void" />
<param index="0" name="func" type="Callable" />
<description>
Sorts the array using a [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url] sort algorithm with a custom [Callable]. Equal elements remain in the same order after sorting.
[param func] is called as many times as necessary, receiving two array elements as arguments. The function should return [code]true[/code] if the first element should be moved [i]behind[/i] the second one, otherwise it should return [code]false[/code].
[codeblock]
func sort_ascending(a, b):
if a[1] &lt; b[1]:
return true
return false

func _ready():
var my_items = [["Tomato", 5], ["Apple", 9], ["Rice", 4], ["Orange", 9]]
my_items.sort_custom_stable(sort_ascending)
print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9], ["Orange", 9]]

# Sort descending, using a lambda function.
my_items.sort_custom_stable(func(a, b): return a[1] &gt; b[1])
print(my_items) # Prints [["Apple", 9], ["Orange", 9], ["Tomato", 5], ["Rice", 4]]
[/codeblock]
It may also be necessary to use this method to sort strings by natural order, with [method String.naturalnocasecmp_to], as in the following example:
[codeblock]
var files = ["newfile1", "newfile2", "newfile10", "newfile11"]
files.sort_custom_stable(func(a, b): return a.naturalnocasecmp_to(b) &lt; 0)
print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"]
[/codeblock]
[b]Note:[/b] In C#, this method is not supported.
</description>
</method>
<method name="sort_stable">
<return type="void" />
<description>
Sorts the array in ascending order using a [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url] sort algorithm. The final order is dependent on the "less than" ([code]&lt;[/code]) comparison between elements. Equal elements remain in the same order after sorting.
[codeblocks]
[gdscript]
var numbers = [10, 5, 2.5, 8, 8.0]
numbers.sort_stable()
print(numbers) # Prints [2.5, 5, 8, 8.0, 10]
[/gdscript]
[csharp]
var numbers = new Godot.Collections.Array { 10, 5, 2.5, 8, 8.0 };
numbers.SortStable();
GD.Print(numbers); // Prints [2.5, 5, 8, 8.0, 10]
[/csharp]
[/codeblocks]
</description>
</method>
</methods>
<operators>
<operator name="operator !=">
Expand Down
46 changes: 46 additions & 0 deletions tests/core/variant/test_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,52 @@ TEST_CASE("[Array] sort()") {
}
}

TEST_CASE("[Array] sort_stable() small") {
Array arr;

arr.push_back(2);
arr.push_back(2.0);
arr.push_back(3);
arr.push_back(3.0);
arr.push_back(1);
arr.push_back(1.0);
arr.sort_stable();
int val = 1;
for (int i = 0; i < arr.size(); i += 2) {
CHECK(arr[i].get_type() == Variant::Type::INT);
CHECK(int(arr[i]) == val);
CHECK(arr[i + 1].get_type() == Variant::Type::FLOAT);
CHECK(float(arr[i + 1]) == val);
val++;
}
}

TEST_CASE("[Array] sort_stable()") {
Array arr;

arr.push_back(3);
arr.push_back(3.0);
arr.push_back(4);
arr.push_back(4.0);
arr.push_back(2);
arr.push_back(2.0);
arr.push_back(1);
arr.push_back(1.0);
arr.push_back(6);
arr.push_back(6.0);
arr.push_back(5);
arr.push_back(5.0);
arr.sort_stable();
int val = 1;
for (int i = 0; i < arr.size(); i += 2) {
CHECK(arr[i].get_type() == Variant::Type::INT);
CHECK(int(arr[i]) == val);
CHECK(arr[i + 1].get_type() == Variant::Type::FLOAT);
CHECK(float(arr[i + 1]) == val);
val++;
}
}

TEST_CASE("[Array] push_front(), pop_front(), pop_back()") {
Array arr;
arr.push_front(1);
Expand Down

0 comments on commit a71da05

Please sign in to comment.