From ca0cbbaf3661e38037241cff3b6cab8467cbf0ed Mon Sep 17 00:00:00 2001 From: Sergey Kozlov Date: Sun, 4 Aug 2024 16:02:08 +0200 Subject: [PATCH] Slice --- .../algorithms/sort/FunctionalSort.scala | 20 ++- .../skozlov/algorithms/sort/InPlaceSort.scala | 4 +- .../algorithms/sort/InsertionSort.scala | 16 +-- .../skozlov/commons/collection/Slice.scala | 106 ++++++++++++++ .../commons/collection/WritableSlice.scala | 49 +++++++ .../skozlov/commons/collection/package.scala | 7 + .../com/github/skozlov/commons/package.scala | 5 + .../skozlov/{algorithms => }/Test.scala | 2 +- .../skozlov/algorithms/sort/SortTest.scala | 7 +- .../collection/WritableSliceTest.scala | 131 ++++++++++++++++++ 10 files changed, 319 insertions(+), 28 deletions(-) create mode 100644 src/main/scala/com/github/skozlov/commons/collection/Slice.scala create mode 100644 src/main/scala/com/github/skozlov/commons/collection/WritableSlice.scala create mode 100644 src/main/scala/com/github/skozlov/commons/collection/package.scala create mode 100644 src/main/scala/com/github/skozlov/commons/package.scala rename src/test/scala/com/github/skozlov/{algorithms => }/Test.scala (79%) create mode 100644 src/test/scala/com/github/skozlov/commons/collection/WritableSliceTest.scala diff --git a/src/main/scala/com/github/skozlov/algorithms/sort/FunctionalSort.scala b/src/main/scala/com/github/skozlov/algorithms/sort/FunctionalSort.scala index 7a3d18f..b5ea51c 100644 --- a/src/main/scala/com/github/skozlov/algorithms/sort/FunctionalSort.scala +++ b/src/main/scala/com/github/skozlov/algorithms/sort/FunctionalSort.scala @@ -1,6 +1,7 @@ package com.github.skozlov.algorithms.sort -import scala.collection.mutable +import com.github.skozlov.commons.collection.{Slice, WritableSlice} + import scala.reflect.ClassTag /** Sort which puts ordered elements into the output sequence and does not @@ -12,29 +13,24 @@ trait FunctionalSort { * sequence will not be modified. * * @param in - * input sequence which contains elements to sort + * input slice which contains elements to sort * @param out - * output sequence to put sorted elements into + * output slice to put sorted elements into * @throws IllegalArgumentException * if the input and output sequences have different sizes */ - def sortFunctionally[A: Ordering]( - in: collection.IndexedSeq[A], - out: mutable.IndexedSeq[A], - ): Unit + def sortFunctionally[A: Ordering](in: Slice[A], out: WritableSlice[A]): Unit /** Sorts the input sequence putting elements into the new array. Input * sequence will not be modified. * @param in - * input sequence which contains elements to sort + * input slice which contains elements to sort * @return * array which contains sorted elements */ - def sortFunctionally[A: Ordering: ClassTag]( - in: collection.IndexedSeq[A] - ): Array[A] = { + def sortFunctionally[A: Ordering: ClassTag](in: Slice[A]): Array[A] = { val out = Array.ofDim[A](in.size) - sortFunctionally(in, out) + sortFunctionally(in, WritableSlice(out)()) out } } diff --git a/src/main/scala/com/github/skozlov/algorithms/sort/InPlaceSort.scala b/src/main/scala/com/github/skozlov/algorithms/sort/InPlaceSort.scala index 65ad598..4f97fa3 100644 --- a/src/main/scala/com/github/skozlov/algorithms/sort/InPlaceSort.scala +++ b/src/main/scala/com/github/skozlov/algorithms/sort/InPlaceSort.scala @@ -1,7 +1,7 @@ package com.github.skozlov.algorithms.sort -import scala.collection.mutable +import com.github.skozlov.commons.collection.WritableSlice trait InPlaceSort { - def sortInPlace[A: Ordering](elements: mutable.IndexedSeq[A]): Unit + def sortInPlace[A: Ordering](elements: WritableSlice[A]): Unit } diff --git a/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala b/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala index 2354278..44f44e9 100644 --- a/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala +++ b/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala @@ -1,7 +1,8 @@ package com.github.skozlov.algorithms.sort +import com.github.skozlov.commons.collection.{Slice, WritableSlice} + import scala.annotation.tailrec -import scala.collection.mutable import scala.math.Ordered.orderingToOrdered /** Simple sorting algorithm that is efficient for small sequences. @@ -14,10 +15,7 @@ import scala.math.Ordered.orderingToOrdered * [[https://en.wikipedia.org/wiki/Insertion_sort]] */ object InsertionSort extends InPlaceSort with FunctionalSort with StableSort { - private def sort[A: Ordering]( - in: collection.IndexedSeq[A], - out: mutable.IndexedSeq[A], - ): Unit = { + private def sort[A: Ordering](in: Slice[A], out: WritableSlice[A]): Unit = { require( in.size == out.size, s"in and out have different sizes: ${in.size} and ${out.size}", @@ -63,15 +61,13 @@ object InsertionSort extends InPlaceSort with FunctionalSort with StableSort { } } - override def sortInPlace[A: Ordering]( - elements: mutable.IndexedSeq[A] - ): Unit = { + override def sortInPlace[A: Ordering](elements: WritableSlice[A]): Unit = { sort(in = elements, out = elements) } override def sortFunctionally[A: Ordering]( - in: collection.IndexedSeq[A], - out: mutable.IndexedSeq[A], + in: Slice[A], + out: WritableSlice[A], ): Unit = { sort(in, out) } diff --git a/src/main/scala/com/github/skozlov/commons/collection/Slice.scala b/src/main/scala/com/github/skozlov/commons/collection/Slice.scala new file mode 100644 index 0000000..1b24c83 --- /dev/null +++ b/src/main/scala/com/github/skozlov/commons/collection/Slice.scala @@ -0,0 +1,106 @@ +package com.github.skozlov.commons.collection + +import com.github.skozlov.commons.collection.SliceOps.checkBounds + +import scala.collection.IndexedSeqView +import scala.collection.IndexedSeqView.SomeIndexedSeqOps + +trait SliceOps[ + A, + Underlying <: SomeIndexedSeqOps[A], + +SubSlice <: SliceOps[A, Underlying, SubSlice], +] extends IndexedSeqView[A] { + this: SubSlice => + + import SliceOps._ + + def underlying: Underlying + def from: Int + def until: Int + + override val length: Int = until - from + + // noinspection ScalaWeakerAccess + @throws[IndexOutOfBoundsException] + def indexToUnderlying(i: Int): Int = { + if (i < 0 || i >= length) { + throw IndexOutOfBoundsException( + s"$i is out of bounds (min 0, max ${length - 1})" + ) + } + from + i + } + + override def apply(i: Int): A = underlying(indexToUnderlying(i)) + + protected def newSubSlice( + underlying: Underlying, + from: Int, + until: Int, + ): SubSlice + + override def slice(from: Int, until: Int): SubSlice = { + if (from == 0 && until == this.length) { + this + } else { + checkBounds(this, from, until) + newSubSlice(underlying, this.from + from, this.from + until) + } + } + + override def take(n: Int): SubSlice = { + slice(0, math.min(math.max(n, 0), length)) + } + + override def drop(n: Int): SubSlice = { + slice(math.min(math.max(n, 0), length), length) + } + + override def takeRight(n: Int): SubSlice = drop(length - math.max(n, 0)) + + override def dropRight(n: Int): SubSlice = take(length - math.max(n, 0)) +} + +object SliceOps { + // noinspection ScalaWeakerAccess + @throws[IllegalArgumentException] + def checkBounds[A]( + underlying: SomeIndexedSeqOps[A], + from: Int, + until: Int, + ): Unit = { + require(from >= 0, s"Negative `from`: $from") + require(from <= until, s"`from` ($from) is greater than `until` ($until)") + require( + until <= underlying.length, + s"`until` ($until) is greater than underlying length (${underlying.length})", + ) + } +} + +type Slice[A] = SliceOps[A, _ <: SomeIndexedSeqOps[A], _ <: SliceOps[A, _, _]] + +object Slice { + class Impl[A]( + override val underlying: SomeIndexedSeqOps[A], + override val from: Int, + override val until: Int, + ) extends SliceOps[A, SomeIndexedSeqOps[A], Impl[A]] { + + checkBounds(underlying, from, until) + + override protected def newSubSlice( + underlying: SomeIndexedSeqOps[A], + from: Int, + until: Int, + ): Impl[A] = { + Impl(underlying, from, until) + } + } + + def apply[A]( + underlying: SomeIndexedSeqOps[A] + )(from: Int = 0, until: Int = underlying.length): Slice[A] = { + Impl(underlying, from, until) + } +} diff --git a/src/main/scala/com/github/skozlov/commons/collection/WritableSlice.scala b/src/main/scala/com/github/skozlov/commons/collection/WritableSlice.scala new file mode 100644 index 0000000..ebd754c --- /dev/null +++ b/src/main/scala/com/github/skozlov/commons/collection/WritableSlice.scala @@ -0,0 +1,49 @@ +package com.github.skozlov.commons.collection + +import com.github.skozlov.commons.collection.SliceOps.checkBounds + +import scala.collection.{View, mutable} + +trait WritableSliceOps[ + A, + Underlying <: SomeMutableIndexedSeqOps[A], + +SubSlice <: WritableSliceOps[A, Underlying, SubSlice], +] extends SliceOps[A, Underlying, SubSlice] + with mutable.IndexedSeqOps[A, View, View[A]] { + this: SubSlice => + + override def update(idx: Int, elem: A): Unit = { + underlying(indexToUnderlying(idx)) = elem + } +} + +type WritableSlice[A] = WritableSliceOps[ + A, + _ <: SomeMutableIndexedSeqOps[A], + _ <: WritableSliceOps[A, _, _], +] + +object WritableSlice { + class Impl[A]( + override val underlying: SomeMutableIndexedSeqOps[A], + override val from: Int, + override val until: Int, + ) extends WritableSliceOps[A, SomeMutableIndexedSeqOps[A], Impl[A]] { + + checkBounds(underlying, from, until) + + override protected def newSubSlice( + underlying: SomeMutableIndexedSeqOps[A], + from: Int, + until: Int, + ): Impl[A] = { + Impl(underlying, from, until) + } + } + + def apply[A]( + underlying: SomeMutableIndexedSeqOps[A] + )(from: Int = 0, until: Int = underlying.length): WritableSlice[A] = { + Impl(underlying, from, until) + } +} diff --git a/src/main/scala/com/github/skozlov/commons/collection/package.scala b/src/main/scala/com/github/skozlov/commons/collection/package.scala new file mode 100644 index 0000000..ed71995 --- /dev/null +++ b/src/main/scala/com/github/skozlov/commons/collection/package.scala @@ -0,0 +1,7 @@ +package com.github.skozlov.commons + +import scala.collection.mutable + +package object collection { + type SomeMutableIndexedSeqOps[A] = mutable.IndexedSeqOps[A, AnyConstr, _] +} diff --git a/src/main/scala/com/github/skozlov/commons/package.scala b/src/main/scala/com/github/skozlov/commons/package.scala new file mode 100644 index 0000000..34c3ee3 --- /dev/null +++ b/src/main/scala/com/github/skozlov/commons/package.scala @@ -0,0 +1,5 @@ +package com.github.skozlov + +package object commons { + type AnyConstr[X] = Any +} diff --git a/src/test/scala/com/github/skozlov/algorithms/Test.scala b/src/test/scala/com/github/skozlov/Test.scala similarity index 79% rename from src/test/scala/com/github/skozlov/algorithms/Test.scala rename to src/test/scala/com/github/skozlov/Test.scala index 7f46548..d0b2969 100644 --- a/src/test/scala/com/github/skozlov/algorithms/Test.scala +++ b/src/test/scala/com/github/skozlov/Test.scala @@ -1,4 +1,4 @@ -package com.github.skozlov.algorithms +package com.github.skozlov import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala b/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala index ae58146..3803660 100644 --- a/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala +++ b/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala @@ -1,6 +1,7 @@ package com.github.skozlov.algorithms.sort -import com.github.skozlov.algorithms.Test +import com.github.skozlov.Test +import com.github.skozlov.commons.collection.{Slice, WritableSlice} class SortTest extends Test { private val cases: Seq[Seq[(Int, Int)]] = { @@ -42,7 +43,7 @@ class SortTest extends Test { private def testInPlaceSort(sort: InPlaceSort): Unit = { val arrays: Seq[Array[(Int, Int)]] = cases map { _.toArray } val results: Seq[Seq[(Int, Int)]] = for (array <- arrays) yield { - sort.sortInPlace(array) + sort.sortInPlace(WritableSlice(array)()) array.toSeq } checkResults(results, sort.isInstanceOf[StableSort]) @@ -53,7 +54,7 @@ class SortTest extends Test { for { _case: Seq[(Int, Int)] <- cases input: Array[(Int, Int)] = _case.toArray - output: Array[(Int, Int)] = sort.sortFunctionally(input) + output: Array[(Int, Int)] = sort.sortFunctionally(Slice(input)()) } yield (input, output) checkResults( inputsAndOutputsAfterSort map { _._2.toSeq }, diff --git a/src/test/scala/com/github/skozlov/commons/collection/WritableSliceTest.scala b/src/test/scala/com/github/skozlov/commons/collection/WritableSliceTest.scala new file mode 100644 index 0000000..5fe8c4e --- /dev/null +++ b/src/test/scala/com/github/skozlov/commons/collection/WritableSliceTest.scala @@ -0,0 +1,131 @@ +package com.github.skozlov.commons.collection + +import com.github.skozlov.Test + +class WritableSliceTest extends Test { + test("constructor") { + val underlying = Array(1, 2, 3, 4, 5) + WritableSlice(underlying)(from = 1, until = 4).toSeq shouldBe Seq(2, 3, 4) + WritableSlice(underlying)(until = 2).toSeq shouldBe Seq(1, 2) + WritableSlice(underlying)(from = 3).toSeq shouldBe Seq(4, 5) + WritableSlice(underlying)(from = 1, until = 1).toSeq shouldBe Seq() + the[IllegalArgumentException] thrownBy WritableSlice(underlying)( + from = -1, + until = 4, + ) should have message "requirement failed: Negative `from`: -1" + the[IllegalArgumentException] thrownBy WritableSlice(underlying)( + from = 2, + until = 1, + ) should have message "requirement failed: `from` (2) is greater than `until` (1)" + the[IllegalArgumentException] thrownBy WritableSlice(underlying)( + from = 3, + until = 6, + ) should have message "requirement failed: `until` (6) is greater than underlying length (5)" + } + + test("changing underlying") { + val underlying = Array(1, 2, 3, 4, 5) + val slice = WritableSlice(underlying)(from = 1, until = 4) + slice.toSeq shouldBe Seq(2, 3, 4) + for (i <- underlying.indices) { + underlying(i) = underlying(i) * 10 + } + slice.toSeq shouldBe Seq(20, 30, 40) + } + + test("length") { + val underlying = Array(1, 2, 3, 4, 5) + WritableSlice(underlying)(from = 1, until = 4).length shouldBe 3 + WritableSlice(underlying)(from = 1, until = 2).length shouldBe 1 + WritableSlice(underlying)(from = 1, until = 1).length shouldBe 0 + } + + test("apply(i: Int)") { + val slice = + WritableSlice(underlying = Array(1, 2, 3, 4))(from = 1, until = 3) + slice(0) shouldBe 2 + slice(1) shouldBe 3 + the[IndexOutOfBoundsException] thrownBy slice( + 2 + ) should have message "2 is out of bounds (min 0, max 1)" + the[IndexOutOfBoundsException] thrownBy slice( + -1 + ) should have message "-1 is out of bounds (min 0, max 1)" + } + + test("update(idx: Int, elem: A)") { + val underlying = Array(1, 2, 3, 4) + val slice = WritableSlice(underlying)(from = 1, until = 3) + slice(0) = 20 + slice(1) = 30 + the[IndexOutOfBoundsException] thrownBy { + slice(2) = 40 + } should have message "2 is out of bounds (min 0, max 1)" + the[IndexOutOfBoundsException] thrownBy { + slice(-1) = 10 + } should have message "-1 is out of bounds (min 0, max 1)" + underlying.toSeq shouldBe Seq(1, 20, 30, 4) + } + + test("slice(from: Int, until: Int)") { + val underlying = Array(1, 2, 3, 4, 5, 6, 7) + val slice = WritableSlice(underlying)(from = 1, until = 6) + slice.slice(from = 1, until = 4).toSeq shouldBe Seq(3, 4, 5) + slice.slice(from = 0, until = 5).toSeq shouldBe Seq(2, 3, 4, 5, 6) + slice.slice(from = 1, until = 1).toSeq shouldBe Seq() + val subSlice: WritableSlice[Int] = + slice.slice(from = 0, until = 0) // checking sub-slice type + the[IllegalArgumentException] thrownBy slice.slice( + from = -1, + until = 5, + ) should have message "requirement failed: Negative `from`: -1" + the[IllegalArgumentException] thrownBy slice.slice( + from = 1, + until = 0, + ) should have message "requirement failed: `from` (1) is greater than `until` (0)" + the[IllegalArgumentException] thrownBy slice.slice( + from = 0, + until = 6, + ) should have message "requirement failed: `until` (6) is greater than underlying length (5)" + } + + test("take(n: Int)") { + val slice = + WritableSlice(underlying = Array(1, 2, 3, 4))(from = 1, until = 3) + (slice take 0).toSeq shouldBe Seq() + (slice take -1).toSeq shouldBe Seq() + (slice take 1).toSeq shouldBe Seq(2) + (slice take 2).toSeq shouldBe Seq(2, 3) + (slice take 3).toSeq shouldBe Seq(2, 3) + } + + test("drop(n: Int)") { + val slice = + WritableSlice(underlying = Array(1, 2, 3, 4))(from = 1, until = 3) + (slice drop 0).toSeq shouldBe Seq(2, 3) + (slice drop -1).toSeq shouldBe Seq(2, 3) + (slice drop 1).toSeq shouldBe Seq(3) + (slice drop 2).toSeq shouldBe Seq() + (slice drop 3).toSeq shouldBe Seq() + } + + test("takeRight(n: Int)") { + val slice = + WritableSlice(underlying = Array(1, 2, 3, 4))(from = 1, until = 3) + (slice takeRight 0).toSeq shouldBe Seq() + (slice takeRight -1).toSeq shouldBe Seq() + (slice takeRight 1).toSeq shouldBe Seq(3) + (slice takeRight 2).toSeq shouldBe Seq(2, 3) + (slice takeRight 3).toSeq shouldBe Seq(2, 3) + } + + test("dropRight(n: Int)") { + val slice = + WritableSlice(underlying = Array(1, 2, 3, 4))(from = 1, until = 3) + (slice dropRight 0).toSeq shouldBe Seq(2, 3) + (slice dropRight -1).toSeq shouldBe Seq(2, 3) + (slice dropRight 1).toSeq shouldBe Seq(2) + (slice dropRight 2).toSeq shouldBe Seq() + (slice dropRight 3).toSeq shouldBe Seq() + } +}