Skip to content

Documentation and tests for IteratorDecorator. #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 90 additions & 15 deletions src/main/scala/scala/collection/decorators/IteratorDecorator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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 =
Expand All @@ -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.
* <pre>
* Vector(1,2,2,3,3,3,2,2)
* .iterator
* .splitBy(identity)
* .toList
* </pre>
* produces
* <pre>
* List(Seq(1),
* Seq(2,2),
* Seq(3,3,3),
* Seq(2,2))
* </pre>
* 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]] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,115 @@ 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
def splitByShouldReturnIteratorOfSingleSeqWhenAllElHaveTheSameKey(): Unit = {
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
def splitByShouldReturnIteratorOfSeqOfConsecutiveElementsWithTheSameKey(): Unit = {
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
)
}
}