diff --git a/core/src/main/scala/cats/data/Newtype.scala b/core/src/main/scala/cats/data/Newtype.scala new file mode 100644 index 0000000000..20e9b02b6d --- /dev/null +++ b/core/src/main/scala/cats/data/Newtype.scala @@ -0,0 +1,12 @@ +package cats +package data + +/** + * Helper trait for `newtype`s. These allow you to create a zero-allocation wrapper around a specific type. + * Similar to `AnyVal` value classes, but never have any runtime overhead. + */ +private[data] trait Newtype { self => + private[data] type Base + private[data] trait Tag extends Any + type Type[A] <: Base with Tag +} diff --git a/core/src/main/scala/cats/data/NonEmptySet.scala b/core/src/main/scala/cats/data/NonEmptySet.scala new file mode 100644 index 0000000000..ade9b6184f --- /dev/null +++ b/core/src/main/scala/cats/data/NonEmptySet.scala @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2018 Luka Jacobowitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats +package data + +import cats.instances.sortedSet._ +import cats.kernel._ + +import scala.collection.immutable._ + + +private[data] object NonEmptySetImpl extends NonEmptySetInstances with Newtype { + + private[cats] def create[A](s: SortedSet[A]): Type[A] = + s.asInstanceOf[Type[A]] + + private[cats] def unwrap[A](s: Type[A]): SortedSet[A] = + s.asInstanceOf[SortedSet[A]] + + + def fromSet[A](as: SortedSet[A]): Option[NonEmptySet[A]] = + if (as.nonEmpty) Option(create(as)) else None + + def fromSetUnsafe[A](set: SortedSet[A]): NonEmptySet[A] = + if (set.nonEmpty) create(set) + else throw new IllegalArgumentException("Cannot create NonEmptySet from empty set") + + + def of[A](a: A, as: A*)(implicit A: Order[A]): NonEmptySet[A] = + create(SortedSet(a +: as: _*)(A.toOrdering)) + def apply[A](head: A, tail: SortedSet[A])(implicit A: Order[A]): NonEmptySet[A] = + create(SortedSet(head)(A.toOrdering) ++ tail) + def one[A](a: A)(implicit A: Order[A]): NonEmptySet[A] = create(SortedSet(a)(A.toOrdering)) + + implicit def catsNonEmptySetOps[A](value: NonEmptySet[A]): NonEmptySetOps[A] = + new NonEmptySetOps(value) +} + + +sealed class NonEmptySetOps[A](val value: NonEmptySet[A]) { + + private implicit val ordering: Ordering[A] = toSortedSet.ordering + private implicit val order: Order[A] = Order.fromOrdering + + /** + * Converts this set to a `SortedSet` + */ + def toSortedSet: SortedSet[A] = NonEmptySetImpl.unwrap(value) + + + /** + * Adds an element to this set, returning a new `NonEmptySet` + */ + def add(a: A): NonEmptySet[A] = NonEmptySet.create(toSortedSet + a) + + /** + * Alias for [[union]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 4, 5) + * scala> nes ++ NonEmptySet.of(1, 2, 7) + * res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 4, 5, 7) + * }}} + */ + def ++(as: NonEmptySet[A]): NonEmptySet[A] = union(as) + + /** + * Alias for [[union]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 4, 5) + * scala> nes | NonEmptySet.of(1, 2, 7) + * res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 4, 5, 7) + * }}} + */ + def | (as: NonEmptySet[A]): NonEmptySet[A] = union(as) + + /** + * Alias for [[diff]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 4, 5) + * scala> nes -- NonEmptySet.of(1, 2, 7) + * res0: scala.collection.immutable.SortedSet[Int] = TreeSet(4, 5) + * }}} + */ + def --(as: NonEmptySet[A]): SortedSet[A] = diff(as) + + /** + * Alias for [[diff]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 4, 5) + * scala> nes &~ NonEmptySet.of(1, 2, 7) + * res0: scala.collection.immutable.SortedSet[Int] = TreeSet(4, 5) + * }}} + */ + def &~(as: NonEmptySet[A]): SortedSet[A] = diff(as) + + /** + * Alias for [[intersect]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 4, 5) + * scala> nes & NonEmptySet.of(1, 2, 7) + * res0: scala.collection.immutable.SortedSet[Int] = TreeSet(1, 2) + * }}} + */ + def &(as: NonEmptySet[A]): SortedSet[A] = intersect(as) + + + /** + * Removes a key from this set, returning a new `SortedSet`. + */ + def -(a: A): SortedSet[A] = toSortedSet - a + + /** + * Applies f to all the elements + */ + def map[B](f: A => B)(implicit B: Order[B]): NonEmptySet[B] = + NonEmptySetImpl.create(SortedSet(toSortedSet.map(f).to: _*)(B.toOrdering)) + + /** + * Converts this set to a `NonEmptyList`. + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 3, 4, 5) + * scala> nes.toNonEmptyList + * res0: cats.data.NonEmptyList[Int] = NonEmptyList(1, 2, 3, 4, 5) + * }}} + */ + def toNonEmptyList: NonEmptyList[A] = NonEmptyList.fromListUnsafe(toSortedSet.toList) + + /** + * Returns the first element of this set. + */ + def head: A = toSortedSet.head + + /** + * Returns all but the first element of this set. + */ + def tail: SortedSet[A] = toSortedSet.tail + + /** + * Returns the last element of this set. + */ + def last: A = toSortedSet.last + + /** + * Alias for [[contains]] + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 3, 4, 5) + * scala> nes(3) + * res0: Boolean = true + * scala> nes(7) + * res1: Boolean = false + * }}} + */ + def apply(a: A): Boolean = contains(a) + + /** + * Tests if some element is contained in this set. + */ + def contains(a: A): Boolean = toSortedSet(a) + + /** + * Computes the difference of this set and another set. + */ + def diff(as: NonEmptySet[A]): SortedSet[A] = toSortedSet -- as.toSortedSet + + /** + * Computes the union between this NES and another NES. + */ + def union(as: NonEmptySet[A]): NonEmptySet[A] = NonEmptySetImpl.create(toSortedSet ++ as.toSortedSet) + + /** + * Computes the intersection between this set and another set. + */ + def intersect(as: NonEmptySet[A]): SortedSet[A] = toSortedSet.filter(as.apply) + + /** + * Tests whether a predicate holds for all elements of this set. + */ + def forall(p: A ⇒ Boolean): Boolean = toSortedSet.forall(p) + + /** + * Tests whether a predicate holds for at least one element of this set. + */ + def exists(f: A ⇒ Boolean): Boolean = toSortedSet.exists(f) + + /** + * Returns the first value that matches the given predicate. + */ + def find(f: A ⇒ Boolean): Option[A] = toSortedSet.find(f) + + /** + * Returns a new `SortedSet` containing all elements where the result of `pf` is defined. + */ + def collect[B](pf: PartialFunction[A, B])(implicit B: Order[B]): SortedSet[B] = { + implicit val ordering = B.toOrdering + toSortedSet.collect(pf) + } + + /** + * Filters all elements of this set that do not satisfy the given predicate. + */ + def filter(p: A ⇒ Boolean): SortedSet[A] = toSortedSet.filter(p) + + /** + * Filters all elements of this set that satisfy the given predicate. + */ + def filterNot(p: A ⇒ Boolean): SortedSet[A] = filter(t => !p(t)) + + + /** + * Left-associative fold using f. + */ + def foldLeft[B](b: B)(f: (B, A) => B): B = + toSortedSet.foldLeft(b)(f) + + /** + * Right-associative fold using f. + */ + def foldRight[B](lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + Foldable[SortedSet].foldRight(toSortedSet, lb)(f) + + /** + * Left-associative reduce using f. + */ + def reduceLeft(f: (A, A) => A): A = + toSortedSet.reduceLeft(f) + + /** + * Apply `f` to the "initial element" of this set and lazily combine it + * with every other value using the given function `g`. + */ + def reduceLeftTo[B](f: A => B)(g: (B, A) => B): B = { + tail.foldLeft(f(head))((b, a) => g(b, a)) + } + + /** + * Left-associative reduce using f. + */ + def reduceRight(f: (A, Eval[A]) => Eval[A]): Eval[A] = + reduceRightTo(identity)(f) + + /** + * Apply `f` to the "initial element" of this set and lazily combine it + * with every other value using the given function `g`. + */ + def reduceRightTo[B](f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = + Always((head, tail)).flatMap { case (a, ga) => + Foldable[SortedSet].reduceRightToOption(ga)(f)(g).flatMap { + case Some(b) => g(a, Now(b)) + case None => Later(f(a)) + } + } + + /** + * Reduce using the Semigroup of A + */ + def reduce[AA >: A](implicit S: Semigroup[AA]): AA = + S.combineAllOption(toSortedSet).get + + /** + * Map a function over all the elements of this set and concatenate the resulting sets into one. + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val nes = NonEmptySet.of(1, 2, 3) + * scala> nes.concatMap(n => NonEmptySet.of(n, n * 4, n * 5)) + * res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 3, 4, 5, 8, 10, 12, 15) + * }}} + */ + def concatMap[B](f: A => NonEmptySet[B])(implicit B: Order[B]): NonEmptySet[B] = { + implicit val ordering = B.toOrdering + NonEmptySetImpl.create(toSortedSet.flatMap(f andThen (_.toSortedSet))) + } + + + /** + * 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[SortedSet[A]].show(toSortedSet)}" + + /** + * Typesafe equality operator. + * + * This method is similar to == except that it only allows two + * NonEmptySet[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: NonEmptySet[A]): Boolean = + Eq[SortedSet[A]].eqv(toSortedSet, that.toSortedSet) + + /** + * Returns the number of elements in this set. + */ + def length: Int = toSortedSet.size + + /** + * Zips this `NonEmptySet` with another `NonEmptySet` and applies a function for each pair of elements. + * + * {{{ + * scala> import cats.data.NonEmptySet + * scala> import cats.implicits._ + * scala> val as = NonEmptySet.of(1, 2, 3) + * scala> val bs = NonEmptySet.of("A", "B", "C") + * scala> as.zipWith(bs)(_ + _) + * res0: cats.data.NonEmptySet[String] = TreeSet(1A, 2B, 3C) + * }}} + */ + def zipWith[B, C](b: NonEmptySet[B])(f: (A, B) => C)(implicit C: Order[C]): NonEmptySet[C] = + NonEmptySetImpl.create(SortedSet((toSortedSet, b.toSortedSet).zipped.map(f).to: _*)(C.toOrdering)) + + /** + * Zips this `NonEmptySet` with its index. + */ + def zipWithIndex: NonEmptySet[(A, Int)] = + NonEmptySetImpl.create(toSortedSet.zipWithIndex) +} + +private[data] sealed abstract class NonEmptySetInstances { + implicit val catsDataInstancesForNonEmptySet: SemigroupK[NonEmptySet] with Reducible[NonEmptySet] = + new SemigroupK[NonEmptySet] with Reducible[NonEmptySet] { + + def combineK[A](a: NonEmptySet[A], b: NonEmptySet[A]): NonEmptySet[A] = + a | b + + override def size[A](fa: NonEmptySet[A]): Long = fa.length.toLong + + override def reduceLeft[A](fa: NonEmptySet[A])(f: (A, A) => A): A = + fa.reduceLeft(f) + + override def reduce[A](fa: NonEmptySet[A])(implicit A: Semigroup[A]): A = + fa.reduce + + def reduceLeftTo[A, B](fa: NonEmptySet[A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g) + + def reduceRightTo[A, B](fa: NonEmptySet[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = + fa.reduceRightTo(f)(g) + + override def foldLeft[A, B](fa: NonEmptySet[A], b: B)(f: (B, A) => B): B = + fa.foldLeft(b)(f) + + override def foldRight[A, B](fa: NonEmptySet[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + fa.foldRight(lb)(f) + + override def foldMap[A, B](fa: NonEmptySet[A])(f: A => B)(implicit B: Monoid[B]): B = + B.combineAll(fa.toSortedSet.iterator.map(f)) + + override def fold[A](fa: NonEmptySet[A])(implicit A: Monoid[A]): A = + fa.reduce + + override def find[A](fa: NonEmptySet[A])(f: A => Boolean): Option[A] = + fa.find(f) + + override def forall[A](fa: NonEmptySet[A])(p: A => Boolean): Boolean = + fa.forall(p) + + override def exists[A](fa: NonEmptySet[A])(p: A => Boolean): Boolean = + fa.exists(p) + + override def toList[A](fa: NonEmptySet[A]): List[A] = fa.toSortedSet.toList + + override def toNonEmptyList[A](fa: NonEmptySet[A]): NonEmptyList[A] = + fa.toNonEmptyList + } + + implicit def catsDataEqForNonEmptySet[A: Order]: Eq[NonEmptySet[A]] = + new Eq[NonEmptySet[A]]{ + def eqv(x: NonEmptySet[A], y: NonEmptySet[A]): Boolean = x === y + } + + implicit def catsDataShowForNonEmptySet[A](implicit A: Show[A]): Show[NonEmptySet[A]] = + Show.show[NonEmptySet[A]](_.show) + + implicit def catsDataSemilatticeForNonEmptySet[A]: Semilattice[NonEmptySet[A]] = new Semilattice[NonEmptySet[A]] { + def combine(x: NonEmptySet[A], y: NonEmptySet[A]): NonEmptySet[A] = x | y + } +} + diff --git a/core/src/main/scala/cats/data/package.scala b/core/src/main/scala/cats/data/package.scala index 59775d7161..a132c68589 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -11,6 +11,9 @@ package object data { def NonEmptyStream[A](head: A, tail: A*): NonEmptyStream[A] = OneAnd(head, tail.toStream) + type NonEmptySet[A] = NonEmptySetImpl.Type[A] + val NonEmptySet = NonEmptySetImpl + type ReaderT[F[_], A, B] = Kleisli[F, A, B] val ReaderT = Kleisli diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 54f2238353..0e9c7bf3df 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -51,6 +51,13 @@ object arbitrary extends ArbitraryInstances0 { implicit def catsLawsCogenForNonEmptyVector[A](implicit A: Cogen[A]): Cogen[NonEmptyVector[A]] = Cogen[Vector[A]].contramap(_.toVector) + implicit def catsLawsArbitraryForNonEmptySet[A: Order](implicit A: Arbitrary[A]): Arbitrary[NonEmptySet[A]] = + Arbitrary(implicitly[Arbitrary[SortedSet[A]]].arbitrary.flatMap(fa => + A.arbitrary.map(a => NonEmptySet(a, fa)))) + + implicit def catsLawsCogenForNonEmptySet[A: Order: Cogen]: Cogen[NonEmptySet[A]] = + Cogen[SortedSet[A]].contramap(_.toSortedSet) + implicit def catsLawsArbitraryForZipVector[A](implicit A: Arbitrary[A]): Arbitrary[ZipVector[A]] = Arbitrary(implicitly[Arbitrary[Vector[A]]].arbitrary.map(v => new ZipVector(v))) diff --git a/tests/src/test/scala/cats/tests/NonEmptySetSuite.scala b/tests/src/test/scala/cats/tests/NonEmptySetSuite.scala new file mode 100644 index 0000000000..36250049e4 --- /dev/null +++ b/tests/src/test/scala/cats/tests/NonEmptySetSuite.scala @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2018 Luka Jacobowitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats +package tests + +import cats.laws.discipline._ +import cats.laws.discipline.arbitrary._ +import cats.data.NonEmptySet +import cats.kernel.laws.discipline.{SemilatticeTests, EqTests} + +import scala.collection.immutable.SortedSet + +class NonEmptySetSuite extends CatsSuite { + + checkAll("NonEmptySet[Int]", SemigroupKTests[NonEmptySet].semigroupK[Int]) + checkAll("NonEmptySet[Int]", ReducibleTests[NonEmptySet].reducible[Option, Int, Int]) + checkAll("NonEmptySet[String]", SemilatticeTests[NonEmptySet[String]].band) + checkAll("NonEmptySet[String]", EqTests[NonEmptySet[String]].eqv) + + test("First element is always the smallest") { + forAll { (nes: NonEmptySet[Int]) => + nes.forall { v => Order[Int].lteqv(nes.head, v) } should === (true) + + } + } + + test("Show is not empty and is formatted as expected") { + forAll { (nes: NonEmptySet[Int]) => + nes.show.nonEmpty should === (true) + nes.show.startsWith("NonEmptySortedSet(") should === (true) + nes.show should === (implicitly[Show[NonEmptySet[Int]]].show(nes)) + nes.show.contains(nes.head.show) should === (true) + } + } + + test("Show is formatted correctly") { + val nonEmptySet = NonEmptySet("Test", SortedSet.empty[String]) + nonEmptySet.show should === ("NonEmptySortedSet(Test)") + } + + test("Creating NonEmptySet + toSet is identity") { + forAll { (i: Int, tail: SortedSet[Int]) => + val set = tail + i + val nonEmptySet = NonEmptySet(i, tail) + set should === (nonEmptySet.toSortedSet) + } + } + + test("NonEmptySet#filter is consistent with Set#filter") { + forAll { (nes: NonEmptySet[Int], p: Int => Boolean) => + val set = nes.toSortedSet + nes.filter(p) should === (set.filter(p)) + } + } + + test("NonEmptySet#filterNot is consistent with Set#filterNot") { + forAll { (nes: NonEmptySet[Int], p: Int => Boolean) => + val set = nes.toSortedSet + nes.filterNot(p) should === (set.filterNot(p)) + } + } + + test("NonEmptySet#collect is consistent with Set#collect") { + forAll { (nes: NonEmptySet[Int], pf: PartialFunction[Int, String]) => + val set = nes.toSortedSet + nes.collect(pf) should === (set.collect(pf)) + } + } + + test("NonEmptySet#find is consistent with Set#find") { + forAll { (nes: NonEmptySet[Int], p: Int => Boolean) => + val set = nes.toSortedSet + nes.find(p) should === (set.find(p)) + } + } + + test("NonEmptySet#exists is consistent with Set#exists") { + forAll { (nes: NonEmptySet[Int], p: Int => Boolean) => + val set = nes.toSortedSet + nes.exists(p) should === (set.exists(p)) + } + } + + test("NonEmptySet#forall is consistent with Set#forall") { + forAll { (nes: NonEmptySet[Int], p: Int => Boolean) => + val set = nes.toSortedSet + nes.forall(p) should === (set.forall(p)) + } + } + + test("NonEmptySet#map is consistent with Set#map") { + forAll { (nes: NonEmptySet[Int], p: Int => String) => + val set = nes.toSortedSet + nes.map(p).toSortedSet should === (set.map(p)) + } + } + + test("reduceLeft consistent with foldLeft") { + forAll { (nes: NonEmptySet[Int], f: (Int, Int) => Int) => + nes.reduceLeft(f) should === (nes.tail.foldLeft(nes.head)(f)) + } + } + + test("reduceRight consistent with foldRight") { + forAll { (nes: NonEmptySet[Int], f: (Int, Eval[Int]) => Eval[Int]) => + val got = nes.reduceRight(f).value + val last = nes.last + val rev = nes - last + val expected = rev.foldRight(last)((a, b) => f(a, Now(b)).value) + got should === (expected) + } + } + + test("reduce consistent with fold") { + forAll { (nes: NonEmptySet[Int]) => + nes.reduce should === (nes.fold) + } + } + + + test("reduce consistent with reduceK") { + forAll { (nes: NonEmptySet[Option[Int]]) => + nes.reduce(SemigroupK[Option].algebra[Int]) should === (nes.reduceK) + } + } + + test("reduceLeftToOption consistent with foldLeft + Option") { + forAll { (nes: NonEmptySet[Int], f: Int => String, g: (String, Int) => String) => + val expected = nes.tail.foldLeft(Option(f(nes.head))) { (opt, i) => + opt.map(s => g(s, i)) + } + nes.reduceLeftToOption(f)(g) should === (expected) + } + } + + test("reduceRightToOption consistent with foldRight + Option") { + forAll { (nes: NonEmptySet[Int], f: Int => String, g: (Int, Eval[String]) => Eval[String]) => + val got = nes.reduceRightToOption(f)(g).value + val last = nes.last + val rev = nes - last + val expected = rev.foldRight(Option(f(last))) { (i, opt) => + opt.map(s => g(i, Now(s)).value) + } + got should === (expected) + } + } + + test("reduceLeftM consistent with foldM") { + forAll { (nes: NonEmptySet[Int], f: Int => Option[Int]) => + val got = nes.reduceLeftM(f)((acc, i) => f(i).map(acc + _)) + val expected = f(nes.head).flatMap { hd => + nes.tail.foldM(hd)((acc, i) => f(i).map(acc + _)) + } + got should === (expected) + } + } + + test("reduceMapM consistent with foldMapM") { + forAll { (nes: NonEmptySet[Int], f: Int => Option[Int]) => + nes.reduceMapM(f) should === (nes.foldMapM(f)) + } + } + + test("fromSet round trip") { + forAll { l: SortedSet[Int] => + NonEmptySet.fromSet(l).map(_.toSortedSet).getOrElse(SortedSet.empty[Int]) should === (l) + } + + forAll { nes: NonEmptySet[Int] => + NonEmptySet.fromSet(nes.toSortedSet) should === (Some(nes)) + } + } + + test("fromSetUnsafe/fromSet consistency") { + forAll { nes: NonEmptySet[Int] => + NonEmptySet.fromSet(nes.toSortedSet) should === (Some(NonEmptySet.fromSetUnsafe(nes.toSortedSet))) + } + } + + test("fromSetUnsafe empty set") { + val _ = intercept[IllegalArgumentException] { + NonEmptySet.fromSetUnsafe(SortedSet.empty[Int]) + } + } + + test("+ consistent with Set") { + forAll { (nes: NonEmptySet[Int], i: Int) => + (nes add i).toSortedSet should === (nes.toSortedSet + i) + } + } + + test("NonEmptySet#zipWithIndex is consistent with Set#zipWithIndex") { + forAll { nes: NonEmptySet[Int] => + nes.zipWithIndex.toSortedSet should === (nes.toSortedSet.zipWithIndex) + } + } + + test("NonEmptySet#length is consistent with Set#size") { + forAll { nes: NonEmptySet[Int] => + nes.length should === (nes.toSortedSet.size) + } + } + + test("NonEmptySet#concat is consistent with Set#++") { + forAll { (nes: NonEmptySet[Int], l: SortedSet[Int], n: Int) => + nes.union(NonEmptySet(n, l)).toSortedSet should === (nes.toSortedSet ++ (l + n)) + } + } + + test("NonEmptySet#zipWith is consistent with Set#zip and then Set#map") { + forAll { (a: NonEmptySet[Int], b: NonEmptySet[Int], f: (Int, Int) => Int) => + a.zipWith(b)(f).toSortedSet should ===(a.toSortedSet.zip(b.toSortedSet).map { case (x, y) => f(x, y) }) + } + } + + test("NonEmptySet.of is consistent with removal") { + forAll { (is: SortedSet[Int], i: Int) => + NonEmptySet.of(i, is.toList: _*) - i should ===(is - i) + } + } + +}