Skip to content

Commit 51eb21d

Browse files
committed
Provide stable sorting when JS engine sorting doesn't look stable
#KT-12473
1 parent 7c3c454 commit 51eb21d

File tree

5 files changed

+176
-31
lines changed

5 files changed

+176
-31
lines changed

libraries/stdlib/js/irRuntime/generated/_ArraysJs.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,8 +1128,7 @@ public actual fun IntArray.sort(): Unit {
11281128
* Sorts the array in-place.
11291129
*/
11301130
public actual fun LongArray.sort(): Unit {
1131-
if (size > 1)
1132-
sort { a: Long, b: Long -> a.compareTo(b) }
1131+
if (size > 1) sort { a: Long, b: Long -> a.compareTo(b) }
11331132
}
11341133

11351134
/**
@@ -1171,16 +1170,14 @@ public actual fun CharArray.sort(): Unit {
11711170
* Sorts the array in-place according to the natural order of its elements.
11721171
*/
11731172
public actual fun <T : Comparable<T>> Array<out T>.sort(): Unit {
1174-
if (size > 1)
1175-
sort { a: T, b: T -> a.compareTo(b) }
1173+
if (size > 1) sortArray(this)
11761174
}
11771175

11781176
/**
11791177
* Sorts the array in-place according to the order specified by the given [comparison] function.
11801178
*/
1181-
@kotlin.internal.InlineOnly
1182-
public inline fun <T> Array<out T>.sort(noinline comparison: (a: T, b: T) -> Int): Unit {
1183-
asDynamic().sort(comparison)
1179+
public fun <T> Array<out T>.sort(comparison: (a: T, b: T) -> Int): Unit {
1180+
if (size > 1) sortArrayWith(this, comparison)
11841181
}
11851182

11861183
/**
@@ -1243,8 +1240,7 @@ public inline fun CharArray.sort(noinline comparison: (a: Char, b: Char) -> Int)
12431240
* Sorts the array in-place according to the order specified by the given [comparator].
12441241
*/
12451242
public actual fun <T> Array<out T>.sortWith(comparator: Comparator<in T>): Unit {
1246-
if (size > 1)
1247-
sort { a, b -> comparator.compare(a, b) }
1243+
if (size > 1) sortArrayWith(this, comparator)
12481244
}
12491245

12501246
/**

libraries/stdlib/js/src/generated/_ArraysJs.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,8 +1159,7 @@ public actual fun IntArray.sort(): Unit {
11591159
* Sorts the array in-place.
11601160
*/
11611161
public actual fun LongArray.sort(): Unit {
1162-
if (size > 1)
1163-
sort { a: Long, b: Long -> a.compareTo(b) }
1162+
if (size > 1) sort { a: Long, b: Long -> a.compareTo(b) }
11641163
}
11651164

11661165
/**
@@ -1207,16 +1206,14 @@ public actual fun CharArray.sort(): Unit {
12071206
* Sorts the array in-place according to the natural order of its elements.
12081207
*/
12091208
public actual fun <T : Comparable<T>> Array<out T>.sort(): Unit {
1210-
if (size > 1)
1211-
sort { a: T, b: T -> a.compareTo(b) }
1209+
if (size > 1) sortArray(this)
12121210
}
12131211

12141212
/**
12151213
* Sorts the array in-place according to the order specified by the given [comparison] function.
12161214
*/
1217-
@kotlin.internal.InlineOnly
1218-
public inline fun <T> Array<out T>.sort(noinline comparison: (a: T, b: T) -> Int): Unit {
1219-
asDynamic().sort(comparison)
1215+
public fun <T> Array<out T>.sort(comparison: (a: T, b: T) -> Int): Unit {
1216+
if (size > 1) sortArrayWith(this, comparison)
12201217
}
12211218

12221219
/**
@@ -1279,8 +1276,7 @@ public inline fun CharArray.sort(noinline comparison: (a: Char, b: Char) -> Int)
12791276
* Sorts the array in-place according to the order specified by the given [comparator].
12801277
*/
12811278
public actual fun <T> Array<out T>.sortWith(comparator: Comparator<in T>): Unit {
1282-
if (size > 1)
1283-
sort { a, b -> comparator.compare(a, b) }
1279+
if (size > 1) sortArrayWith(this, comparator)
12841280
}
12851281

12861282
/**
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
3+
* that can be found in the license/LICENSE.txt file.
4+
*/
5+
6+
package kotlin.collections
7+
8+
internal fun <T> sortArrayWith(array: Array<out T>, comparison: (T, T) -> Int) {
9+
if (getStableSortingIsSupported()) {
10+
array.asDynamic().sort(comparison)
11+
} else {
12+
mergeSort(array.unsafeCast<Array<T>>(), 0, array.lastIndex, Comparator(comparison))
13+
}
14+
}
15+
16+
internal fun <T> sortArrayWith(array: Array<out T>, comparator: Comparator<in T>) {
17+
if (getStableSortingIsSupported()) {
18+
val comparison = { a: T, b: T -> comparator.compare(a, b) }
19+
array.asDynamic().sort(comparison)
20+
} else {
21+
mergeSort(array.unsafeCast<Array<T>>(), 0, array.lastIndex, comparator)
22+
}
23+
}
24+
25+
internal fun <T : Comparable<T>> sortArray(array: Array<out T>) {
26+
if (getStableSortingIsSupported()) {
27+
val comparison = { a: T, b: T -> a.compareTo(b) }
28+
array.asDynamic().sort(comparison)
29+
} else {
30+
mergeSort(array.unsafeCast<Array<T>>(), 0, array.lastIndex, naturalOrder())
31+
}
32+
}
33+
34+
private var _stableSortingIsSupported: Boolean? = null
35+
private fun getStableSortingIsSupported(): Boolean {
36+
_stableSortingIsSupported?.let { return it }
37+
_stableSortingIsSupported = false
38+
39+
val array = js("[]").unsafeCast<Array<Int>>()
40+
// known implementations may use stable sort for arrays of up to 512 elements
41+
// so we create slightly more elements to test stability
42+
for (index in 0 until 600) array.asDynamic().push(index)
43+
val comparison = { a: Int, b: Int -> (a and 3) - (b and 3) }
44+
array.asDynamic().sort(comparison)
45+
for (index in 1 until array.size) {
46+
val a = array[index - 1]
47+
val b = array[index]
48+
if ((a and 3) == (b and 3) && a >= b) return false
49+
}
50+
_stableSortingIsSupported = true
51+
return true
52+
}
53+
54+
55+
private fun <T> mergeSort(array: Array<T>, start: Int, endInclusive: Int, comparator: Comparator<in T>) {
56+
val buffer = arrayOfNulls<Any?>(array.size).unsafeCast<Array<T>>()
57+
val result = mergeSort(array, buffer, start, endInclusive, comparator)
58+
if (result !== array) {
59+
result.forEachIndexed { i, v -> array[i] = v }
60+
}
61+
}
62+
63+
// Both start and end are inclusive indices.
64+
private fun <T> mergeSort(array: Array<T>, buffer: Array<T>, start: Int, end: Int, comparator: Comparator<in T>): Array<T> {
65+
if (start == end) {
66+
return array
67+
}
68+
69+
val median = (start + end) / 2
70+
val left = mergeSort(array, buffer, start, median, comparator)
71+
val right = mergeSort(array, buffer, median + 1, end, comparator)
72+
73+
val target = if (left === buffer) array else buffer
74+
75+
// Merge.
76+
var leftIndex = start
77+
var rightIndex = median + 1
78+
for (i in start..end) {
79+
when {
80+
leftIndex <= median && rightIndex <= end -> {
81+
val leftValue = left[leftIndex]
82+
val rightValue = right[rightIndex]
83+
84+
if (comparator.compare(leftValue, rightValue) <= 0) {
85+
target[i] = leftValue
86+
leftIndex++
87+
} else {
88+
target[i] = rightValue
89+
rightIndex++
90+
}
91+
}
92+
leftIndex <= median -> {
93+
target[i] = left[leftIndex]
94+
leftIndex++
95+
}
96+
else /* rightIndex <= end */ -> {
97+
target[i] = right[rightIndex]
98+
rightIndex++
99+
}
100+
}
101+
}
102+
103+
return target
104+
}

libraries/stdlib/test/collections/ArraysTest.kt

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
2+
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
33
* that can be found in the license/LICENSE.txt file.
44
*/
55

@@ -1412,6 +1412,21 @@ class ArraysTest {
14121412
}
14131413
}
14141414

1415+
@Test fun sortStable() {
1416+
val keyRange = 'A'..'D'
1417+
for (size in listOf(10, 100, 2000)) {
1418+
val array = Array(size) { index -> Sortable(keyRange.random(), index) }
1419+
1420+
array.sortedArray().assertStableSorted()
1421+
array.sortedArrayDescending().assertStableSorted(descending = true)
1422+
1423+
array.sort()
1424+
array.assertStableSorted()
1425+
array.sortDescending()
1426+
array.assertStableSorted(descending = true)
1427+
}
1428+
}
1429+
14151430
@Test fun sortByInPlace() {
14161431
val data = arrayOf("aa" to 20, "ab" to 3, "aa" to 3)
14171432
data.sortBy { it.second }
@@ -1431,6 +1446,22 @@ class ArraysTest {
14311446
assertEquals(listOf(1, 2, 0), indices.sortedBy { values[it] })
14321447
}
14331448

1449+
@Test fun sortByStable() {
1450+
val keyRange = 'A'..'D'
1451+
for (size in listOf(10, 100, 2000)) {
1452+
val array = Array(size) { index -> Sortable(keyRange.random(), index) }
1453+
1454+
array.sortedBy { it.key }.iterator().assertStableSorted()
1455+
array.sortedByDescending { it.key }.iterator().assertStableSorted(descending = true)
1456+
1457+
array.sortBy { it.key }
1458+
array.assertStableSorted()
1459+
1460+
array.sortByDescending { it.key }
1461+
array.assertStableSorted(descending = true)
1462+
}
1463+
}
1464+
14341465
@Test fun sortedNullableBy() {
14351466
fun String.nullIfEmpty() = if (this.isEmpty()) null else this
14361467
arrayOf(null, "").let {
@@ -1457,6 +1488,20 @@ class ArraysTest {
14571488
}
14581489
}
14591490

1491+
private data class Sortable<K : Comparable<K>>(val key: K, val index: Int) : Comparable<Sortable<K>> {
1492+
override fun compareTo(other: Sortable<K>): Int = this.key.compareTo(other.key)
1493+
}
1494+
1495+
private fun <K : Comparable<K>> Array<out Sortable<K>>.assertStableSorted(descending: Boolean = false) =
1496+
iterator().assertStableSorted(descending = descending)
1497+
1498+
private fun <K : Comparable<K>> Iterator<Sortable<K>>.assertStableSorted(descending: Boolean = false) {
1499+
assertSorted { a, b ->
1500+
val relation = a.key.compareTo(b.key)
1501+
(if (descending) relation > 0 else relation < 0) || relation == 0 && a.index < b.index
1502+
}
1503+
}
1504+
14601505
private class ArraySortedChecker<A, T>(val array: A, val comparator: Comparator<in T>) {
14611506
public fun <R> checkSorted(sorted: A.() -> R, sortedDescending: A.() -> R, iterator: R.() -> Iterator<T>) {
14621507
array.sorted().iterator().assertSorted { a, b -> comparator.compare(a, b) <= 0 }

libraries/tools/kotlin-stdlib-gen/src/templates/Arrays.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
2+
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
33
* that can be found in the license/LICENSE.txt file.
44
*/
55

@@ -884,10 +884,7 @@ object ArrayOps : TemplateGroupBase() {
884884
returns("Unit")
885885
on(Platform.JS) {
886886
body {
887-
"""
888-
if (size > 1)
889-
sort { a: T, b: T -> a.compareTo(b) }
890-
"""
887+
"""if (size > 1) sortArray(this)"""
891888
}
892889
specialFor(ArraysOfPrimitives) {
893890
if (primitive != PrimitiveType.Long) {
@@ -898,6 +895,10 @@ object ArrayOps : TemplateGroupBase() {
898895
on(Backend.IR) {
899896
body { "this.asDynamic().sort()" }
900897
}
898+
} else {
899+
body {
900+
"""if (size > 1) sort { a: T, b: T -> a.compareTo(b) }"""
901+
}
901902
}
902903
}
903904
}
@@ -939,26 +940,29 @@ object ArrayOps : TemplateGroupBase() {
939940
}
940941
on(Platform.JS) {
941942
body {
942-
"""
943-
if (size > 1)
944-
sort { a, b -> comparator.compare(a, b) }
945-
"""
943+
"""if (size > 1) sortArrayWith(this, comparator)"""
946944
}
947945
}
948946
on(Platform.Native) {
949947
body { """if (size > 1) kotlin.util.sortArrayWith(this, 0, size, comparator)""" }
950948
}
951949
}
952950

953-
val f_sort_comparison = fn("sort(noinline comparison: (a: T, b: T) -> Int)") {
951+
val f_sort_comparison = fn("sort(comparison: (a: T, b: T) -> Int)") {
954952
platforms(Platform.JS)
955953
include(ArraysOfObjects, ArraysOfPrimitives)
956954
exclude(PrimitiveType.Boolean)
957955
} builder {
958-
inlineOnly()
959956
returns("Unit")
960957
doc { "Sorts the array in-place according to the order specified by the given [comparison] function." }
961-
body { "asDynamic().sort(comparison)" }
958+
specialFor(ArraysOfPrimitives) {
959+
inlineOnly()
960+
signature("sort(noinline comparison: (a: T, b: T) -> Int)")
961+
body { "asDynamic().sort(comparison)" }
962+
}
963+
specialFor(ArraysOfObjects) {
964+
body { """if (size > 1) sortArrayWith(this, comparison)""" }
965+
}
962966
}
963967

964968
val f_sort_objects = fn("sort()") {

0 commit comments

Comments
 (0)