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..f542c82 --- /dev/null +++ b/src/main/scala/com/github/skozlov/commons/collection/Slice.scala @@ -0,0 +1,53 @@ +package com.github.skozlov.commons.collection + +import scala.collection.IndexedSeqView.SomeIndexedSeqOps +import scala.collection.{IndexedSeqView, SeqOps} + +class Slice[+A](val underlying: SomeIndexedSeqOps[A], val from: Int, val until: Int) extends IndexedSeqView[A] { + import Slice.* + + checkBounds(underlying, from, until) + + override val length: Int = until - from + + @throws[IndexOutOfBoundsException] + protected 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)) + + override def slice(from: Int, until: Int): Slice[A] = { + if (from == 0 && until == this.length) { + this + } + else { + checkBounds(this, from, until) + Slice(underlying, this.from + from, this.from + until) + } + } + + override def take(n: Int): Slice[A] = slice(0, math.min(math.max(n, 0), length)) + + override def drop(n: Int): Slice[A] = slice(math.min(math.max(n, 0), length), length) + + override def takeRight(n: Int): Slice[A] = drop(length - math.max(n, 0)) + + override def dropRight(n: Int): Slice[A] = take(length - math.max(n, 0)) +} + +object Slice { + @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 AnyConstr[X] = Any + + type SomeSeqOps[+A] = SeqOps[A, AnyConstr, _] +} 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..4c0f1c1 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,6 @@ package com.github.skozlov.algorithms.sort -import com.github.skozlov.algorithms.Test +import com.github.skozlov.Test class SortTest extends Test { private val cases: Seq[Seq[(Int, Int)]] = { diff --git a/src/test/scala/com/github/skozlov/commons/collection/SliceTest.scala b/src/test/scala/com/github/skozlov/commons/collection/SliceTest.scala new file mode 100644 index 0000000..3f6bdb2 --- /dev/null +++ b/src/test/scala/com/github/skozlov/commons/collection/SliceTest.scala @@ -0,0 +1,88 @@ +package com.github.skozlov.commons.collection + +import com.github.skozlov.Test + +class SliceTest extends Test { + test("constructor") { + val underlying = Array(1, 2, 3, 4, 5) + Slice(underlying, from = 1, until = 4).toSeq shouldBe Seq(2, 3, 4) + Slice(underlying, from = 0, until = 2).toSeq shouldBe Seq(1, 2) + Slice(underlying, from = 3, until = 5).toSeq shouldBe Seq(4, 5) + Slice(underlying, from = 1, until = 1).toSeq shouldBe Seq() + the[IllegalArgumentException] thrownBy Slice(underlying, from = -1, until = 4) should have message "requirement failed: Negative `from`: -1" + the[IllegalArgumentException] thrownBy Slice(underlying, from = 2, until = 1) should have message "requirement failed: `from` (2) is greater than `until` (1)" + the[IllegalArgumentException] thrownBy Slice(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 = Slice(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) + Slice(underlying, from = 1, until = 4).length shouldBe 3 + Slice(underlying, from = 1, until = 2).length shouldBe 1 + Slice(underlying, from = 1, until = 1).length shouldBe 0 + } + + test("apply(i: Int)"){ + val slice = Slice(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("slice(from: Int, until: Int)") { + val underlying = Array(1, 2, 3, 4, 5, 6, 7) + val slice = Slice(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() + 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 = Slice(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 = Slice(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 = Slice(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 = Slice(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() + } +}