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 27, 2024
1 parent db76de5 commit 25e1695
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 3 deletions.
42 changes: 41 additions & 1 deletion core/templates/sort_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ struct _DefaultComparator {
template <typename T, typename Comparator = _DefaultComparator<T>, bool Validate = SORT_ARRAY_VALIDATE_ENABLED>
class SortArray {
enum {
INTROSORT_THRESHOLD = 16
INTROSORT_THRESHOLD = 16,
MERGESORT_THRESHOLD = 8
};

public:
Expand Down Expand Up @@ -315,6 +316,45 @@ class SortArray {
}
introselect(p_first, p_nth, p_last, p_array, bitlog(p_last - p_first) * 2);
}

inline void merge(T *p_array, int64_t p_mid, int64_t p_len, T *p_tmp) {
for (int i = 0; i < p_mid; i++) {
p_tmp[i] = p_array[i];
}

int64_t i1 = 0, i2 = p_mid, idst = 0;
while (i1 < p_mid) {
if (i2 == p_len) {
p_array[idst++] = p_tmp[i1++];
} else if (compare(p_array[i2], p_tmp[i1])) {
p_array[idst++] = p_array[i2++];
} else {
p_array[idst++] = p_tmp[i1++];
}
}
}

inline void stable_sort(T *p_array, int64_t p_len, T *p_tmp) {
if (p_len < MERGESORT_THRESHOLD) {
insertion_sort(0, p_len, p_array);
} else {
int64_t len1 = p_len / 2;
int64_t len2 = p_len - len1;
stable_sort(p_array, len1, p_tmp);
stable_sort(p_array + len1, len2, p_tmp);
merge(p_array, len1, p_len, p_tmp);
}
}

inline void stable_sort(T *p_array, int64_t p_len) {
if (p_len < MERGESORT_THRESHOLD) {
insertion_sort(0, p_len, p_array);
} else {
T *tmp = memnew_arr(T, (p_len / 2));
stable_sort(p_array, p_len, tmp);
memdelete_arr(tmp);
}
}
};

#endif // SORT_ARRAY_H
12 changes: 12 additions & 0 deletions core/variant/array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,23 @@ 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() };
sort.stable_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 } };
sort.stable_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
52 changes: 50 additions & 2 deletions doc/classes/Array.xml
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,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 Down Expand Up @@ -737,10 +737,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
26 changes: 26 additions & 0 deletions modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,32 @@ public void Sort()
NativeFuncs.godotsharp_array_sort(ref self);
}

/// <summary>
/// Sorts the array using a stable sorting algorithm
/// Note: Strings are sorted in alphabetical order (as opposed to natural order).
/// This may lead to unexpected behavior when sorting an array of strings ending
/// with a sequence of numbers.
/// To sort with a custom predicate use
/// <see cref="Enumerable.OrderBy{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>.
/// </summary>
/// <example>
/// <code>
/// var strings = new Godot.Collections.Array { "string1", "string2", "string10", "string11" };
/// strings.SortStable();
/// GD.Print(strings); // Prints [string1, string10, string11, string2]
/// </code>
/// </example>
/// <exception cref="InvalidOperationException">
/// The array is read-only.
/// </exception>
public void SortStable()
{
ThrowIfReadOnly();

var self = (godot_array)NativeValue;
NativeFuncs.godotsharp_array_sort_stable(ref self);
}

/// <summary>
/// Concatenates two <see cref="Array"/>s together, with the <paramref name="right"/>
/// being added to the end of the <see cref="Array"/> specified in <paramref name="left"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ public static partial void godotsharp_array_slice(ref godot_array p_self, int p_

public static partial void godotsharp_array_sort(ref godot_array p_self);

public static partial void godotsharp_array_sort_stable(ref godot_array p_self);

public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str);

// Dictionary
Expand Down
5 changes: 5 additions & 0 deletions modules/mono/glue/runtime_interop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,10 @@ void godotsharp_array_sort(Array *p_self) {
p_self->sort();
}

void godotsharp_array_sort_stable(Array *p_self) {
p_self->sort_stable();
}

void godotsharp_array_to_string(const Array *p_self, String *r_str) {
*r_str = Variant(*p_self).operator String();
}
Expand Down Expand Up @@ -1595,6 +1599,7 @@ static const void *unmanaged_callbacks[]{
(void *)godotsharp_array_shuffle,
(void *)godotsharp_array_slice,
(void *)godotsharp_array_sort,
(void *)godotsharp_array_sort_stable,
(void *)godotsharp_array_to_string,
(void *)godotsharp_dictionary_try_get_value,
(void *)godotsharp_dictionary_set_value,
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 25e1695

Please sign in to comment.