diff --git a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala index caba75d..632e6de 100644 --- a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala +++ b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala @@ -3,8 +3,39 @@ package decorators import scala.annotation.tailrec +/** Enriches Iterator with additional methods. + * + * @define mayNotTerminateInf + * Note: may not terminate for infinite iterators. + * @define consumesIterator + * After calling this method, one should discard the iterator it was called + * on. Using it is undefined and subject to change. + * @define consumesAndProducesIterator + * After calling this method, one should discard the iterator it was called + * on, and use only the iterator that was returned. Using the old iterator + * is undefined, subject to change, and may result in changes to the new + * iterator as well. + * @define pseudoCodeExample + * The `===` operator in this pseudo code stands for 'is equivalent to'; + * both sides of the `===` give the same result. + */ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { + /** + * Inserts a separator value between each element. + * + * {{{ + * Iterator(1, 2, 3).intersperse(0) === Iterator(1, 0, 2, 0, 3) + * Iterator('a', 'b', 'c').intersperse(',') === Iterator('a', ',', 'b', ',', 'c') + * Iterator('a').intersperse(',') === Iterator('a') + * Iterator().intersperse(',') === Iterator() + * }}} + * $pseudoCodeExample + * + * @param sep the separator value. + * @return The resulting iterator contains all elements from the source iterator, separated by the `sep` value. + * @note Reuse: $consumesAndProducesIterator + */ def intersperse[B >: A](sep: B): Iterator[B] = new Iterator[B] { var intersperseNext = false override def hasNext = intersperseNext || `this`.hasNext @@ -15,6 +46,27 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { } } + /** + * Inserts a start value at the start of the iterator, a separator value between each element, and + * an end value at the end of the iterator. + * + * {{{ + * Iterator(1, 2, 3).intersperse(-1, 0, 99) === Iterator(-1, 1, 0, 2, 0, 3, 99) + * Iterator('a', 'b', 'c').intersperse('[', ',', ']') === Iterator('[', 'a', ',', 'b', ',', 'c', ']') + * Iterator('a').intersperse('[', ',', ']') === Iterator('[', 'a', ']') + * Iterator().intersperse('[', ',', ']') === Iterator('[', ']') + * }}} + * $pseudoCodeExample + * + * @param start the starting value. + * @param sep the separator value. + * @param end the ending value. + * @return The resulting iterator + * begins with the `start` value and ends with the `end` value. + * Inside, are all elements from the source iterator separated by + * the `sep` value. + * @note Reuse: $consumesAndProducesIterator + */ def intersperse[B >: A](start: B, sep: B, end: B): Iterator[B] = new Iterator[B] { var started = false var finished = false @@ -41,6 +93,27 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { } } + /** + * Folds elements with combination function `op` until + * all elements have been processed, or `op` returns `None`. + * $mayNotTerminateInf + * + * {{{ + * def sumOp(acc: Int, e: Int): Option[Int] = if (e == 4) None else Some(acc + e) + * Iterator.empty.foldSomeLeft(0)(sumOp) === 0 + * Iterator(1, 2, 3).foldSomeLeft(0)(sumOp) === 6 + * Iterator(1, 2, 3, 4, 5).foldSomeLeft(0)(sumOp) === 6 + * }}} + * $pseudoCodeExample + * + * @param z the start value + * @param op the binary operator + * @tparam B the result type of the binary operator + * @return the result of evaluating `op` on the previous result of `op` (or `z` for the first time) and + * elements of the source iterator, stopping when all the elements have been + * iterated or earlier when `op` returns `None` + * @note Reuse: $consumesIterator + */ def foldSomeLeft[B](z: B)(op: (B, A) => Option[B]): B = { var result: B = z while (`this`.hasNext) { @@ -52,6 +125,10 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { result } + /** + * $mayNotTerminateInf + * @note Reuse: $consumesIterator + */ def lazyFoldLeft[B](z: B)(op: (B, => A) => B): B = { var result = z var finished = false @@ -66,6 +143,10 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { result } + /** + * $mayNotTerminateInf + * @note Reuse: $consumesIterator + */ def lazyFoldRight[B](z: B)(op: A => Either[B, B => B]): B = { def chainEval(x: B, fs: immutable.List[B => B]): B = @@ -87,26 +168,20 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { } /** - * Constructs an iterator where consecutive elements are accumulated as - * long as the output of f for each element doesn't change. - *
- * Vector(1,2,2,3,3,3,2,2) - * .iterator - * .splitBy(identity) - * .toList - *- * produces - *
- * List(Seq(1), - * Seq(2,2), - * Seq(3,3,3), - * Seq(2,2)) - *+ * Constructs an iterator in which each element is a the sequence of accumulated elements + * from the source iterator that have the same key, where the key is calculated by `f`. + * + * {{{ + * Iterator(1,2,2,3,3,3,2,2).splitBy(identity) === Iterator(Seq(1), Seq(2,2), Seq(3,3,3), Seq(2,2)) + * Iterator((1,1), (1,2), (2, 3)).splitBy(_._1) === Iterator(Seq((1,1), (1,2)), Seq((2,3))) + * }}} + * $pseudoCodeExample * * @param f the function to compute a key for an element * @tparam K the type of the computed key * @return an iterator of sequences of the consecutive elements with the * same key in the original iterator + * @note Reuse: $consumesIterator */ def splitBy[K](f: A => K): Iterator[immutable.Seq[A]] = new AbstractIterator[immutable.Seq[A]] { diff --git a/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala b/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala index 08d93fc..b435004 100644 --- a/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala +++ b/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala @@ -6,11 +6,81 @@ import org.junit.{Assert, Test} import scala.util.Try class IteratorDecoratorTest { + @Test + def intersperseShouldIntersperseASeparator(): Unit = { + Assert.assertEquals(Seq(1, 0, 2, 0, 3), Iterator(1, 2, 3).intersperse(0).toSeq) + Assert.assertEquals(Seq('a', ',', 'b', ',', 'c'), Iterator('a', 'b', 'c').intersperse(',').toSeq) + Assert.assertEquals(Seq('a'), Iterator('a').intersperse(',').toSeq) + Assert.assertEquals(Seq.empty, Iterator().intersperse(',').toSeq) + } + + @Test + def intersperseShouldIntersperseASeparatorAndInsertStartAndEnd(): Unit = { + Assert.assertEquals(Seq(-1, 1, 0, 2, 0, 3, 99), Iterator(1, 2, 3).intersperse(-1, 0, 99).toSeq) + Assert.assertEquals(Seq('[', 'a', ',', 'b', ',', 'c', ']'), + Iterator('a', 'b', 'c').intersperse('[', ',', ']').toSeq) + Assert.assertEquals(Seq('[', 'a', ']'), Iterator('a').intersperse('[', ',', ']').toSeq) + Assert.assertEquals(Seq('[', ']'), Iterator().intersperse('[', ',', ']').toSeq) + } + + @Test + def foldSomeLeftShouldFold(): Unit = { + def sumOp(acc: Int, e: Int): Option[Int] = if (e == 4) None else Some(acc + e) + Assert.assertEquals(0, Iterator().foldSomeLeft(0)(sumOp)) + Assert.assertEquals(6, Iterator(1, 2, 3).foldSomeLeft(0)(sumOp)) + Assert.assertEquals(6, Iterator(1, 2, 3, 4, 5).foldSomeLeft(0)(sumOp)) + Assert.assertEquals(0, Iterator(4, 5).foldSomeLeft(0)(sumOp)) + } + + @Test + def lazyFoldLeftShouldFold(): Unit = { + // Notice how sumOp doesn't evaluate `e` under some conditions. + def sumOp(acc: Int, e: => Int): Int = if (acc >= 5) acc else acc + e + Assert.assertEquals(0, Iterator().lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(3, Iterator(1, 1, 1).lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(6, Iterator(1, 2, 3, 4, 5).lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(5, Iterator(1, 1, 1, 1, 1, 1, 1, 1).lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(9, Iterator(4, 5).lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(9, Iterator(4, 5, 1).lazyFoldLeft(0)(sumOp)) + Assert.assertEquals(10, Iterator(10, 20, 30).lazyFoldLeft(0)(sumOp)) + } + + @Test + def lazyFoldLeftShouldFoldWeirdEdgeCases(): Unit = { + // `delayedSumOp` doesn't return `acc`, causing a delayed stop of the iteration. + def delayedSumOp(acc: Int, e: => Int): Int = if (acc >= 5) 5 else acc + e + Assert.assertEquals(0, Iterator().lazyFoldLeft(0)(delayedSumOp)) + Assert.assertEquals(3, Iterator(1, 1, 1).lazyFoldLeft(0)(delayedSumOp)) + Assert.assertEquals(9, Iterator(4, 5).lazyFoldLeft(0)(delayedSumOp)) + Assert.assertEquals(5, Iterator(4, 5, 1).lazyFoldLeft(0)(delayedSumOp)) + Assert.assertEquals(5, Iterator(6, 1).lazyFoldLeft(0)(delayedSumOp)) + + // `alwaysGrowingSumOp` returns a new value every time, causing no stop in the iteration. + def alwaysGrowingSumOp(acc: Int, e: => Int): Int = if (acc >= 5) acc + 1 else acc + e + Assert.assertEquals(0, Iterator().lazyFoldLeft(0)(alwaysGrowingSumOp)) + Assert.assertEquals(3, Iterator(1, 1, 1).lazyFoldLeft(0)(alwaysGrowingSumOp)) + Assert.assertEquals(9, Iterator(5, 10, 10, 10, 10).lazyFoldLeft(0)(alwaysGrowingSumOp)) + Assert.assertEquals(9, Iterator(4, 5).lazyFoldLeft(0)(alwaysGrowingSumOp)) + Assert.assertEquals(10, Iterator(4, 5, 20).lazyFoldLeft(0)(alwaysGrowingSumOp)) + } + + @Test + def lazyFoldRightShouldFold(): Unit = { + def sumOp(acc: Int): Either[Int, Int => Int] = if (acc >= 5) Left(acc) else Right(acc + _) + Assert.assertEquals(0, Iterator().lazyFoldRight(0)(sumOp)) + Assert.assertEquals(3, Iterator(1, 1, 1).lazyFoldRight(0)(sumOp)) + Assert.assertEquals(15, Iterator(1, 2, 3, 4, 5).lazyFoldRight(0)(sumOp)) + Assert.assertEquals(8, Iterator(1, 1, 1, 1, 1, 1, 1, 1).lazyFoldRight(0)(sumOp)) + Assert.assertEquals(5, Iterator(5, 4).lazyFoldRight(0)(sumOp)) + Assert.assertEquals(6, Iterator(1, 5, 4).lazyFoldRight(0)(sumOp)) + Assert.assertEquals(32, Iterator(32, 21, 10).lazyFoldRight(0)(sumOp)) + } + @Test def splitByShouldHonorEmptyIterator(): Unit = { val groupedIterator = Iterator.empty.splitBy(identity) Assert.assertFalse(groupedIterator.hasNext) - Assert.assertEquals(Try(groupedIterator.next).toString, Try(Iterator.empty.next()).toString) + Assert.assertEquals(Try(Iterator.empty.next()).toString, Try(groupedIterator.next).toString) } @Test @@ -18,9 +88,9 @@ class IteratorDecoratorTest { val value = Vector("1", "1", "1") val groupedIterator = value.iterator.splitBy(identity) Assert.assertTrue(groupedIterator.hasNext) - Assert.assertEquals(groupedIterator.next.toVector, value) + Assert.assertEquals(value, groupedIterator.next.toVector) Assert.assertFalse(groupedIterator.hasNext) - Assert.assertEquals(Try(groupedIterator.next).toString, Try(Iterator.empty.next()).toString) + Assert.assertEquals(Try(Iterator.empty.next()).toString, Try(groupedIterator.next).toString) } @Test @@ -28,14 +98,23 @@ class IteratorDecoratorTest { val value = Vector("1", "2", "2", "3", "3", "3", "2", "2") val groupedIterator = value.iterator.splitBy(identity) Assert.assertTrue(groupedIterator.hasNext) - Assert.assertEquals(groupedIterator.next.toVector, Vector("1")) + Assert.assertEquals(Vector("1"), groupedIterator.next.toVector) Assert.assertTrue(groupedIterator.hasNext) - Assert.assertEquals(groupedIterator.next.toVector, Vector("2", "2")) + Assert.assertEquals(Vector("2", "2"), groupedIterator.next.toVector) Assert.assertTrue(groupedIterator.hasNext) - Assert.assertEquals(groupedIterator.next.toVector, Vector("3", "3", "3")) + Assert.assertEquals(Vector("3", "3", "3"), groupedIterator.next.toVector) Assert.assertTrue(groupedIterator.hasNext) - Assert.assertEquals(groupedIterator.next.toVector, Vector("2", "2")) + Assert.assertEquals(Vector("2", "2"), groupedIterator.next.toVector) Assert.assertFalse(groupedIterator.hasNext) - Assert.assertEquals(Try(groupedIterator.next).toString, Try(Iterator.empty.next()).toString) + Assert.assertEquals(Try(Iterator.empty.next()).toString, Try(groupedIterator.next).toString) + } + + @Test + def splitByShouldSplitByFunction(): Unit = { + Assert.assertEquals(Seq(Seq((1,1), (1,2)), Seq((2,3))), Iterator((1,1), (1,2), (2,3)).splitBy(_._1).toSeq) + Assert.assertEquals( + Seq(Seq((1,1), (1,2)), Seq((2,3)), Seq((1,4))), + Iterator((1,1), (1,2), (2,3), (1,4)).splitBy(_._1).toSeq + ) } }