diff --git a/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala b/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala index 57ee14aa15..5a9ed774c9 100644 --- a/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala +++ b/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala @@ -8,7 +8,7 @@ import cats.kernel.std.all._ import org.typelevel.discipline.{ Laws } import org.typelevel.discipline.scalatest.Discipline -import org.scalacheck.{ Arbitrary } +import org.scalacheck.{ Arbitrary, Gen } import Arbitrary.arbitrary import org.scalatest.FunSuite import scala.util.Random @@ -79,6 +79,58 @@ class LawTests extends FunSuite with Discipline { laws[GroupLaws, (Int, Int)].check(_.band) laws[GroupLaws, Unit].check(_.boundedSemilattice) + + // Comparison related + + // Something that can give NaN for test + def subsetPartialOrder[A]: PartialOrder[Set[A]] = new PartialOrder[Set[A]] { + def partialCompare(x: Set[A], y: Set[A]): Double = + if (x == y) 0.0 + else if (x subsetOf y) -1.0 + else if (y subsetOf x) 1.0 + else Double.NaN + } + + laws[OrderLaws, Set[Int]]("subset").check(_.partialOrder(subsetPartialOrder[Int])) + + implicit val arbitraryComparison: Arbitrary[Comparison] = + Arbitrary(Gen.oneOf(Comparison.GreaterThan, Comparison.EqualTo, Comparison.LessThan)) + + laws[OrderLaws, Comparison].check(_.eqv) + + test("comparison") { + val order = Order[Int] + val eqv = Eq[Comparison] + eqv.eqv(order.comparison(1, 0), Comparison.GreaterThan) && + eqv.eqv(order.comparison(0, 0), Comparison.EqualTo) && + eqv.eqv(order.comparison(-1, 0), Comparison.LessThan) + } + + test("partialComparison") { + val po = subsetPartialOrder[Int] + val eqv = Eq[Option[Comparison]] + eqv.eqv(po.partialComparison(Set(1), Set()), Some(Comparison.GreaterThan)) && + eqv.eqv(po.partialComparison(Set(), Set()), Some(Comparison.EqualTo)) && + eqv.eqv(po.partialComparison(Set(), Set(1)), Some(Comparison.LessThan)) && + eqv.eqv(po.partialComparison(Set(1, 2), Set(2, 3)), None) + } + + test("signum . toInt . comparison = signum . compare") { + check { (i: Int, j: Int) => + val found = Order[Int].comparison(i, j) + val expected = Order[Int].compare(i, j) + Eq[Int].eqv(found.toInt.signum, expected.signum) + } + } + + test("signum . toDouble . partialComparison = signum . partialCompare") { + check { (x: Set[Int], y: Set[Int]) => + val found = subsetPartialOrder[Int].partialComparison(x, y).map(_.toDouble.signum) + val expected = Some(subsetPartialOrder[Int].partialCompare(x, y)).filter(d => !d.isNaN).map(_.signum) + Eq[Option[Int]].eqv(found, expected) + } + } + // esoteric machinery follows... implicit lazy val band: Band[(Int, Int)] = diff --git a/kernel/src/main/scala/cats/kernel/Comparison.scala b/kernel/src/main/scala/cats/kernel/Comparison.scala new file mode 100644 index 0000000000..536db022e6 --- /dev/null +++ b/kernel/src/main/scala/cats/kernel/Comparison.scala @@ -0,0 +1,30 @@ +package cats.kernel + +/** ADT encoding the possible results of a comparison */ +sealed abstract class Comparison(val toInt: Int, val toDouble: Double) extends Product with Serializable + +object Comparison { + final case object GreaterThan extends Comparison(1, 1.0) + final case object EqualTo extends Comparison(0, 0.0) + final case object LessThan extends Comparison(-1, -1.0) + + // Used for fromDouble + private val SomeGt = Some(Comparison.GreaterThan) + private val SomeEq = Some(Comparison.EqualTo) + private val SomeLt = Some(Comparison.LessThan) + + def fromInt(int: Int): Comparison = { + if (int > 0) Comparison.GreaterThan + else if (int == 0) Comparison.EqualTo + else Comparison.LessThan + } + + def fromDouble(double: Double): Option[Comparison] = { + if (double.isNaN) None + else if (double > 0.0) SomeGt + else if (double == 0.0) SomeEq + else SomeLt + } + + implicit val catsKernelEqForComparison: Eq[Comparison] = Eq.fromUniversalEquals +} diff --git a/kernel/src/main/scala/cats/kernel/Order.scala b/kernel/src/main/scala/cats/kernel/Order.scala index 052467961c..577214057e 100644 --- a/kernel/src/main/scala/cats/kernel/Order.scala +++ b/kernel/src/main/scala/cats/kernel/Order.scala @@ -20,7 +20,6 @@ import scala.{specialized => sp} * By the totality law, x <= y and y <= x cannot be both false. */ trait Order[@sp A] extends Any with PartialOrder[A] { self => - /** * Result of comparing `x` with `y`. Returns an Int whose sign is: * - negative iff `x < y` @@ -29,6 +28,12 @@ trait Order[@sp A] extends Any with PartialOrder[A] { self => */ def compare(x: A, y: A): Int + /** + * Like `compare`, but returns a [[cats.kernel.Comparison]] instead of an Int. + * Has the benefit of being able to pattern match on, but not as performant. + */ + def comparison(x: A, y: A): Comparison = Comparison.fromInt(compare(x, y)) + def partialCompare(x: A, y: A): Double = compare(x, y).toDouble /** @@ -135,7 +140,6 @@ abstract class OrderFunctions[O[T] <: Order[T]] extends PartialOrderFunctions[O] } object Order extends OrderFunctions[Order] { - /** * Access an implicit `Order[A]`. */ diff --git a/kernel/src/main/scala/cats/kernel/PartialOrder.scala b/kernel/src/main/scala/cats/kernel/PartialOrder.scala index 68e11817af..9d5f35ecb6 100644 --- a/kernel/src/main/scala/cats/kernel/PartialOrder.scala +++ b/kernel/src/main/scala/cats/kernel/PartialOrder.scala @@ -23,7 +23,6 @@ import scala.{specialized => sp} * false true = 1.0 (corresponds to x > y) */ trait PartialOrder[@sp A] extends Any with Eq[A] { self => - /** * Result of comparing `x` with `y`. Returns NaN if operands are not * comparable. If operands are comparable, returns a Double whose @@ -34,6 +33,13 @@ trait PartialOrder[@sp A] extends Any with Eq[A] { self => */ def partialCompare(x: A, y: A): Double + /** + * Like `partialCompare`, but returns a [[cats.kernel.Comparison]] instead of an Double. + * Has the benefit of being able to pattern match on, but not as performant. + */ + def partialComparison(x: A, y: A): Option[Comparison] = + Comparison.fromDouble(partialCompare(x, y)) + /** * Result of comparing `x` with `y`. Returns None if operands are * not comparable. If operands are comparable, returns Some[Int] @@ -137,7 +143,6 @@ abstract class PartialOrderFunctions[P[T] <: PartialOrder[T]] extends EqFunction } object PartialOrder extends PartialOrderFunctions[PartialOrder] { - /** * Access an implicit `PartialOrder[A]`. */