Skip to content

Commit a5afe3d

Browse files
Restore stability for sorting, add tests (#197)
* Add simple stability test for sortWith * Use larger input array for sortWith stability test * Stabilize mergesort * Add ST tests, simplify sortByImpl fix * Add note about stability to documentation * Fix compiler warning: replace `STA.empty` with `STA.new` * Remove fold1 member in NonEmptyArray's Foldable1 instance * Update foldMap1Default with left version Co-authored-by: Jordan Martinez <[email protected]>
1 parent 370b8f5 commit a5afe3d

File tree

7 files changed

+37
-20
lines changed

7 files changed

+37
-20
lines changed

src/Data/Array.js

-6
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,6 @@ exports.sortByImpl = (function () {
295295
xs1[k++] = y;
296296
++j;
297297
}
298-
else if (c === 0) {
299-
xs1[k++] = x;
300-
xs1[k++] = y;
301-
++i;
302-
++j;
303-
}
304298
else {
305299
xs1[k++] = x;
306300
++i;

src/Data/Array.purs

+5-1
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ foreign import scanr :: forall a b. (a -> b -> b) -> b -> Array a -> Array b
790790
--------------------------------------------------------------------------------
791791

792792
-- | Sort the elements of an array in increasing order, creating a new array.
793+
-- | Sorting is stable: the order of equal elements is preserved.
793794
-- |
794795
-- | ```purescript
795796
-- | sort [2, -3, 1] = [-3, 1, 2]
@@ -800,6 +801,8 @@ sort xs = sortBy compare xs
800801

801802
-- | Sort the elements of an array in increasing order, where elements are
802803
-- | compared using the specified partial ordering, creating a new array.
804+
-- | Sorting is stable: the order of elements is preserved if they are equal
805+
-- | according to the specified partial ordering.
803806
-- |
804807
-- | ```purescript
805808
-- | compareLength a b = compare (length a) (length b)
@@ -813,7 +816,8 @@ sortBy comp = sortByImpl comp case _ of
813816
LT -> -1
814817

815818
-- | Sort the elements of an array in increasing order, where elements are
816-
-- | sorted based on a projection
819+
-- | sorted based on a projection. Sorting is stable: the order of elements is
820+
-- | preserved if they are equal according to the projection.
817821
-- |
818822
-- | ```purescript
819823
-- | sortWith (_.age) [{name: "Alice", age: 42}, {name: "Bob", age: 21}]

src/Data/Array/NonEmpty/Internal.purs

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Data.Foldable (class Foldable)
1515
import Data.FoldableWithIndex (class FoldableWithIndex)
1616
import Data.FunctorWithIndex (class FunctorWithIndex)
1717
import Data.Ord (class Ord1)
18-
import Data.Semigroup.Foldable (class Foldable1, foldMap1Default)
18+
import Data.Semigroup.Foldable (class Foldable1, foldMap1DefaultL)
1919
import Data.Semigroup.Traversable (class Traversable1, sequence1Default)
2020
import Data.Traversable (class Traversable)
2121
import Data.TraversableWithIndex (class TraversableWithIndex)
@@ -48,8 +48,7 @@ derive newtype instance foldableNonEmptyArray :: Foldable NonEmptyArray
4848
derive newtype instance foldableWithIndexNonEmptyArray :: FoldableWithIndex Int NonEmptyArray
4949

5050
instance foldable1NonEmptyArray :: Foldable1 NonEmptyArray where
51-
foldMap1 = foldMap1Default
52-
fold1 = foldl1Impl (<>)
51+
foldMap1 = foldMap1DefaultL
5352
foldr1 = foldr1Impl
5453
foldl1 = foldl1Impl
5554

src/Data/Array/ST.js

-6
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,6 @@ exports.sortByImpl = (function () {
123123
xs1[k++] = y;
124124
++j;
125125
}
126-
else if (c === 0) {
127-
xs1[k++] = x;
128-
xs1[k++] = y;
129-
++i;
130-
++j;
131-
}
132126
else {
133127
xs1[k++] = x;
134128
++i;

src/Data/Array/ST.purs

+7-3
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ empty = new
8686
-- | Create a mutable copy of an immutable array.
8787
foreign import thaw :: forall h a. Array a -> ST h (STArray h a)
8888

89-
-- | Sort a mutable array in place.
89+
-- | Sort a mutable array in place. Sorting is stable: the order of equal
90+
-- | elements is preserved.
9091
sort :: forall a h. Ord a => STArray h a -> ST h (STArray h a)
9192
sort = sortBy compare
9293

@@ -101,7 +102,9 @@ foreign import shiftImpl
101102
-> STArray h a
102103
-> ST h (Maybe a)
103104

104-
-- | Sort a mutable array in place using a comparison function.
105+
-- | Sort a mutable array in place using a comparison function. Sorting is
106+
-- | stable: the order of elements is preserved if they are equal according to
107+
-- | the comparison function.
105108
sortBy
106109
:: forall a h
107110
. (a -> a -> Ordering)
@@ -119,7 +122,8 @@ foreign import sortByImpl
119122
-> STArray h a
120123
-> ST h (STArray h a)
121124

122-
-- | Sort a mutable array in place based on a projection.
125+
-- | Sort a mutable array in place based on a projection. Sorting is stable: the
126+
-- | order of elements is preserved if they are equal according to the projection.
123127
sortWith
124128
:: forall a b h
125129
. Ord b

test/Test/Data/Array.purs

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Data.Foldable (for_, foldMapDefaultR, class Foldable, all, traverse_)
1010
import Data.Traversable (scanl, scanr)
1111
import Data.Maybe (Maybe(..), isNothing, fromJust)
1212
import Data.Ord.Down (Down(..))
13-
import Data.Tuple (Tuple(..))
13+
import Data.Tuple (Tuple(..), fst)
1414
import Data.Unfoldable (replicateA)
1515
import Effect (Effect)
1616
import Effect.Console (log)
@@ -288,9 +288,17 @@ testArray = do
288288
log "sortBy should reorder a list into ascending order based on the result of a comparison function"
289289
assert $ A.sortBy (flip compare) [1, 3, 2, 5, 6, 4] == [6, 5, 4, 3, 2, 1]
290290

291+
log "sortBy should not reorder elements that are equal according to a comparison function"
292+
let s1 = map (Tuple "a") (A.range 1 100)
293+
assert $ A.sortBy (comparing fst) s1 == s1
294+
291295
log "sortWith should reorder a list into ascending order based on the result of compare over a projection"
292296
assert $ A.sortWith identity [1, 3, 2, 5, 6, 4] == [1, 2, 3, 4, 5, 6]
293297

298+
log "sortWith should not reorder elements that are equal according to a projection"
299+
let s2 = map (Tuple "a") (A.range 1 100)
300+
assert $ A.sortWith fst s2 == s2
301+
294302
log "take should keep the specified number of items from the front of an array, discarding the rest"
295303
assert $ (A.take 1 [1, 2, 3]) == [1]
296304
assert $ (A.take 2 [1, 2, 3]) == [1, 2]

test/Test/Data/Array/ST.purs

+14
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ module Test.Data.Array.ST (testArrayST) where
33
import Prelude
44

55
import Control.Monad.ST as ST
6+
import Data.Array (range)
67
import Data.Array.ST (withArray)
78
import Data.Array.ST as STA
89
import Data.Foldable (all)
910
import Data.Maybe (Maybe(..), isNothing)
11+
import Data.Tuple (Tuple(..), fst)
1012
import Effect (Effect)
1113
import Effect.Console (log)
1214
import Test.Assert (assert)
@@ -234,11 +236,23 @@ testArrayST = do
234236
STA.sortBy (flip compare) =<< STA.unsafeThaw [1, 3, 2, 5, 6, 4]
235237
) == [6, 5, 4, 3, 2, 1]
236238

239+
log "sortBy should not reorder elements that are equal according to a comparison function"
240+
let s1 = map (Tuple "a") (range 1 100)
241+
assert $ STA.run (
242+
STA.sortBy (comparing fst) =<< STA.unsafeThaw s1
243+
) == s1
244+
237245
log "sortWith should reorder a list into ascending order based on the result of compare over a projection"
238246
assert $ STA.run (
239247
STA.sortWith identity =<< STA.unsafeThaw [1, 3, 2, 5, 6, 4]
240248
) == [1, 2, 3, 4, 5, 6]
241249

250+
log "sortWith should not reorder elements that are equal according to a projection"
251+
let s2 = map (Tuple "a") (range 1 100)
252+
assert $ STA.run (
253+
STA.sortWith fst =<< STA.unsafeThaw s2
254+
) == s2
255+
242256
log "splice should be able to delete multiple items at a specified index"
243257

244258
assert $ STA.run (do

0 commit comments

Comments
 (0)