From f71de9c5a169223f064553d6a493313f169e00e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Fromentin?= <42907886+Iltotore@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:18:40 +0200 Subject: [PATCH] feat: Polish Cats semigroups/monoids for refined types (#277) Closes #271 Closes #270 Closes #268 --- build.sc | 3 +- cats/src/io/github/iltotore/iron/Bounds.scala | 110 ++++++++++++++++ cats/src/io/github/iltotore/iron/cats.scala | 79 +----------- .../io/github/iltotore/iron/instances.scala | 120 ++++++++++++++++++ .../io/github/iltotore/iron/CatsSuite.scala | 71 +++++++++-- .../io/github/iltotore/iron/ShiftSuite.scala | 48 +++++++ 6 files changed, 343 insertions(+), 88 deletions(-) create mode 100644 cats/src/io/github/iltotore/iron/Bounds.scala create mode 100644 cats/src/io/github/iltotore/iron/instances.scala create mode 100644 cats/test/src/io/github/iltotore/iron/ShiftSuite.scala diff --git a/build.sc b/build.sc index fc5f0cb9..271da25e 100644 --- a/build.sc +++ b/build.sc @@ -286,7 +286,8 @@ object cats extends SubModule { def artifactName = "iron-cats" def ivyDeps = Agg( - ivy"org.typelevel::cats-core::2.8.0" + ivy"org.typelevel::cats-core::2.8.0", + ivy"org.typelevel::algebra::2.8.0" ) object test extends Tests { diff --git a/cats/src/io/github/iltotore/iron/Bounds.scala b/cats/src/io/github/iltotore/iron/Bounds.scala new file mode 100644 index 00000000..73f87050 --- /dev/null +++ b/cats/src/io/github/iltotore/iron/Bounds.scala @@ -0,0 +1,110 @@ +package io.github.iltotore.iron + +import io.github.iltotore.iron.constraint.numeric.Greater +import io.github.iltotore.iron.constraint.numeric.* +import scala.compiletime.constValue +import scala.math.Numeric.Implicits.infixNumericOps +import scala.math.Numeric.IntIsIntegral +import scala.math.Ordering.Implicits.infixOrderingOps + +/** + * A way to shift out-of-bounds [[A]] values constrained with [[C]] constraint. + * + * @tparam A the base type + * @tparam C the constraint type + */ +trait Bounds[A, C]: + + /** + * Shift value if out of bounds. + * + * @param value the value to eventually shift + * @return the passed value as is or shifted if necessary + */ + def shift(value: A): A :| C + +object Bounds: + + /** + * Bounds for interval [L, U]. + * + * @return + */ + inline given closedBounds[A, L <: A, U <: A, C](using + C ==> Interval.Closed[L, U], + Interval.Closed[L, U] ==> C, + Numeric[A] + ): Bounds[A, C] = + new: + override def shift(value: A): A :| C = + if value > constValue[U] then (value - constValue[U] + constValue[L] - summon[Numeric[A]].one).assume[C] + else if value < constValue[L] then ((constValue[U]: A) + value - constValue[L] + summon[Numeric[A]].one).assume[C] + else value.assume[C] + + //Positive + given posIntBounds[C](using C ==> Positive, Positive ==> C): Bounds[Int, C] = value => + if value <= 0 then (value + Int.MaxValue).assume[C] + else value.assume[C] + + given posLongBounds[C](using C ==> Positive, Positive ==> C): Bounds[Long, C] = value => + if value <= 0 then (value + Long.MaxValue).assume[Positive] + else value.assume[C] + + given posFloatBounds[C](using C ==> Positive, Positive ==> C): Bounds[Float, C] = value => + if value <= 0 then (value + Float.MaxValue).assume[C] + else value.assume[C] + + given posDoubleBounds[C](using C ==> Positive, Positive ==> C): Bounds[Double, C] = value => + if value <= 0 then (value + Double.MaxValue).assume[C] + else value.assume[C] + + //Positive0 + given pos0IntBounds[C](using C ==> Positive0, Positive0 ==> C): Bounds[Int, C] = value => + if value < 0 then (value + Int.MaxValue + 1).assume[C] + else value.assume[C] + + given pos0LongBounds[C](using C ==> Positive0, Positive0 ==> C): Bounds[Long, C] = value => + if value < 0 then (value + Long.MaxValue + 1).assume[C] + else value.assume[C] + + given pos0FloatBounds[C](using C ==> Positive0, Positive0 ==> C): Bounds[Float, C] = value => + if value < 0 then (value + Float.MaxValue + 1).assume[C] + else value.assume[C] + + given pos0DoubleBounds[C](using C ==> Positive0, Positive0 ==> C): Bounds[Double, C] = value => + if value < 0 then (value + Double.MaxValue + 1).assume[C] + else value.assume[C] + + //Negative + given negIntBounds[C](using C ==> Negative, Negative ==> C): Bounds[Int, C] = value => + if value >= 0 then (value + Int.MinValue).assume[C] + else value.assume[C] + + given negLongBounds[C](using C ==> Negative, Negative ==> C): Bounds[Long, C] = value => + if value >= 0 then (value + Long.MinValue).assume[C] + else value.assume[C] + + given negFloatBounds[C](using C ==> Negative, Negative ==> C): Bounds[Float, C] = value => + if value >= 0 then (value + Float.MinValue).assume[C] + else value.assume[C] + + given negDoubleBounds[C](using C ==> Negative, Negative ==> C): Bounds[Double, C] = value => + if value >= 0 then (value + Double.MinValue).assume[C] + else value.assume[C] + + //Negative + given neg0IntBounds[C](using C ==> Negative0, Negative0 ==> C): Bounds[Int, C] = value => + if value > 0 then (value + Int.MinValue - 1).assume[C] + else value.assume[C] + + given neg0LongBounds[C](using C ==> Negative0, Negative0 ==> C): Bounds[Long, C] = value => + if value > 0 then (value + Long.MinValue - 1).assume[C] + else value.assume[C] + + given neg0FloatBounds[C](using C ==> Negative0, Negative0 ==> C): Bounds[Float, C] = value => + if value > 0 then (value + Float.MinValue - 1).assume[C] + else value.assume[C] + + given neg0DoubleBounds[C](using C ==> Negative0, Negative0 ==> C): Bounds[Double, C] = value => + if value > 0 then (value + Double.MinValue - 1).assume[C] + else value.assume[C] \ No newline at end of file diff --git a/cats/src/io/github/iltotore/iron/cats.scala b/cats/src/io/github/iltotore/iron/cats.scala index 68873460..906c28b3 100644 --- a/cats/src/io/github/iltotore/iron/cats.scala +++ b/cats/src/io/github/iltotore/iron/cats.scala @@ -1,9 +1,7 @@ package io.github.iltotore.iron -import _root_.cats.data.* -import _root_.cats.kernel.{CommutativeMonoid, Hash, LowerBounded, PartialOrder, UpperBounded} +import _root_.cats.data.{EitherNec, EitherNel, Validated, ValidatedNec, ValidatedNel} import _root_.cats.syntax.either.* -import _root_.cats.{Eq, Monoid, Order, Show, Traverse} import _root_.cats.data.Validated.{Invalid, Valid} import _root_.cats.Functor import _root_.cats.implicits.* @@ -12,6 +10,7 @@ import io.github.iltotore.iron.constraint.numeric.{Greater, Less, Negative, Posi import scala.util.NotGiven import scala.util.boundary import scala.util.boundary.break +import _root_.cats.Traverse object cats extends IronCatsInstances: @@ -322,77 +321,3 @@ object cats extends IronCatsInstances: traverse.traverse(wrapper): value => Validated.condNel[InvalidValue[A], T](ops.rtc.test(value), ops.assume(value), InvalidValue(value, ops.rtc.message)) - /** - * Represent all Cats' typeclass instances for Iron. - */ -private trait IronCatsInstances extends IronCatsLowPriority, RefinedTypeOpsCats: - - given [F[_]](using functor: Functor[F]): MapLogic[F] with - - override def map[A, B](wrapper: F[A], f: A => B): F[B] = functor.map(wrapper)(f) - - // The `NotGiven` implicit parameter is mandatory to avoid ambiguous implicit error when both Eq[A] and Hash[A]/PartialOrder[A] exist - inline given [A, C](using inline ev: Eq[A], notHashOrOrder: NotGiven[Hash[A] | PartialOrder[A]]): Eq[A :| C] = ev.asInstanceOf[Eq[A :| C]] - - inline given [A, C](using inline ev: PartialOrder[A], notOrder: NotGiven[Order[A]]): PartialOrder[A :| C] = ev.asInstanceOf[PartialOrder[A :| C]] - - inline given [A, C](using inline ev: Order[A]): Order[A :| C] = ev.asInstanceOf[Order[A :| C]] - - inline given [A, C](using inline ev: Show[A]): Show[A :| C] = ev.asInstanceOf[Show[A :| C]] - - inline given [A, C, V](using inline ev: LowerBounded[A], implication: C ==> Greater[V]): LowerBounded[A :| C] = - ev.asInstanceOf[LowerBounded[A :| C]] - - inline given [A, C, V](using inline ev: UpperBounded[A], implication: C ==> Greater[V]): UpperBounded[A :| C] = - ev.asInstanceOf[UpperBounded[A :| C]] - - private def posMonoid[A, C](using ev: CommutativeMonoid[A], shift: PosShift[A], implication: C ==> Positive): CommutativeMonoid[A :| C] = - new CommutativeMonoid[A :| C]: - - override def empty: A :| C = ev.empty.asInstanceOf[A :| C] - - override def combine(a: A :| C, b: A :| C): A :| C = shift.shift(ev.combine(a, b)).asInstanceOf[A :| C] - - inline given posIntCommutativeMonoid[C](using C ==> Positive): CommutativeMonoid[Int :| C] = posMonoid - - inline given posLongCommutativeMonoid[C](using C ==> Positive): CommutativeMonoid[Long :| C] = posMonoid - - inline given posFloatCommutativeMonoid[C](using C ==> Positive): CommutativeMonoid[Float :| C] = posMonoid - - inline given posDoubleCommutativeMonoid[C](using C ==> Positive): CommutativeMonoid[Double :| C] = posMonoid - - private def negMonoid[A, C](using ev: CommutativeMonoid[A], shift: NegShift[A], implication: C ==> Negative): CommutativeMonoid[A :| C] = - new CommutativeMonoid[A :| C]: - - override def empty: A :| C = ev.empty.asInstanceOf[A :| C] - - override def combine(a: A :| C, b: A :| C): A :| C = shift.shift(ev.combine(a, b)).asInstanceOf[A :| C] - - inline given negIntCommutativeMonoid[C](using C ==> Negative): CommutativeMonoid[Int :| C] = negMonoid - - inline given negLongCommutativeMonoid[C](using C ==> Negative): CommutativeMonoid[Long :| C] = negMonoid - - inline given negFloatCommutativeMonoid[C](using C ==> Negative): CommutativeMonoid[Float :| C] = negMonoid - - inline given negDoubleCommutativeMonoid[C](using C ==> Negative): CommutativeMonoid[Double :| C] = negMonoid - -/** - * Cats' instances for Iron that need to have a lower priority to avoid ambiguous implicits. - */ -private trait IronCatsLowPriority: - - inline given [A, C](using inline ev: Hash[A]): Hash[A :| C] = ev.asInstanceOf[Hash[A :| C]] - -private trait RefinedTypeOpsCats extends RefinedTypeOpsCatsLowPriority: - - inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Eq[mirror.IronType]): Eq[T] = ev.asInstanceOf[Eq[T]] - - inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Order[mirror.IronType]): Order[T] = ev.asInstanceOf[Order[T]] - - inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Show[mirror.IronType]): Show[T] = ev.asInstanceOf[Show[T]] - - inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: PartialOrder[mirror.IronType]): PartialOrder[T] = ev.asInstanceOf[PartialOrder[T]] - -private trait RefinedTypeOpsCatsLowPriority: - - inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]] diff --git a/cats/src/io/github/iltotore/iron/instances.scala b/cats/src/io/github/iltotore/iron/instances.scala new file mode 100644 index 00000000..f98aa516 --- /dev/null +++ b/cats/src/io/github/iltotore/iron/instances.scala @@ -0,0 +1,120 @@ +package io.github.iltotore.iron + +import _root_.cats.kernel.{CommutativeMonoid, CommutativeSemigroup, Hash, LowerBounded, PartialOrder, UpperBounded} +import _root_.cats.{Eq, Monoid, Order, Show, Traverse} +import io.github.iltotore.iron.constraint.numeric.* +import scala.util.NotGiven +import _root_.cats.Functor +import algebra.instances.all.* +import algebra.ring.{AdditiveCommutativeMonoid, AdditiveCommutativeSemigroup, MultiplicativeGroup, MultiplicativeMonoid} + +/** + * Represent all Cats' typeclass instances for Iron. + */ +private[iron] trait IronCatsInstances extends IronCatsLowPriority, RefinedTypeOpsCats: + + given [F[_]](using functor: Functor[F]): MapLogic[F] with + + override def map[A, B](wrapper: F[A], f: A => B): F[B] = functor.map(wrapper)(f) + + // The `NotGiven` implicit parameter is mandatory to avoid ambiguous implicit error when both Eq[A] and Hash[A]/PartialOrder[A] exist + inline given [A, C](using inline ev: Eq[A], notHashOrOrder: NotGiven[Hash[A] | PartialOrder[A]]): Eq[A :| C] = ev.asInstanceOf[Eq[A :| C]] + + inline given [A, C](using inline ev: PartialOrder[A], notOrder: NotGiven[Order[A]]): PartialOrder[A :| C] = ev.asInstanceOf[PartialOrder[A :| C]] + + inline given [A, C](using inline ev: Order[A]): Order[A :| C] = ev.asInstanceOf[Order[A :| C]] + + inline given [A, C](using inline ev: Show[A]): Show[A :| C] = ev.asInstanceOf[Show[A :| C]] + + inline given [A, C, V](using inline ev: LowerBounded[A], implication: C ==> Greater[V]): LowerBounded[A :| C] = + ev.asInstanceOf[LowerBounded[A :| C]] + + inline given [A, C, V](using inline ev: UpperBounded[A], implication: C ==> Greater[V]): UpperBounded[A :| C] = + ev.asInstanceOf[UpperBounded[A :| C]] + + private def commutativeSemigroup[A, C](using inner: CommutativeSemigroup[A], bounds: Bounds[A, C]): CommutativeSemigroup[A :| C] = + new CommutativeSemigroup[A :| C]: + + override def combine(a: A :| C, b: A :| C): A :| C = bounds.shift(inner.combine(a, b)) + + given posIntCommutativeSemigroup: CommutativeSemigroup[Int :| Positive] = commutativeSemigroup[Int, Positive] + given posLongCommutativeSemigroup: CommutativeSemigroup[Long :| Positive] = commutativeSemigroup[Long, Positive] + given posFloatCommutativeSemigroup: CommutativeSemigroup[Float :| Positive] = commutativeSemigroup[Float, Positive] + given posDoubleCommutativeSemigroup: CommutativeSemigroup[Double :| Positive] = commutativeSemigroup[Double, Positive] + + given negIntCommutativeSemigroup: CommutativeSemigroup[Int :| Negative] = commutativeSemigroup[Int, Negative] + given negLongCommutativeSemigroup: CommutativeSemigroup[Long :| Negative] = commutativeSemigroup[Long, Negative] + given negFloatCommutativeSemigroup: CommutativeSemigroup[Float :| Negative] = commutativeSemigroup[Float, Negative] + given negDoubleCommutativeSemigroup: CommutativeSemigroup[Double :| Negative] = commutativeSemigroup[Double, Negative] + + private def commutativeMonoid[A, C](using inner: CommutativeMonoid[A], bounds: Bounds[A, C]): CommutativeMonoid[A :| C] = + new CommutativeMonoid[A :| C]: + + override def empty: A :| C = inner.empty.assume[C] + + override def combine(a: A :| C, b: A :| C): A :| C = bounds.shift(inner.combine(a, b)) + + given posIntCommutativeMonoid: CommutativeMonoid[Int :| Positive0] = commutativeMonoid[Int, Positive0] + given posLongCommutativeMonoid: CommutativeMonoid[Long :| Positive0] = commutativeMonoid[Long, Positive0] + given posFloatCommutativeMonoid: CommutativeMonoid[Float :| Positive0] = commutativeMonoid[Float, Positive0] + given posDoubleCommutativeMonoid: CommutativeMonoid[Double :| Positive0] = commutativeMonoid[Double, Positive0] + + given negIntCommutativeMonoid: CommutativeMonoid[Int :| Negative0] = commutativeMonoid[Int, Negative0] + given negLongCommutativeMonoid: CommutativeMonoid[Long :| Negative0] = commutativeMonoid[Long, Negative0] + given negFloatCommutativeMonoid: CommutativeMonoid[Float :| Negative0] = commutativeMonoid[Float, Negative0] + given negDoubleCommutativeMonoid: CommutativeMonoid[Double :| Negative0] = commutativeMonoid[Double, Negative0] + +/** + * Cats' instances for Iron that need to have a lower priority to avoid ambiguous implicits. + */ +private trait IronCatsLowPriority: + + inline given [A, C](using inline ev: Hash[A]): Hash[A :| C] = ev.asInstanceOf[Hash[A :| C]] + +private trait RefinedTypeOpsCats extends RefinedTypeOpsCatsLowPriority: + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Eq[mirror.IronType]): Eq[T] = ev.asInstanceOf[Eq[T]] + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Order[mirror.IronType]): Order[T] = ev.asInstanceOf[Order[T]] + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Show[mirror.IronType]): Show[T] = ev.asInstanceOf[Show[T]] + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: PartialOrder[mirror.IronType]): PartialOrder[T] = ev.asInstanceOf[PartialOrder[T]] + +private trait RefinedTypeOpsCatsLowPriority: + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]] + + private def additiveCommutativeSemigroup[A, C](using inner: AdditiveCommutativeSemigroup[A], bounds: Bounds[A, C]): AdditiveCommutativeSemigroup[A :| C] = (x, y) => + bounds.shift(inner.plus(x, y)) + + given posIntAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Int :| Positive] = additiveCommutativeSemigroup[Int, Positive] + given posLongAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Long :| Positive] = additiveCommutativeSemigroup[Long, Positive] + given posFloatAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Float :| Positive] = additiveCommutativeSemigroup[Float, Positive] + given posDoubleAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Double :| Positive] = additiveCommutativeSemigroup[Double, Positive] + + given negIntAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Int :| Negative] = additiveCommutativeSemigroup[Int, Negative] + given negLongAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Long :| Negative] = additiveCommutativeSemigroup[Long, Negative] + given negFloatAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Float :| Negative] = additiveCommutativeSemigroup[Float, Negative] + given negDoubleAdditiveCommutativeSemigroup: AdditiveCommutativeSemigroup[Double :| Negative] = additiveCommutativeSemigroup[Double, Negative] + + private def additiveCommutativeMonoid[A, C](using inner: AdditiveCommutativeMonoid[A], bounds: Bounds[A, C]): AdditiveCommutativeMonoid[A :| C] = new: + + override def zero: A :| C = inner.zero.assume[C] + override def plus(x: A :| C, y: A :| C): A :| C = bounds.shift(inner.plus(x, y)) + + given posIntAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Int :| Positive0] = additiveCommutativeMonoid[Int, Positive0] + given posLongAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Long :| Positive0] = additiveCommutativeMonoid[Long, Positive0] + given posFloatAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Float :| Positive0] = additiveCommutativeMonoid[Float, Positive0] + given posDoubleAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Double :| Positive0] = additiveCommutativeMonoid[Double, Positive0] + + given negIntAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Int :| Negative0] = additiveCommutativeMonoid[Int, Negative0] + given negLongAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Long :| Negative0] = additiveCommutativeMonoid[Long, Negative0] + given negFloatAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Float :| Negative0] = additiveCommutativeMonoid[Float, Negative0] + given negDoubleAdditiveCommutativeMonoid: AdditiveCommutativeMonoid[Double :| Negative0] = additiveCommutativeMonoid[Double, Negative0] + + given multiplicativeMonoid[A, C](using inner: MultiplicativeMonoid[A]): MultiplicativeMonoid[A :| C] = + inner.assumeAll[C] + + given multiplicativeGroup[A, C](using inner: MultiplicativeGroup[A]): MultiplicativeGroup[A :| C] = + inner.assumeAll[C] diff --git a/cats/test/src/io/github/iltotore/iron/CatsSuite.scala b/cats/test/src/io/github/iltotore/iron/CatsSuite.scala index ebb59c75..aa73e67c 100644 --- a/cats/test/src/io/github/iltotore/iron/CatsSuite.scala +++ b/cats/test/src/io/github/iltotore/iron/CatsSuite.scala @@ -13,6 +13,7 @@ import _root_.cats.data.NonEmptyList import _root_.cats.data.Validated.{Invalid, Valid} import scala.runtime.stdLibPatches.Predef.assert +import algebra.ring.{AdditiveCommutativeMonoid, AdditiveCommutativeSemigroup} object CatsSuite extends TestSuite: @@ -68,23 +69,73 @@ object CatsSuite extends TestSuite: Show[Person] test("alley") { - test("commutativeMonoid") { + test("commutativeSemigroup"): test("int"): - test("pos") - assert(CommutativeMonoid[Int :| Positive].combine(1, 5) == 6) - test("neg") - assert(CommutativeMonoid[Int :| Negative].combine(-1, -5) == -6) + test("pos") - assert(CommutativeSemigroup[Int :| Positive].combine(1, 5) == 6) + test("neg") - assert(CommutativeSemigroup[Int :| Negative].combine(-1, -5) == -6) test("long"): - test("pos") - assert(CommutativeMonoid[Long :| Positive].combine(1, 5) == 6) - test("neg") - assert(CommutativeMonoid[Long :| Negative].combine(-1, -5) == -6) + test("pos") - assert(CommutativeSemigroup[Long :| Positive].combine(1, 5) == 6) + test("neg") - assert(CommutativeSemigroup[Long :| Negative].combine(-1, -5) == -6) test("float"): - test("pos") - assert(CommutativeMonoid[Float :| Positive].combine(1, 5) == 6) - test("neg") - assert(CommutativeMonoid[Float :| Negative].combine(-1, -5) == -6) + test("pos") - assert(CommutativeSemigroup[Float :| Positive].combine(1, 5) == 6) + test("neg") - assert(CommutativeSemigroup[Float :| Negative].combine(-1, -5) == -6) test("double"): - test("pos") - assert(CommutativeMonoid[Double :| Positive].combine(1, 5) == 6) - test("neg") - assert(CommutativeMonoid[Double :| Negative].combine(-1, -5) == -6) - } + test("pos") - assert(CommutativeSemigroup[Double :| Positive].combine(1, 5) == 6) + test("neg") - assert(CommutativeSemigroup[Double :| Negative].combine(-1, -5) == -6) + + test("commutativeMonoid"): + test("int"): + test("pos") - assert(CommutativeMonoid[Int :| Positive0].combine(1, 5) == 6) + test("neg") - assert(CommutativeMonoid[Int :| Negative0].combine(-1, -5) == -6) + + test("long"): + test("pos") - assert(CommutativeMonoid[Long :| Positive0].combine(1, 5) == 6) + test("neg") - assert(CommutativeMonoid[Long :| Negative0].combine(-1, -5) == -6) + + test("float"): + test("pos") - assert(CommutativeMonoid[Float :| Positive0].combine(1, 5) == 6) + test("neg") - assert(CommutativeMonoid[Float :| Negative0].combine(-1, -5) == -6) + + test("double"): + test("pos") - assert(CommutativeMonoid[Double :| Positive0].combine(1, 5) == 6) + test("neg") - assert(CommutativeMonoid[Double :| Negative0].combine(-1, -5) == -6) + + test("additiveCommutativeSemigroup"): + test("int"): + test("pos") - assert(AdditiveCommutativeSemigroup[Int :| Positive].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeSemigroup[Int :| Negative].plus(-1, -5) == -6) + + test("long"): + test("pos") - assert(AdditiveCommutativeSemigroup[Long :| Positive].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeSemigroup[Long :| Negative].plus(-1, -5) == -6) + + test("float"): + test("pos") - assert(AdditiveCommutativeSemigroup[Float :| Positive].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeSemigroup[Float :| Negative].plus(-1, -5) == -6) + + test("double"): + test("pos") - assert(AdditiveCommutativeSemigroup[Double :| Positive].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeSemigroup[Double :| Negative].plus(-1, -5) == -6) + + test("additiveCommutativeMonoid"): + test("int"): + test("pos") - assert(AdditiveCommutativeMonoid[Int :| Positive0].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeMonoid[Int :| Negative0].plus(-1, -5) == -6) + + test("long"): + test("pos") - assert(AdditiveCommutativeMonoid[Long :| Positive0].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeMonoid[Long :| Negative0].plus(-1, -5) == -6) + + test("float"): + test("pos") - assert(AdditiveCommutativeMonoid[Float :| Positive0].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeMonoid[Float :| Negative0].plus(-1, -5) == -6) + + test("double"): + test("pos") - assert(AdditiveCommutativeMonoid[Double :| Positive0].plus(1, 5) == 6) + test("neg") - assert(AdditiveCommutativeMonoid[Double :| Negative0].plus(-1, -5) == -6) } test("eitherNec"): diff --git a/cats/test/src/io/github/iltotore/iron/ShiftSuite.scala b/cats/test/src/io/github/iltotore/iron/ShiftSuite.scala new file mode 100644 index 00000000..62e83b2c --- /dev/null +++ b/cats/test/src/io/github/iltotore/iron/ShiftSuite.scala @@ -0,0 +1,48 @@ +package io.github.iltotore.iron + +import utest.* +import scala.math.Numeric.IntIsIntegral +import io.github.iltotore.iron.constraint.numeric.* + +object ShiftSuite extends TestSuite: + + def assertShift[A, C](value: A, expected: A :| C)(using bounds: Bounds[A, C]): Unit = + assert(bounds.shift(value) == expected) + + val tests: Tests = Tests: + test("closed"): + test("inBounds") - assertShift[Int, Interval.Closed[0, 10]](5, 5) + test("upper") - assertShift[Int, Interval.Closed[0, 10]](11, 0) + test("lower") - assertShift[Int, Interval.Closed[0, 10]](-1, 10) + + test("positive"): + test("inBounds") - assertShift[Int, Positive](5, 5) + test("int") - assertShift[Int, Positive](0, Int.MaxValue) + test("long") - assertShift[Long, Positive](0, Long.MaxValue) + test("float") - assertShift[Float, Positive](0, Float.MaxValue) + test("double") - assertShift[Double, Positive](0, Double.MaxValue) + + test("positive0"): + test("inBounds"): + test - assertShift[Int, Positive0](5, 5) + test - assertShift[Int, Positive0](0, 0) + test("int") - assertShift[Int, Positive0](-1, Int.MaxValue) + test("long") - assertShift[Long, Positive0](-1, Long.MaxValue) + test("float") - assertShift[Float, Positive0](-1, Float.MaxValue) + test("double") - assertShift[Double, Positive0](-1, Double.MaxValue) + + test("negative"): + test("inBounds") - assertShift[Int, Negative](-5, -5) + test("int") - assertShift[Int, Negative](0, Int.MinValue) + test("long") - assertShift[Long, Negative](0, Long.MinValue) + test("float") - assertShift[Float, Negative](0, Float.MinValue) + test("double") - assertShift[Double, Negative](0, Double.MinValue) + + test("negative0"): + test("inBounds"): + test - assertShift[Int, Negative0](-5, -5) + test - assertShift[Int, Negative0](0, 0) + test("int") - assertShift[Int, Negative0](1, Int.MinValue) + test("long") - assertShift[Long, Negative0](1, Long.MinValue) + test("float") - assertShift[Float, Negative0](1, Float.MinValue) + test("double") - assertShift[Double, Negative0](1, Double.MinValue) \ No newline at end of file