Skip to content

Commit

Permalink
Add Array.sort and Array.sort_by
Browse files Browse the repository at this point in the history
These methods are used to sort an Array, and perform a stable sort. The
current algorithm used is the recursive merge sort algorithm. While
faster algorithms exist, they are typically much more complex to
understand. In the future we may change the algorithm, if this is deemed
worth the effort.

Changelog: added
  • Loading branch information
yorickpeterse committed Jun 18, 2023
1 parent 99b22c5 commit 31a8a17
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 5 deletions.
122 changes: 117 additions & 5 deletions std/src/std/array.inko
Original file line number Diff line number Diff line change
@@ -1,13 +1,67 @@
# An ordered, integer-indexed generic collection of values.
import std::clone::Clone
import std::cmp::(Contains, Equal)
import std::cmp::(Compare, Contains, Equal, Ordering, min)
import std::drop::Drop
import std::fmt::(Format, Formatter)
import std::hash::(Hash, Hasher)
import std::iter::(Enum, Iter)
import std::option::Option
import std::rand::Shuffle

fn stable_sort[T: Compare[T]](
array: mut Array[T],
compare: mut fn (ref T, ref T) -> Bool,
) {
let len = array.length

# The algorithm here is the recursive merge sort algorithm. While faster
# algorithms exist (e.g. Timsort, at least in certain cases), merge sort is
# the easiest to implement and still offers good performance.
if len <= 1 { return }

# We don't set a length for `tmp` so we don't drop any of the temporary
# values in it, as that would result in `self` being left with invalid
# values. This works because merge() doesn't perform any bounds checking.
let tmp: Array[T] = Array.with_capacity(len)

len.times fn (i) { _INKO.array_set(tmp, i, _INKO.array_get(array, i)) }
merge_sort(tmp, array, start: 0, end: len, compare: compare)
}

fn merge_sort[T: Compare[T]](
a: mut Array[T],
b: mut Array[T],
start: Int,
end: Int,
compare: mut fn (ref T, ref T) -> Bool
) {
if end - start <= 1 { return }

# https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html
let mid = start.wrapping_add(end) >>> 1

merge_sort(b, a, start, mid, compare)
merge_sort(b, a, mid, end, compare)

let mut i = start
let mut j = mid
let mut k = start

while k < end {
if i < mid
and (j >= end or compare.call(a.get_unchecked(i), a.get_unchecked(j)))
{
_INKO.array_set(b, k, _INKO.array_get(a, i))
i += 1
} else {
_INKO.array_set(b, k, _INKO.array_get(a, j))
j += 1
}

k += 1
}
}

# Checks if `index` is in the range of zero up to (but excluding) `length`.
#
# # Panics
Expand Down Expand Up @@ -161,7 +215,7 @@ class builtin Array[T] {
fn pub opt(index: Int) -> Option[ref T] {
if index < 0 or index >= length { return Option.None }

Option.Some((ref _INKO.array_get(self, index)) as ref T)
Option.Some(get_unchecked(index))
}

# Returns an immutable reference to the value at the given index.
Expand All @@ -177,7 +231,7 @@ class builtin Array[T] {
# numbers.get(0) # => 10
fn pub get(index: Int) -> ref T {
bounds_check(index, length)
(ref _INKO.array_get(self, index)) as ref T
get_unchecked(index)
}

# Stores a value at the given index.
Expand Down Expand Up @@ -224,6 +278,20 @@ class builtin Array[T] {
result
}

# TODO: remove
# fn pub mut swap_at(index: Int, with: Int) {
# bounds_check(index, length)
# bounds_check(with, length)
#
# let index_val = _INKO.array_get(self, index)
# let with_val = _INKO.array_get(self, with)
#
# _INKO.array_set(self, index, with_val)
# _INKO.array_set(self, with, index_val)
# _INKO.moved(index_val)
# _INKO.moved(with_val)
# }

# Returns an iterator that yields immutable references to the values in
# `self`.
fn pub iter -> Iter[ref T] {
Expand Down Expand Up @@ -356,6 +424,10 @@ class builtin Array[T] {
b -= 1
}
}

fn get_unchecked(index: Int) -> ref T {
(ref _INKO.array_get(self, index)) as ref T
}
}

impl Array if T: mut {
Expand All @@ -377,7 +449,7 @@ impl Array if T: mut {
fn pub mut opt_mut(index: Int) -> Option[mut T] {
if index < 0 or index >= length { return Option.None }

Option.Some((mut _INKO.array_get(self, index)) as mut T)
Option.Some(get_unchecked_mut(index))
}

# Returns a mutable reference to the value at the given index.
Expand All @@ -393,13 +465,17 @@ impl Array if T: mut {
# numbers.get_mut(0) # => 10
fn pub mut get_mut(index: Int) -> mut T {
bounds_check(index, length)
(mut _INKO.array_get(self, index)) as mut T
get_unchecked_mut(index)
}

# Returns an iterator that yields mutable references to the values in `self`.
fn pub mut iter_mut -> Iter[mut T] {
Enum.indexed(length) fn (index) { get_mut(index) }
}

fn mut get_unchecked_mut(index: Int) -> mut T {
(mut _INKO.array_get(self, index)) as mut T
}
}

impl Drop for Array {
Expand Down Expand Up @@ -495,6 +571,42 @@ impl Format for Array if T: Format {
}
}

impl Array if T: Compare[T] {
# Sorts the values in `self` in ascending order.
#
# This method performs a stable sort, meaning it maintains the relative order
# of duplicate values.
#
# # Examples
#
# let nums = [0, 3, 3, 5, 9, 1]
#
# nums.sort
# nums # => [0, 1, 3, 3, 5, 9]
fn pub mut sort {
stable_sort(self) fn (a, b) { a <= b }
}

# Sorts the values in `self` using a custom comparison closure.
#
# Like `Array.sort`, this method performs a stable sort.
#
# # Examples
#
# let nums = [0, 3, 3, 5, 9, 1]
#
# nums.sort_by fn (a, b) { b.cmp(a) }
# nums # => [9, 5, 3, 3, 1, 0]
fn pub mut sort_by(block: fn (ref T, ref T) -> Ordering) {
stable_sort(self) fn (a, b) {
match block.call(a, b) {
case Less or Equal -> true
case _ -> false
}
}
}
}

# An iterator that moves values out of an `Array`.
#
# When this iterator is dropped, any values not yet moved out of the `Array` are
Expand Down
38 changes: 38 additions & 0 deletions std/test/std/test_array.inko
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import helpers::(fmt, hash)
import std::cmp::(Compare, Ordering)
import std::drop::(drop, Drop)
import std::rand::Random
import std::test::Tests

class Person {
let @name: String
let @age: Int
}

impl Compare[Person] for Person {
fn pub cmp(other: ref Person) -> Ordering {
@age.cmp(other.age)
}
}

class Counter {
let @value: Int

Expand Down Expand Up @@ -248,6 +260,32 @@ fn pub tests(t: mut Tests) {
t.equal(fmt([10, 20]), '[10, 20]')
}

t.test('Array.sort') fn (t) {
let nums = [56, 20, 28, 71, 42, 49, 1, 59, 19, 18, 27, 6, 31, 89, 32]
let people = [
Person { @name = 'Eve', @age = 22 },
Person { @name = 'Steve', @age = 22 },
Person { @name = 'Alice', @age = 20 },
Person { @name = 'Bob', @age = 21 },
]

nums.sort
people.sort

t.equal(nums, [1, 6, 18, 19, 20, 27, 28, 31, 32, 42, 49, 56, 59, 71, 89])
t.equal(
people.iter.map fn (p) { p.name }.to_array,
['Alice', 'Bob', 'Eve', 'Steve']
)
}

t.test('Array.sort') fn (t) {
let nums = [56, 20, 28, 71, 42, 49, 1, 59, 19, 18, 27, 6, 31, 89, 32]

nums.sort_by fn (a, b) { b.cmp(a) }
t.equal(nums, [89, 71, 59, 56, 49, 42, 32, 31, 28, 27, 20, 19, 18, 6, 1])
}

t.test('IntoIter.next') fn (t) {
let vals = [10, 20].into_iter

Expand Down

0 comments on commit 31a8a17

Please sign in to comment.