Skip to content

Commit 25e1695

Browse files
committed
Add Array.sort_stable and Array.sort_custom_stable
Implemented stable sorting for Arrays using merge sort/insertion sort
1 parent db76de5 commit 25e1695

File tree

9 files changed

+186
-3
lines changed

9 files changed

+186
-3
lines changed

core/templates/sort_array.h

+41-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ struct _DefaultComparator {
5454
template <typename T, typename Comparator = _DefaultComparator<T>, bool Validate = SORT_ARRAY_VALIDATE_ENABLED>
5555
class SortArray {
5656
enum {
57-
INTROSORT_THRESHOLD = 16
57+
INTROSORT_THRESHOLD = 16,
58+
MERGESORT_THRESHOLD = 8
5859
};
5960

6061
public:
@@ -315,6 +316,45 @@ class SortArray {
315316
}
316317
introselect(p_first, p_nth, p_last, p_array, bitlog(p_last - p_first) * 2);
317318
}
319+
320+
inline void merge(T *p_array, int64_t p_mid, int64_t p_len, T *p_tmp) {
321+
for (int i = 0; i < p_mid; i++) {
322+
p_tmp[i] = p_array[i];
323+
}
324+
325+
int64_t i1 = 0, i2 = p_mid, idst = 0;
326+
while (i1 < p_mid) {
327+
if (i2 == p_len) {
328+
p_array[idst++] = p_tmp[i1++];
329+
} else if (compare(p_array[i2], p_tmp[i1])) {
330+
p_array[idst++] = p_array[i2++];
331+
} else {
332+
p_array[idst++] = p_tmp[i1++];
333+
}
334+
}
335+
}
336+
337+
inline void stable_sort(T *p_array, int64_t p_len, T *p_tmp) {
338+
if (p_len < MERGESORT_THRESHOLD) {
339+
insertion_sort(0, p_len, p_array);
340+
} else {
341+
int64_t len1 = p_len / 2;
342+
int64_t len2 = p_len - len1;
343+
stable_sort(p_array, len1, p_tmp);
344+
stable_sort(p_array + len1, len2, p_tmp);
345+
merge(p_array, len1, p_len, p_tmp);
346+
}
347+
}
348+
349+
inline void stable_sort(T *p_array, int64_t p_len) {
350+
if (p_len < MERGESORT_THRESHOLD) {
351+
insertion_sort(0, p_len, p_array);
352+
} else {
353+
T *tmp = memnew_arr(T, (p_len / 2));
354+
stable_sort(p_array, p_len, tmp);
355+
memdelete_arr(tmp);
356+
}
357+
}
318358
};
319359

320360
#endif // SORT_ARRAY_H

core/variant/array.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -632,11 +632,23 @@ void Array::sort() {
632632
_p->array.sort_custom<_ArrayVariantSort>();
633633
}
634634

635+
void Array::sort_stable() {
636+
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
637+
SortArray<Variant, _ArrayVariantSort> sort{ _ArrayVariantSort() };
638+
sort.stable_sort(_p->array.ptrw(), size());
639+
}
640+
635641
void Array::sort_custom(const Callable &p_callable) {
636642
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
637643
_p->array.sort_custom<CallableComparator, true>(p_callable);
638644
}
639645

646+
void Array::sort_custom_stable(const Callable &p_callable) {
647+
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
648+
SortArray<Variant, CallableComparator, true> sort{ CallableComparator{ p_callable } };
649+
sort.stable_sort(_p->array.ptrw(), size());
650+
}
651+
640652
void Array::shuffle() {
641653
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
642654
const int n = _p->array.size();

core/variant/array.h

+2
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ class Array {
145145
Variant pick_random() const;
146146

147147
void sort();
148+
void sort_stable();
148149
void sort_custom(const Callable &p_callable);
150+
void sort_custom_stable(const Callable &p_callable);
149151
void shuffle();
150152
int bsearch(const Variant &p_value, bool p_before = true) const;
151153
int bsearch_custom(const Variant &p_value, const Callable &p_callable, bool p_before = true) const;

core/variant/variant_call.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -2297,7 +2297,9 @@ static void _register_variant_builtin_methods_array() {
22972297
bind_method(Array, pop_front, sarray(), varray());
22982298
bind_method(Array, pop_at, sarray("position"), varray());
22992299
bind_method(Array, sort, sarray(), varray());
2300+
bind_method(Array, sort_stable, sarray(), varray());
23002301
bind_method(Array, sort_custom, sarray("func"), varray());
2302+
bind_method(Array, sort_custom_stable, sarray("func"), varray());
23012303
bind_method(Array, shuffle, sarray(), varray());
23022304
bind_method(Array, bsearch, sarray("value", "before"), varray(true));
23032305
bind_method(Array, bsearch_custom, sarray("value", "func", "before"), varray(true));

doc/classes/Array.xml

+50-2
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@
706706
GD.Print(numbers); // Prints [2.5, 5, 8, 10]
707707
[/csharp]
708708
[/codeblocks]
709-
[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].
709+
[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]
710710
</description>
711711
</method>
712712
<method name="sort_custom">
@@ -737,10 +737,58 @@
737737
print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"]
738738
[/codeblock]
739739
[b]Note:[/b] In C#, this method is not supported.
740-
[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.
740+
[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]
741741
[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.
742742
</description>
743743
</method>
744+
<method name="sort_custom_stable">
745+
<return type="void" />
746+
<param index="0" name="func" type="Callable" />
747+
<description>
748+
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.
749+
[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].
750+
[codeblock]
751+
func sort_ascending(a, b):
752+
if a[1] &lt; b[1]:
753+
return true
754+
return false
755+
756+
func _ready():
757+
var my_items = [["Tomato", 5], ["Apple", 9], ["Rice", 4], ["Orange", 9]]
758+
my_items.sort_custom_stable(sort_ascending)
759+
print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9], ["Orange", 9]]
760+
761+
# Sort descending, using a lambda function.
762+
my_items.sort_custom_stable(func(a, b): return a[1] &gt; b[1])
763+
print(my_items) # Prints [["Apple", 9], ["Orange", 9], ["Tomato", 5], ["Rice", 4]]
764+
[/codeblock]
765+
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:
766+
[codeblock]
767+
var files = ["newfile1", "newfile2", "newfile10", "newfile11"]
768+
files.sort_custom_stable(func(a, b): return a.naturalnocasecmp_to(b) &lt; 0)
769+
print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"]
770+
[/codeblock]
771+
[b]Note:[/b] In C#, this method is not supported.
772+
</description>
773+
</method>
774+
<method name="sort_stable">
775+
<return type="void" />
776+
<description>
777+
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.
778+
[codeblocks]
779+
[gdscript]
780+
var numbers = [10, 5, 2.5, 8, 8.0]
781+
numbers.sort_stable()
782+
print(numbers) # Prints [2.5, 5, 8, 8.0, 10]
783+
[/gdscript]
784+
[csharp]
785+
var numbers = new Godot.Collections.Array { 10, 5, 2.5, 8, 8.0 };
786+
numbers.SortStable();
787+
GD.Print(numbers); // Prints [2.5, 5, 8, 8.0, 10]
788+
[/csharp]
789+
[/codeblocks]
790+
</description>
791+
</method>
744792
</methods>
745793
<operators>
746794
<operator name="operator !=">

modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs

+26
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,32 @@ public void Sort()
473473
NativeFuncs.godotsharp_array_sort(ref self);
474474
}
475475

476+
/// <summary>
477+
/// Sorts the array using a stable sorting algorithm
478+
/// Note: Strings are sorted in alphabetical order (as opposed to natural order).
479+
/// This may lead to unexpected behavior when sorting an array of strings ending
480+
/// with a sequence of numbers.
481+
/// To sort with a custom predicate use
482+
/// <see cref="Enumerable.OrderBy{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>.
483+
/// </summary>
484+
/// <example>
485+
/// <code>
486+
/// var strings = new Godot.Collections.Array { "string1", "string2", "string10", "string11" };
487+
/// strings.SortStable();
488+
/// GD.Print(strings); // Prints [string1, string10, string11, string2]
489+
/// </code>
490+
/// </example>
491+
/// <exception cref="InvalidOperationException">
492+
/// The array is read-only.
493+
/// </exception>
494+
public void SortStable()
495+
{
496+
ThrowIfReadOnly();
497+
498+
var self = (godot_array)NativeValue;
499+
NativeFuncs.godotsharp_array_sort_stable(ref self);
500+
}
501+
476502
/// <summary>
477503
/// Concatenates two <see cref="Array"/>s together, with the <paramref name="right"/>
478504
/// being added to the end of the <see cref="Array"/> specified in <paramref name="left"/>.

modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs

+2
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ public static partial void godotsharp_array_slice(ref godot_array p_self, int p_
423423

424424
public static partial void godotsharp_array_sort(ref godot_array p_self);
425425

426+
public static partial void godotsharp_array_sort_stable(ref godot_array p_self);
427+
426428
public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str);
427429

428430
// Dictionary

modules/mono/glue/runtime_interop.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,10 @@ void godotsharp_array_sort(Array *p_self) {
11381138
p_self->sort();
11391139
}
11401140

1141+
void godotsharp_array_sort_stable(Array *p_self) {
1142+
p_self->sort_stable();
1143+
}
1144+
11411145
void godotsharp_array_to_string(const Array *p_self, String *r_str) {
11421146
*r_str = Variant(*p_self).operator String();
11431147
}
@@ -1595,6 +1599,7 @@ static const void *unmanaged_callbacks[]{
15951599
(void *)godotsharp_array_shuffle,
15961600
(void *)godotsharp_array_slice,
15971601
(void *)godotsharp_array_sort,
1602+
(void *)godotsharp_array_sort_stable,
15981603
(void *)godotsharp_array_to_string,
15991604
(void *)godotsharp_dictionary_try_get_value,
16001605
(void *)godotsharp_dictionary_set_value,

tests/core/variant/test_array.h

+46
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,52 @@ TEST_CASE("[Array] sort()") {
169169
}
170170
}
171171

172+
TEST_CASE("[Array] sort_stable() small") {
173+
Array arr;
174+
175+
arr.push_back(2);
176+
arr.push_back(2.0);
177+
arr.push_back(3);
178+
arr.push_back(3.0);
179+
arr.push_back(1);
180+
arr.push_back(1.0);
181+
arr.sort_stable();
182+
int val = 1;
183+
for (int i = 0; i < arr.size(); i += 2) {
184+
CHECK(arr[i].get_type() == Variant::Type::INT);
185+
CHECK(int(arr[i]) == val);
186+
CHECK(arr[i + 1].get_type() == Variant::Type::FLOAT);
187+
CHECK(float(arr[i + 1]) == val);
188+
val++;
189+
}
190+
}
191+
192+
TEST_CASE("[Array] sort_stable()") {
193+
Array arr;
194+
195+
arr.push_back(3);
196+
arr.push_back(3.0);
197+
arr.push_back(4);
198+
arr.push_back(4.0);
199+
arr.push_back(2);
200+
arr.push_back(2.0);
201+
arr.push_back(1);
202+
arr.push_back(1.0);
203+
arr.push_back(6);
204+
arr.push_back(6.0);
205+
arr.push_back(5);
206+
arr.push_back(5.0);
207+
arr.sort_stable();
208+
int val = 1;
209+
for (int i = 0; i < arr.size(); i += 2) {
210+
CHECK(arr[i].get_type() == Variant::Type::INT);
211+
CHECK(int(arr[i]) == val);
212+
CHECK(arr[i + 1].get_type() == Variant::Type::FLOAT);
213+
CHECK(float(arr[i + 1]) == val);
214+
val++;
215+
}
216+
}
217+
172218
TEST_CASE("[Array] push_front(), pop_front(), pop_back()") {
173219
Array arr;
174220
arr.push_front(1);

0 commit comments

Comments
 (0)