diff --git a/laws/src/main/scala/cats/laws/discipline/MiniFloat.scala b/laws/src/main/scala/cats/laws/discipline/MiniFloat.scala new file mode 100644 index 0000000000..774ad3c595 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/MiniFloat.scala @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats +package laws +package discipline + +import cats.implicits.{catsSyntaxPartialOrder, toTraverseFilterOps} +import cats.kernel.{BoundedSemilattice, CommutativeMonoid} + +/** + * Similar to `Float`, but with a much smaller domain. The exact range of [[MiniFloat]] may be tuned from time to time, + * so consumers of this type should avoid depending on its exact range. + * + * `MiniFloat` has overflow and floating-point error characteristics similar to `Float`, but these are exaggerated due + * to its small domain. It is only approximately commutative and associative under addition and multiplication, due to + * floating-point errors, overflows, and the behaviour of `NaN`. + * + * Note that unlike `Float`, `MiniFloat` does not support the representation of negative zero (`-0f`). + */ +sealed abstract class MiniFloat private (val toFloat: Float) { + def toDouble: Double = toFloat.toDouble + def toInt: Int = toFloat.toInt + def toLong: Long = toFloat.toLong + + def +(that: MiniFloat): MiniFloat = MiniFloat.from(this.toFloat + that.toFloat) + def -(that: MiniFloat): MiniFloat = MiniFloat.from(this.toFloat - that.toFloat) + def *(that: MiniFloat): MiniFloat = MiniFloat.from(this.toFloat * that.toFloat) + def /(that: MiniFloat): MiniFloat = MiniFloat.from(this.toFloat / that.toFloat) + def unary_- : MiniFloat = MiniFloat.from(-this.toFloat) + + def isNaN: Boolean = toFloat.isNaN + def isFinite: Boolean = java.lang.Float.isFinite(toFloat) + + override def toString = s"MiniFloat($toFloat)" + + override def equals(other: Any): Boolean = other match { + case that: MiniFloat => this.toFloat == that.toFloat + case _ => false + } + + override def hashCode: Int = java.lang.Float.hashCode(toFloat) + +} + +object MiniFloat { + + object PositiveInfinity extends MiniFloat(Float.PositiveInfinity) + object NegativeInfinity extends MiniFloat(Float.NegativeInfinity) + object NaN extends MiniFloat(Float.NaN) + + final private class Finite private (significand: Int, exponent: Int) + extends MiniFloat(significand * math.pow(Finite.base.toDouble, exponent.toDouble).toFloat) + + private[MiniFloat] object Finite { + + private[MiniFloat] val base = 2 + + private val minSignificand = -2 + private val maxSignificand = 2 + + private val minExponent = -1 + private val maxExponent = 2 + + val allValues: List[Finite] = { + for { + significand <- Range.inclusive(minSignificand, maxSignificand) + exponent <- Range.inclusive(minExponent, maxExponent) + } yield new Finite(significand, exponent) + }.toList.ordDistinct(Order.by[Finite, Float](_.toFloat)).sortBy(_.toFloat) + + private[MiniFloat] val allValuesAsFloats: List[Float] = allValues.map(_.toFloat) + + val zero = new Finite(0, 0) + val max = new Finite(maxSignificand, maxExponent) + val min = new Finite(minSignificand, maxExponent) + val minPositive = new Finite(significand = 1, exponent = minExponent) + + /** + * Returns `None` if the given float cannot fit in an instance of `Finite`. + */ + def from(float: Float): Option[Finite] = { + val exponent: Int = getExponent(float) + val significand: Int = math.round(float / math.pow(Finite.base.toDouble, exponent.toDouble).toFloat) + + if (significand == 0 || exponent < minExponent) { + Some(zero) + } else if (withinBounds(significand, exponent)) { + Some(new Finite(significand, exponent)) + } else if (exponent > maxExponent) { + try { + val ordersOfMagnitudeToShift = Math.subtractExact(exponent, maxExponent) + + val proposedSignificand: Int = Math.multiplyExact( + significand, + math.pow(base.toDouble, ordersOfMagnitudeToShift.toDouble).toInt + ) + val proposedExponent: Int = Math.subtractExact(exponent, ordersOfMagnitudeToShift) + + if (withinBounds(proposedSignificand, proposedExponent)) { + Some(new Finite(proposedSignificand, proposedExponent)) + } else { + None + } + } catch { + case _: ArithmeticException => None + } + } else { + None + } + } + + private def withinBounds(significand: Int, exponent: Int): Boolean = + (minExponent <= exponent && exponent <= maxExponent) && + (minSignificand <= significand && significand <= maxSignificand) + + private val floatExponentStartBit: Int = 23 + private val floatExponentLength: Int = 8 + private val floatExponentBias: Int = 127 + private val floatExponentMask: Int = ((1 << floatExponentLength) - 1) << floatExponentStartBit + + // This does the same thing as java.lang.Math.getExponent, but that method is not available in scalaJS so we have to + // do the same thing here. + private def getExponent(float: Float): Int = + ((floatExponentMask & java.lang.Float.floatToIntBits(float)) >> floatExponentStartBit) - floatExponentBias + + } + + val Zero: MiniFloat = Finite.zero + val NegativeOne: MiniFloat = MiniFloat.from(-1f) + val One: MiniFloat = MiniFloat.from(1f) + + val MaxValue: MiniFloat = Finite.max + val MinValue: MiniFloat = Finite.min + val MinPositiveValue: MiniFloat = Finite.minPositive + + def allValues: List[MiniFloat] = + List(NegativeInfinity) ++ + Finite.allValues :+ + PositiveInfinity :+ + NaN + + def from(float: Float): MiniFloat = + float match { + case Float.PositiveInfinity => PositiveInfinity + case Float.NegativeInfinity => NegativeInfinity + case f if f.isNaN => NaN + case _ => + Finite + .from(float) + .getOrElse { + if (float > 0) PositiveInfinity else NegativeInfinity + } + } + + def from(double: Double): MiniFloat = from(double.toFloat) + def from(int: Int): MiniFloat = from(int.toFloat) + def from(long: Long): MiniFloat = from(long.toFloat) + + /** + * Note that since `MiniFloat` is used primarily for tests, this `Eq` instance defines `NaN` as equal to itself. This + * differs from the `Order` defined for `Float`. + */ + implicit val catsLawsEqInstancesForMiniFloat: Order[MiniFloat] with Hash[MiniFloat] = + new Order[MiniFloat] with Hash[MiniFloat] { + override def compare(x: MiniFloat, y: MiniFloat): Int = Order[Float].compare(x.toFloat, y.toFloat) + override def hash(x: MiniFloat): Int = Hash[Float].hash(x.toFloat) + } + + implicit val catsLawsExhaustiveCheckForMiniInt: ExhaustiveCheck[MiniFloat] = new ExhaustiveCheck[MiniFloat] { + override def allValues: List[MiniFloat] = MiniFloat.allValues + } + + val miniFloatMax: CommutativeMonoid[MiniFloat] with BoundedSemilattice[MiniFloat] = + new CommutativeMonoid[MiniFloat] with BoundedSemilattice[MiniFloat] { + override def empty: MiniFloat = MiniFloat.NegativeInfinity + override def combine(x: MiniFloat, y: MiniFloat): MiniFloat = if (x > y) x else y + } + +} diff --git a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala index c95122856c..08ce6160e2 100644 --- a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala @@ -415,6 +415,13 @@ object arbitrary extends ArbitraryInstances0 with ScalaVersionSpecific.Arbitrary implicit val catsLawsArbitraryForMiniInt: Arbitrary[MiniInt] = Arbitrary(Gen.oneOf(MiniInt.allValues)) + + implicit val catsLawsCogenForMiniFloat: Cogen[MiniFloat] = + Cogen[Float].contramap(_.toFloat) + + implicit val catsLawsArbitraryForMiniFloat: Arbitrary[MiniFloat] = + Arbitrary(Gen.oneOf(MiniFloat.allValues)) + } sealed private[discipline] trait ArbitraryInstances0 { diff --git a/tests/shared/src/test/scala-2.12/cats/tests/ScalaVersionSpecific.scala b/tests/shared/src/test/scala-2.12/cats/tests/ScalaVersionSpecific.scala index 62b1a1346b..a70dddf4fd 100644 --- a/tests/shared/src/test/scala-2.12/cats/tests/ScalaVersionSpecific.scala +++ b/tests/shared/src/test/scala-2.12/cats/tests/ScalaVersionSpecific.scala @@ -24,9 +24,8 @@ package cats.tests import cats.kernel.{Eq, Order} import cats.laws.discipline.{ExhaustiveCheck, MiniInt} import cats.laws.discipline.MiniInt._ +import cats.laws.discipline.MiniFloat import cats.laws.discipline.eq._ -import cats.laws.discipline.DeprecatedEqInstances -import org.scalacheck.Arbitrary trait ScalaVersionSpecificFoldableSuite trait ScalaVersionSpecificParallelSuite @@ -51,23 +50,43 @@ trait ScalaVersionSpecificAlgebraInvariantSuite { } // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class - implicit protected def eqNumeric[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.by { numeric => - // This allows us to catch the case where the fromInt overflows. We use the None to compare two Numeric instances, - // verifying that when fromInt throws for one, it throws for the other. - val fromMiniInt: MiniInt => Option[A] = - miniInt => - try Some(numeric.fromInt(miniInt.toInt)) - catch { - case _: IllegalArgumentException => None // MiniInt overflow - } + protected val fractionalForMiniFloat: Fractional[MiniFloat] = new Fractional[MiniFloat] { + def compare(x: MiniFloat, y: MiniFloat): Int = Order[MiniFloat].compare(x, y) + def plus(x: MiniFloat, y: MiniFloat): MiniFloat = x + y + def minus(x: MiniFloat, y: MiniFloat): MiniFloat = x + (-y) + def times(x: MiniFloat, y: MiniFloat): MiniFloat = x * y + def div(x: MiniFloat, y: MiniFloat): MiniFloat = x / y + def negate(x: MiniFloat): MiniFloat = -x + def fromInt(x: Int): MiniFloat = MiniFloat.from(x) + def toInt(x: MiniFloat): Int = x.toInt + def toLong(x: MiniFloat): Long = x.toInt.toLong + def toFloat(x: MiniFloat): Float = x.toInt.toFloat + def toDouble(x: MiniFloat): Double = x.toInt.toDouble + } + /** + * Emulates the behaviour of `Numeric#fromInt`, but using MiniInt as the input. This allows us to exercise the + * implementation of `fromInt` for an instance of `Numeric` while still taking advantage of the `ExhaustiveCheck` + * instance for `MiniInt`. + * + * Note that this will return `None` when `fromInt` overflows. We can use this to compare two `Numeric` instances, + * verifying that when `fromInt` throws for one, it throws for the other. + */ + private def numericFromMiniInt[A](miniInt: MiniInt, numeric: Numeric[A]): Option[A] = + try Some(numeric.fromInt(miniInt.toInt)) + catch { + case _: IllegalArgumentException => None // MiniInt overflow + } + + // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class + implicit protected def eqNumeric[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.by { numeric => ( numeric.compare _, numeric.plus _, numeric.minus _, numeric.times _, numeric.negate _, - fromMiniInt, + numericFromMiniInt[A](_, numeric), numeric.toInt _, numeric.toLong _, numeric.toFloat _, @@ -75,24 +94,4 @@ trait ScalaVersionSpecificAlgebraInvariantSuite { ) } - // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class - @annotation.nowarn("cat=deprecation") - implicit protected def eqFractional[A: Eq: Arbitrary]: Eq[Fractional[A]] = { - import DeprecatedEqInstances.catsLawsEqForFn1 - - Eq.by { fractional => - ( - fractional.compare _, - fractional.plus _, - fractional.minus _, - fractional.times _, - fractional.negate _, - fractional.fromInt _, - fractional.toInt _, - fractional.toLong _, - fractional.toFloat _, - fractional.toDouble _ - ) - } - } } diff --git a/tests/shared/src/test/scala-2.13+/cats/tests/ScalaVersionSpecific.scala b/tests/shared/src/test/scala-2.13+/cats/tests/ScalaVersionSpecific.scala index 52ce3cc375..b1e5e583aa 100644 --- a/tests/shared/src/test/scala-2.13+/cats/tests/ScalaVersionSpecific.scala +++ b/tests/shared/src/test/scala-2.13+/cats/tests/ScalaVersionSpecific.scala @@ -23,13 +23,9 @@ package cats.tests import cats._ import cats.data.NonEmptyLazyList -import cats.laws.discipline.DeprecatedEqInstances -import cats.laws.discipline.ExhaustiveCheck -import cats.laws.discipline.MiniInt -import cats.laws.discipline.NonEmptyParallelTests -import cats.laws.discipline.ParallelTests -import cats.laws.discipline.arbitrary._ +import cats.laws.discipline.{ExhaustiveCheck, MiniFloat, MiniInt, NonEmptyParallelTests, ParallelTests} import cats.laws.discipline.eq._ +import cats.laws.discipline.arbitrary._ import cats.syntax.all._ import org.scalacheck.Arbitrary import org.scalacheck.Prop._ @@ -208,16 +204,37 @@ trait ScalaVersionSpecificAlgebraInvariantSuite { } // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class - implicit protected def eqNumeric[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.by { numeric => - // This allows us to catch the case where the fromInt overflows. We use the None to compare two Numeric instances, - // verifying that when fromInt throws for one, it throws for the other. - val fromMiniInt: MiniInt => Option[A] = - miniInt => - try Some(numeric.fromInt(miniInt.toInt)) - catch { - case _: IllegalArgumentException => None // MiniInt overflow - } + protected val fractionalForMiniFloat: Fractional[MiniFloat] = new Fractional[MiniFloat] { + def compare(x: MiniFloat, y: MiniFloat): Int = Order[MiniFloat].compare(x, y) + def plus(x: MiniFloat, y: MiniFloat): MiniFloat = x + y + def minus(x: MiniFloat, y: MiniFloat): MiniFloat = x + (-y) + def times(x: MiniFloat, y: MiniFloat): MiniFloat = x * y + def div(x: MiniFloat, y: MiniFloat): MiniFloat = x / y + def negate(x: MiniFloat): MiniFloat = -x + def fromInt(x: Int): MiniFloat = MiniFloat.from(x) + def toInt(x: MiniFloat): Int = x.toInt + def toLong(x: MiniFloat): Long = x.toInt.toLong + def toFloat(x: MiniFloat): Float = x.toInt.toFloat + def toDouble(x: MiniFloat): Double = x.toInt.toDouble + def parseString(str: String): Option[MiniFloat] = Fractional[Float].parseString(str).map(MiniFloat.from(_)) + } + + /** + * Emulates the behaviour of `Numeric#fromInt`, but using MiniInt as the input. This allows us to exercise the + * implementation of `fromInt` for an instance of `Numeric` while still taking advantage of the `ExhaustiveCheck` + * instance for `MiniInt`. + * + * Note that this will return `None` when `fromInt` overflows. We can use this to compare two `Numeric` instances, + * verifying that when `fromInt` throws for one, it throws for the other. + */ + private def numericFromMiniInt[A](miniInt: MiniInt, numeric: Numeric[A]): Option[A] = + try Some(numeric.fromInt(miniInt.toInt)) + catch { + case _: IllegalArgumentException => None // MiniInt overflow + } + // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class + implicit protected def eqNumeric[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.by { numeric => val parseMiniIntStrings: Option[MiniInt] => Option[A] = { case Some(miniInt) => numeric.parseString(miniInt.toInt.toString) case None => numeric.parseString("invalid") // Use this to test parsing of non-numeric strings @@ -229,7 +246,7 @@ trait ScalaVersionSpecificAlgebraInvariantSuite { numeric.minus _, numeric.times _, numeric.negate _, - fromMiniInt, + numericFromMiniInt[A](_, numeric), numeric.toInt _, numeric.toLong _, numeric.toFloat _, @@ -238,35 +255,6 @@ trait ScalaVersionSpecificAlgebraInvariantSuite { ) } - // This version-specific instance is required since 2.12 and below do not have parseString on the Numeric class - @annotation.nowarn("cat=deprecation") - implicit protected def eqFractional[A: Eq: Arbitrary]: Eq[Fractional[A]] = { - // This deprecated instance is required since there is not `ExhaustiveCheck` for any types for which a `Fractional` - // can easily be defined - import DeprecatedEqInstances.catsLawsEqForFn1 - - Eq.by { fractional => - val parseFloatStrings: Option[Double] => Option[A] = { - case Some(f) => fractional.parseString(f.toString) - case None => fractional.parseString("invalid") // Use this to test parsing of non-numeric strings - } - - ( - fractional.compare _, - fractional.plus _, - fractional.minus _, - fractional.times _, - fractional.negate _, - fractional.fromInt _, - fractional.toInt _, - fractional.toLong _, - fractional.toFloat _, - fractional.toDouble _, - parseFloatStrings - ) - } - } - } class TraverseLazyListSuite extends TraverseSuite[LazyList]("LazyList") diff --git a/tests/shared/src/test/scala/cats/tests/AlgebraInvariantSuite.scala b/tests/shared/src/test/scala/cats/tests/AlgebraInvariantSuite.scala index ea083a372f..f0a3dc4515 100644 --- a/tests/shared/src/test/scala/cats/tests/AlgebraInvariantSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/AlgebraInvariantSuite.scala @@ -29,6 +29,7 @@ import cats.laws.discipline.{ InvariantMonoidalTests, InvariantSemigroupalTests, InvariantTests, + MiniFloat, MiniInt, SerializableTests } @@ -181,9 +182,8 @@ class AlgebraInvariantSuite extends CatsSuite with ScalaVersionSpecificAlgebraIn implicit private val arbNumericMiniInt: Arbitrary[Numeric[MiniInt]] = Arbitrary(Gen.const(integralForMiniInt)) implicit private val arbIntegralMiniInt: Arbitrary[Integral[MiniInt]] = Arbitrary(Gen.const(integralForMiniInt)) - implicit private val arbFractionalFloat: Arbitrary[Fractional[Float]] = Arbitrary( - Gen.const(implicitly[Fractional[Float]]) - ) + implicit private val arbFractionalFloat: Arbitrary[Fractional[MiniFloat]] = + Arbitrary(Gen.const(fractionalForMiniFloat)) implicit protected def eqIntegral[A: Eq: ExhaustiveCheck]: Eq[Integral[A]] = { def makeDivisionOpSafe(unsafeF: (A, A) => A): (A, A) => Option[A] = @@ -209,6 +209,14 @@ class AlgebraInvariantSuite extends CatsSuite with ScalaVersionSpecificAlgebraIn } } + implicit protected def eqFractional[A: Eq: ExhaustiveCheck]: Eq[Fractional[A]] = + Eq.by { fractional => + ( + fractional: Numeric[A], + fractional.div(_, _) + ) + } + checkAll("InvariantMonoidal[Semigroup]", SemigroupTests[Int](InvariantMonoidal[Semigroup].point(0)).semigroup) checkAll("InvariantMonoidal[CommutativeSemigroup]", CommutativeSemigroupTests[Int](InvariantMonoidal[CommutativeSemigroup].point(0)).commutativeSemigroup @@ -220,7 +228,7 @@ class AlgebraInvariantSuite extends CatsSuite with ScalaVersionSpecificAlgebraIn checkAll("Invariant[Numeric]", InvariantTests[Numeric].invariant[MiniInt, Boolean, Boolean]) checkAll("Invariant[Integral]", InvariantTests[Integral].invariant[MiniInt, Boolean, Boolean]) - checkAll("Invariant[Fractional]", InvariantTests[Fractional].invariant[Float, Boolean, Boolean]) + checkAll("Invariant[Fractional]", InvariantTests[Fractional].invariant[MiniFloat, Boolean, Boolean]) { val S: Semigroup[Int] = Semigroup[Int].imap(identity)(identity) diff --git a/tests/shared/src/test/scala/cats/tests/MiniFloatSuite.scala b/tests/shared/src/test/scala/cats/tests/MiniFloatSuite.scala new file mode 100644 index 0000000000..f9266e4bc0 --- /dev/null +++ b/tests/shared/src/test/scala/cats/tests/MiniFloatSuite.scala @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.tests + +import cats.kernel.laws.discipline.{ + BoundedSemilatticeTests, + CommutativeMonoidTests, + HashTests, + OrderTests, + SerializableTests +} +import cats.kernel.{BoundedSemilattice, CommutativeMonoid, Hash, Order} +import cats.laws.discipline.MiniFloat +import cats.laws.discipline.MiniFloat._ +import cats.laws.discipline.arbitrary._ +import cats.syntax.eq._ +import cats.tests.MiniFloatSuite.FloatArithmeticOp +import munit.Location +import org.scalacheck.Prop._ +import org.scalacheck.{Arbitrary, Gen} + +class MiniFloatSuite extends CatsSuite { + + // checks on allValues + + test("allValues contains no duplicates") { + val occurrencesPerFloatValue = MiniFloat.allValues + .groupBy(_.toFloat) + .view + + val duplicates: List[(Float, List[MiniFloat])] = occurrencesPerFloatValue.toList.filter(_._2.size > 1) + + assert(duplicates.isEmpty, s"Minifloats with duplicate values $duplicates") + } + + private def testAllValuesContains(value: MiniFloat)(implicit loc: Location): Unit = + test(s"allValues contains $value") { + MiniFloat.allValues.contains(value) + } + + testAllValuesContains(MiniFloat.Zero) + testAllValuesContains(MiniFloat.NegativeOne) + testAllValuesContains(MiniFloat.One) + testAllValuesContains(MiniFloat.MaxValue) + testAllValuesContains(MiniFloat.MinValue) + testAllValuesContains(MiniFloat.MinPositiveValue) + testAllValuesContains(MiniFloat.PositiveInfinity) + testAllValuesContains(MiniFloat.NegativeInfinity) + testAllValuesContains(MiniFloat.NaN) + + test("allValues has max") { + val maxInAllValues = MiniFloat.allValues.filter(_.isFinite).maxBy(_.toFloat) + + assert(maxInAllValues === MiniFloat.MaxValue) + } + + test("allValues has min") { + val minInAllValues = MiniFloat.allValues.filter(_.isFinite).minBy(_.toFloat) + + assert(minInAllValues === MiniFloat.MinValue) + } + + test("behaves the same as Float for all operations on some key values") { + val genKeyMiniFloatValues: Gen[MiniFloat] = Gen.oneOf( + MiniFloat.NegativeInfinity, + MiniFloat.NegativeOne, + MiniFloat.Zero, + MiniFloat.One, + MiniFloat.PositiveInfinity, + MiniFloat.NaN + ) + + forAll(FloatArithmeticOp.arb.arbitrary, genKeyMiniFloatValues, genKeyMiniFloatValues) { + (op: FloatArithmeticOp, left: MiniFloat, right: MiniFloat) => + val testHint = s"$left ${op.char} $right" + val expectedAsFloat: Float = op.forFloat(left.toFloat, right.toFloat) + + // This is necessary since Float.NaN != Float.NaN + if (expectedAsFloat.isNaN) { + assert(op.forMiniFloat(left, right).isNaN, testHint) + } else { + assert(op.forMiniFloat(left, right).toFloat === expectedAsFloat, testHint) + } + } + } + + // Float conversions + + private def testFloatConversion(float: Float, expected: MiniFloat)(implicit loc: Location): Unit = + test(s"MiniFloat.fromFloat($float) === $expected") { + assert(MiniFloat.from(float) === expected) + } + + private def testFloatConversion(float: Float, expected: Float)(implicit loc: Location): Unit = + test(s"MiniFloat.fromFloat($float) === $expected") { + val miniFloat = MiniFloat.from(float) + + // This is necessary since Float.NaN != Float.NaN + if (miniFloat.isNaN) { + assert(expected.isNaN) + } else { + assert(miniFloat.toFloat === expected) + } + } + + testFloatConversion(Float.NegativeInfinity, MiniFloat.NegativeInfinity) + testFloatConversion(Float.MinValue, MiniFloat.NegativeInfinity) + testFloatConversion(-16f, MiniFloat.NegativeInfinity) + testFloatConversion(Math.nextDown(-12f), MiniFloat.NegativeInfinity) + testFloatConversion(-12f, -8f) + testFloatConversion(Math.nextDown(-6f), -8f) + testFloatConversion(-6f, -4f) + testFloatConversion(Math.nextDown(-3f), -4f) + testFloatConversion(-3f, -2f) + testFloatConversion(-1f, MiniFloat.NegativeOne) + testFloatConversion(1f, MiniFloat.One) + testFloatConversion(0f, MiniFloat.Zero) + testFloatConversion(Float.MinPositiveValue, MiniFloat.Zero) + testFloatConversion(-0f, MiniFloat.Zero) + testFloatConversion(Math.nextDown(0.125f), MiniFloat.Zero) + testFloatConversion(0.125f, MiniFloat.Zero) + testFloatConversion(Math.nextDown(0.25f), MiniFloat.Zero) + testFloatConversion(0.25f, MiniFloat.Zero) + testFloatConversion(0.5f, 0.5f) + testFloatConversion(1f, 1f) + testFloatConversion(2f, 2f) + testFloatConversion(Math.nextDown(3f), 2f) + testFloatConversion(3f, 4f) + testFloatConversion(4f, 4f) + testFloatConversion(5f, 4f) + testFloatConversion(6f, 8f) + testFloatConversion(7f, 8f) + testFloatConversion(8f, 8f) + testFloatConversion(Math.nextDown(12f), 8f) + testFloatConversion(12f, MiniFloat.PositiveInfinity) + testFloatConversion(16f, MiniFloat.PositiveInfinity) + testFloatConversion(Float.MaxValue, MiniFloat.PositiveInfinity) + testFloatConversion(Float.PositiveInfinity, MiniFloat.PositiveInfinity) + testFloatConversion(Float.NaN, MiniFloat.NaN) + + test("MiniFloat.fromFloat(-0) is not negative") { + assert(!(MiniFloat.from(-0).toFloat < 0)) + } + + test(s"fromDouble consistent with fromFloat") { + forAll { (n: Double) => + assert(MiniFloat.from(n) === MiniFloat.from(n.toFloat), n) + } + } + + test(s"toDouble consistent with toFloat") { + forAll { (mf: MiniFloat) => + val fromToDouble = mf.toDouble.toFloat + val fromToFloat = mf.toFloat + + // This is necessary since Float.NaN != Float.NaN + if (fromToDouble.isNaN) { + assert(fromToFloat.isNaN) + } else { + assert(fromToDouble === fromToFloat, mf) + } + } + } + + // Special number tests and behaviours + + private def testSpecialNumberExpectations( + mf: MiniFloat, + expectIsNaN: Boolean, + expectIsFinite: Boolean + )(implicit + loc: Location + ): Unit = { + test(s"$mf isNan $expectIsNaN") { + assert(mf.isNaN === expectIsNaN) + } + + test(s"$mf isFinite $expectIsFinite") { + assert(mf.isFinite === expectIsFinite) + } + } + + testSpecialNumberExpectations(MiniFloat.NaN, expectIsNaN = true, expectIsFinite = false) + testSpecialNumberExpectations(MiniFloat.PositiveInfinity, expectIsNaN = false, expectIsFinite = false) + testSpecialNumberExpectations(MiniFloat.NegativeInfinity, expectIsNaN = false, expectIsFinite = false) + testSpecialNumberExpectations(MiniFloat.Zero, expectIsNaN = false, expectIsFinite = true) + testSpecialNumberExpectations(MiniFloat.MaxValue, expectIsNaN = false, expectIsFinite = true) + testSpecialNumberExpectations(MiniFloat.MinValue, expectIsNaN = false, expectIsFinite = true) + testSpecialNumberExpectations(MiniFloat.MinPositiveValue, expectIsNaN = false, expectIsFinite = true) + + // Negation + + test("negate zero is zero") { + assert(-MiniFloat.Zero === MiniFloat.Zero) + } + + test("negate one is one") { + assert(-MiniFloat.One === MiniFloat.NegativeOne) + } + + test("negate ∞ is -∞") { + assert(-MiniFloat.PositiveInfinity === MiniFloat.NegativeInfinity) + } + + test("negate -∞ is ∞") { + assert(-MiniFloat.NegativeInfinity === MiniFloat.PositiveInfinity) + } + + test("negate NaN is NaN") { + assert((-MiniFloat.NaN).isNaN) + } + + test("negation inverse") { + forAll { (mf: MiniFloat) => + assert((-(-mf) === mf) || mf.isNaN) + } + } + + // Addition + + test("add commutative") { + forAll { (left: MiniFloat, right: MiniFloat) => + assert(left + right === right + left) + } + } + + test("zero addition identity") { + forAll { (mf: MiniFloat) => + assert(mf + MiniFloat.Zero === mf) + } + } + + test("max plus 1") { + assert(MiniFloat.MaxValue + MiniFloat.One === MiniFloat.MaxValue) + } + + test("max plus max") { + assert(MiniFloat.MaxValue + MiniFloat.MaxValue === MiniFloat.PositiveInfinity) + } + + test("∞ + ∞") { + assert(MiniFloat.PositiveInfinity + MiniFloat.PositiveInfinity === MiniFloat.PositiveInfinity) + } + + test("∞ + 1") { + assert(MiniFloat.PositiveInfinity + MiniFloat.One === MiniFloat.PositiveInfinity) + } + + test("∞ + (-∞)") { + assert((MiniFloat.PositiveInfinity + MiniFloat.NegativeInfinity).isNaN) + } + + test("NaN addition") { + forAll { (mf: MiniFloat) => + assert((mf + MiniFloat.NaN).isNaN) + } + } + + // Subtraction + + test("subtract consistent with addition") { + forAll { (left: MiniFloat, right: MiniFloat) => + assert((left + (-right)) === left - right) + } + } + + test("NaN subtraction") { + forAll { (mf: MiniFloat) => + assert((mf - MiniFloat.NaN).isNaN) + } + } + + test("∞ - ∞") { + assert((MiniFloat.PositiveInfinity - MiniFloat.PositiveInfinity).isNaN) + } + + // Multiplication + + test("multiplication commutative") { + forAll { (left: MiniFloat, right: MiniFloat) => + assert(left * right === right * left) + } + } + + test("one multiplicative identity") { + forAll { (mf: MiniFloat) => + assert(mf * MiniFloat.One === mf) + } + } + + test("max * 1") { + assert(MiniFloat.MaxValue * MiniFloat.One === MiniFloat.MaxValue) + } + + test("max * max") { + assert(MiniFloat.MaxValue * MiniFloat.MaxValue === MiniFloat.PositiveInfinity) + } + + test("∞ * ∞") { + assert(MiniFloat.PositiveInfinity * MiniFloat.PositiveInfinity === MiniFloat.PositiveInfinity) + } + + test("∞ * 1") { + assert(MiniFloat.PositiveInfinity * MiniFloat.One === MiniFloat.PositiveInfinity) + } + + test("∞ * (-∞)") { + assert(MiniFloat.PositiveInfinity * MiniFloat.NegativeInfinity === MiniFloat.NegativeInfinity) + } + + test("NaN multiplication") { + forAll { (mf: MiniFloat) => + assert((mf * MiniFloat.NaN).isNaN) + } + } + + // Division + + test("divide by zero") { + forAll { (mf: MiniFloat) => + val result = mf / MiniFloat.Zero + + if (mf.isNaN || mf === MiniFloat.Zero) { + assert(result.isNaN) + } else if (mf.toFloat < 0f) { + assert(result === MiniFloat.NegativeInfinity) + } else { + assert(result === MiniFloat.PositiveInfinity) + } + } + } + + test("division consistent with float division") { + forAll { (left: MiniFloat, right: MiniFloat) => + val result = left / right + + if (result.isNaN) { + assert(MiniFloat.from(left.toFloat / right.toFloat).isNaN) + } else { + assert(result === MiniFloat.from(left.toFloat / right.toFloat)) + } + } + } + + // Order + + test("ordering consistent with float") { + forAll { (left: MiniFloat, right: MiniFloat) => + assert( + implicitly[Order[MiniFloat]].compare(left, right) === + implicitly[Order[Float]].compare(left.toFloat, right.toFloat) + ) + } + } + + test("float roundtrip") { + forAll { (f: MiniFloat) => + assert(MiniFloat.from(f.toFloat) === f) + } + } + + checkAll("MiniFloat", OrderTests[MiniFloat].order) + checkAll("Order[MiniFloat]", SerializableTests.serializable(Order[MiniFloat])) + + checkAll("MiniFloat", HashTests[MiniFloat].hash) + checkAll("Hash[MiniFloat]", SerializableTests.serializable(Hash[MiniFloat])) + + // This specific case has failed for ScalaNative on the CI. Testing specifically to ensure we don't miss a flaky test. + test("NaN hash is consistent with universal hash") { + HashTests[MiniFloat].laws.sameAsUniversalHash(MiniFloat.NaN, MiniFloat.NaN) + } + + { + implicit val m: CommutativeMonoid[MiniFloat] with BoundedSemilattice[MiniFloat] = miniFloatMax + checkAll("CommutativeMonoid[MiniFloat] maximum", CommutativeMonoidTests[MiniFloat].commutativeMonoid) + checkAll("BoundedSemilatticeMonoid[MiniFloat] maximum", BoundedSemilatticeTests[MiniFloat].boundedSemilattice) + checkAll("CommutativeMonoid[MiniFloat] maximum", SerializableTests.serializable(miniFloatMax)) + } + +} + +object MiniFloatSuite { + + sealed abstract private class FloatArithmeticOp( + val char: Char, + val forMiniFloat: (MiniFloat, MiniFloat) => MiniFloat, + val forFloat: (Float, Float) => Float + ) + + private object FloatArithmeticOp { + case object Addition extends FloatArithmeticOp('+', _ + _, _ + _) + case object Multiplication extends FloatArithmeticOp('*', _ * _, _ * _) + case object Subtraction extends FloatArithmeticOp('-', _ - _, _ - _) + case object Division extends FloatArithmeticOp('/', _ / _, _ / _) + + implicit val arb: Arbitrary[FloatArithmeticOp] = + Arbitrary(Gen.oneOf(Addition, Multiplication, Subtraction, Division)) + } + +} diff --git a/tests/shared/src/test/scala/cats/tests/MiniIntSuite.scala b/tests/shared/src/test/scala/cats/tests/MiniIntSuite.scala index 274992e72a..a10e164caf 100644 --- a/tests/shared/src/test/scala/cats/tests/MiniIntSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/MiniIntSuite.scala @@ -45,7 +45,7 @@ class MiniIntSuite extends CatsSuite { { implicit val m: CommutativeMonoid[MiniInt] = miniIntMultiplication - checkAll("MiniInt addition", CommutativeMonoidTests[MiniInt].commutativeMonoid) + checkAll("MiniInt multiplication", CommutativeMonoidTests[MiniInt].commutativeMonoid) checkAll("CommutativeMonoid[MiniInt] multiplication", SerializableTests.serializable(miniIntMultiplication)) }