Skip to content
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

Add distinct method to NonEmptyList and NonEmptyVector. #1240

Closed
wants to merge 4 commits into from
Closed
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
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cats.instances.list._
import cats.syntax.order._

import scala.annotation.tailrec
import scala.collection.immutable.TreeSet
import scala.collection.mutable.ListBuffer

/**
Expand Down Expand Up @@ -105,6 +106,20 @@ final case class NonEmptyList[A](head: A, tail: List[A]) {
def show(implicit A: Show[A]): String =
toList.iterator.map(A.show).mkString("NonEmptyList(", ", ", ")")

/**
* Remove duplicates. Duplicates are checked using `Order[_]` instance.
*/
def distinct(implicit O: Order[A]): NonEmptyList[A] = {
implicit val ord = O.toOrdering

val buf = ListBuffer.empty[A]
tail.foldLeft(TreeSet(head)) { (elementsSoFar, a) =>
if (elementsSoFar(a)) elementsSoFar else { buf += a; elementsSoFar + a }
}

NonEmptyList(head, buf.toList)
}

override def toString: String = s"NonEmpty$toList"
}

Expand Down
124 changes: 69 additions & 55 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,142 +2,156 @@ package cats
package data

import scala.annotation.tailrec
import scala.collection.immutable.VectorBuilder
import scala.collection.immutable.{TreeSet, VectorBuilder}
import cats.instances.vector._

/**
* A data type which represents a `Vector` guaranteed to contain at least one element.
* <br/>
* Note that the constructor is `private` to prevent accidental construction of an empty
* `NonEmptyVector`. However, due to https://issues.scala-lang.org/browse/SI-6601, on
* Scala 2.10, this may be bypassed due to a compiler bug.
*/
* A data type which represents a `Vector` guaranteed to contain at least one element.
* <br/>
* Note that the constructor is `private` to prevent accidental construction of an empty
* `NonEmptyVector`. However, due to https://issues.scala-lang.org/browse/SI-6601, on
* Scala 2.10, this may be bypassed due to a compiler bug.
*/
final class NonEmptyVector[A] private (val toVector: Vector[A]) extends AnyVal {

/** Gets the element at the index, if it exists */
def get(i: Int): Option[A] =
toVector.lift(i)
toVector.lift(i)

/** Gets the element at the index, or throws an exception if none exists */
def getUnsafe(i: Int): A = toVector(i)

/** Updates the element at the index, if it exists */
def updated(i: Int, a: A): Option[NonEmptyVector[A]] =
if (toVector.isDefinedAt(i)) Some(new NonEmptyVector(toVector.updated(i, a))) else None
if (toVector.isDefinedAt(i)) Some(new NonEmptyVector(toVector.updated(i, a))) else None

/**
* Updates the element at the index, or throws an `IndexOutOfBoundsException`
* if none exists (if `i` does not satisfy `0 <= i < length`).
*/
* Updates the element at the index, or throws an `IndexOutOfBoundsException`
* if none exists (if `i` does not satisfy `0 <= i < length`).
*/
def updatedUnsafe(i: Int, a: A):
NonEmptyVector[A] = new NonEmptyVector(toVector.updated(i, a))
NonEmptyVector[A] = new NonEmptyVector(toVector.updated(i, a))

def head: A = toVector.head

def tail: Vector[A] = toVector.tail

/**
* remove elements not matching the predicate
*/
* remove elements not matching the predicate
*/
def filter(f: A => Boolean): Vector[A] = toVector.filter(f)

/**
* Alias for [[concat]]
*/
* Alias for [[concat]]
*/
def ++(other: Vector[A]): NonEmptyVector[A] = concat(other)

/**
* Append another `Vector` to this, producing a new `NonEmptyVector`.
*/
* Append another `Vector` to this, producing a new `NonEmptyVector`.
*/
def concat(other: Vector[A]): NonEmptyVector[A] = new NonEmptyVector(toVector ++ other)

/**
* Append another `NonEmptyVector` to this, producing a new `NonEmptyVector`.
*/
* Append another `NonEmptyVector` to this, producing a new `NonEmptyVector`.
*/
def concatNEV(other: NonEmptyVector[A]): NonEmptyVector[A] = new NonEmptyVector(toVector ++ other.toVector)

/**
* Find the first element matching the predicate, if one exists
*/
* Find the first element matching the predicate, if one exists
*/
def find(f: A => Boolean): Option[A] = toVector.find(f)

/**
* Check whether at least one element satisfies the predicate.
*/
* Check whether at least one element satisfies the predicate.
*/
def exists(f: A => Boolean): Boolean = toVector.exists(f)

/**
* Check whether all elements satisfy the predicate.
*/
* Check whether all elements satisfy the predicate.
*/
def forall(f: A => Boolean): Boolean = toVector.forall(f)

/**
* Left-associative fold using f.
*/
* Left-associative fold using f.
*/
def foldLeft[B](b: B)(f: (B, A) => B): B =
toVector.foldLeft(b)(f)
toVector.foldLeft(b)(f)

/**
* Right-associative fold using f.
*/
* Right-associative fold using f.
*/
def foldRight[B](lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Foldable[Vector].foldRight(toVector, lb)(f)
Foldable[Vector].foldRight(toVector, lb)(f)

/**
* Applies f to all the elements
*/
def map[B](f: A => B): NonEmptyVector[B] =
new NonEmptyVector(toVector.map(f))
new NonEmptyVector(toVector.map(f))

/**
* Applies f to all elements and combines the result
*/
def flatMap[B](f: A => NonEmptyVector[B]): NonEmptyVector[B] =
new NonEmptyVector(toVector.flatMap(a => f(a).toVector))
new NonEmptyVector(toVector.flatMap(a => f(a).toVector))

/**
* Left-associative reduce using f.
*/
def reduceLeft(f: (A, A) => A): A =
tail.foldLeft(head)(f)
tail.foldLeft(head)(f)

/**
* Reduce using the Semigroup of A
*/
def reduce(implicit S: Semigroup[A]): A =
S.combineAllOption(toVector).get
S.combineAllOption(toVector).get

/**
* Typesafe equality operator.
*
* This method is similar to == except that it only allows two
* NonEmptyVector[A] values to be compared to each other, and uses
* equality provided by Eq[_] instances, rather than using the
* universal equality provided by .equals.
*/
* Typesafe equality operator.
*
* This method is similar to == except that it only allows two
* NonEmptyVector[A] values to be compared to each other, and uses
* equality provided by Eq[_] instances, rather than using the
* universal equality provided by .equals.
*/
def ===(that: NonEmptyVector[A])(implicit A: Eq[A]): Boolean = Eq[Vector[A]].eqv(toVector, that.toVector)

/**
* Typesafe stringification method.
*
* This method is similar to .toString except that it stringifies
* values according to Show[_] instances, rather than using the
* universal .toString method.
*/
* Typesafe stringification method.
*
* This method is similar to .toString except that it stringifies
* values according to Show[_] instances, rather than using the
* universal .toString method.
*/
def show(implicit A: Show[A]): String =
s"NonEmpty${Show[Vector[A]].show(toVector)}"
s"NonEmpty${Show[Vector[A]].show(toVector)}"

def length: Int = toVector.length

override def toString: String = s"NonEmpty${toVector.toString}"

/**
* Remove duplicates. Duplicates are checked using `Order[_]` instance.
*/
def distinct(implicit O: Order[A]): NonEmptyVector[A] = {
implicit val ord = O.toOrdering

val buf = Vector.newBuilder[A]
tail.foldLeft(TreeSet(head)) { (elementsSoFar, a) =>
if (elementsSoFar(a)) elementsSoFar else { buf += a; elementsSoFar + a }
}

NonEmptyVector(head, buf.result())
}
}

private[data] sealed trait NonEmptyVectorInstances {

implicit val catsDataInstancesForNonEmptyVector: SemigroupK[NonEmptyVector] with Reducible[NonEmptyVector]
with Comonad[NonEmptyVector] with Traverse[NonEmptyVector] with MonadRec[NonEmptyVector] =
with Comonad[NonEmptyVector] with Traverse[NonEmptyVector] with MonadRec[NonEmptyVector] =
new NonEmptyReducible[NonEmptyVector, Vector] with SemigroupK[NonEmptyVector]
with Comonad[NonEmptyVector] with Traverse[NonEmptyVector] with MonadRec[NonEmptyVector] {
with Comonad[NonEmptyVector] with Traverse[NonEmptyVector] with MonadRec[NonEmptyVector] {

def combineK[A](a: NonEmptyVector[A], b: NonEmptyVector[A]): NonEmptyVector[A] =
a concatNEV b
Expand Down Expand Up @@ -185,14 +199,14 @@ private[data] sealed trait NonEmptyVectorInstances {
def tailRecM[A, B](a: A)(f: A => NonEmptyVector[A Xor B]): NonEmptyVector[B] = {
val buf = new VectorBuilder[B]
@tailrec def go(v: NonEmptyVector[A Xor B]): Unit = v.head match {
case Xor.Right(b) =>
case Xor.Right(b) =>
buf += b
NonEmptyVector.fromVector(v.tail) match {
case Some(t) => go(t)
case None => ()
}
case Xor.Left(a) => go(f(a).concat(v.tail))
}
}
go(f(a))
NonEmptyVector.fromVectorUnsafe(buf.result())
}
Expand Down
6 changes: 6 additions & 0 deletions tests/src/test/scala/cats/tests/NonEmptyListTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ class NonEmptyListTests extends CatsSuite {
(i :: nel).toList should === (i :: nel.toList)
}
}

test("NonEmptyList#distinct is consistent with List#distinct") {
forAll { nel: NonEmptyList[Int] =>
nel.distinct.toList should === (nel.toList.distinct)
}
}
}

class ReducibleNonEmptyListCheck extends ReducibleCheck[NonEmptyList]("NonEmptyList") {
Expand Down
7 changes: 6 additions & 1 deletion tests/src/test/scala/cats/tests/NonEmptyVectorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import scala.util.Properties
class NonEmptyVectorTests extends CatsSuite {
// Lots of collections here.. telling ScalaCheck to calm down a bit
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
PropertyCheckConfig(maxSize = 5, minSuccessful = 20)
PropertyCheckConfig(maxSize = 5, minSuccessful = 20)

checkAll("NonEmptyVector[Int]", OrderLaws[NonEmptyVector[Int]].eqv)

Expand Down Expand Up @@ -258,6 +258,11 @@ class NonEmptyVectorTests extends CatsSuite {
"val bad: NonEmptyVector[Int] = NonEmptyVector(Vector(1))" shouldNot compile
}

test("NonEmptyVector#distinct is consistent with Vector#distinct") {
forAll { nonEmptyVector: NonEmptyVector[Int] =>
nonEmptyVector.distinct.toVector should === (nonEmptyVector.toVector.distinct)
}
}
}

class ReducibleNonEmptyVectorCheck extends ReducibleCheck[NonEmptyVector]("NonEmptyVector") {
Expand Down