diff --git a/main/src/io/github/iltotore/iron/compileTime.scala b/main/src/io/github/iltotore/iron/compileTime.scala index 9bdad499..cfdbea77 100644 --- a/main/src/io/github/iltotore/iron/compileTime.scala +++ b/main/src/io/github/iltotore/iron/compileTime.scala @@ -10,6 +10,8 @@ import scala.quoted.* */ object compileTime: + type NumConstant = Int | Long | Float | Double + /** * The zero number of the given type. * @tparam A the numerical primitive type. @@ -144,13 +146,32 @@ object compileTime: */ type %[A, B] = NumOp[A, B, int.%, long.%, float.%, double.%] + /** + * Polymorphic `ToDouble`. + * + * @tparam A the constant type to cast. + */ + type ToDouble[A <: NumConstant] <: Double = A match + case Int => int.ToDouble[A] + case Long => long.ToDouble[A] + case Float => float.ToDouble[A] + case Double => A & Double + + /** + * Get the `Double` value of the given type. + * + * @tparam A the type to convert to `Double`. + * @return the String representation of the given type. Equivalent to `constValue[ToDouble[A]]`. + */ + transparent inline def doubleValue[A <: NumConstant]: Double = constValue[ToDouble[A]] + /** * Get the `String` value of the given type. * * @tparam A the type to convert to `String`. * @return the String representation of the given type. Equivalent to `constValue[ToString[A]]`. */ - inline def stringValue[A]: String = constValue[ToString[A]] + transparent inline def stringValue[A]: String = constValue[ToString[A]] def applyConstraint[A, C, Impl <: Constraint[A, C]](expr: Expr[A], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = // Using quotes directly causes a "deferred inline error" diff --git a/main/src/io/github/iltotore/iron/constraint/numeric.scala b/main/src/io/github/iltotore/iron/constraint/numeric.scala index 60234244..5ea0325c 100644 --- a/main/src/io/github/iltotore/iron/constraint/numeric.scala +++ b/main/src/io/github/iltotore/iron/constraint/numeric.scala @@ -2,10 +2,7 @@ package io.github.iltotore.iron.constraint import io.github.iltotore.iron.constraint.any.* import io.github.iltotore.iron.compileTime.* -import io.github.iltotore.iron.{==>, Constraint, Implication, IntNumber, Number} - -import scala.compiletime.constValue -import scala.compiletime.ops.any.ToString +import io.github.iltotore.iron.{==>, Constraint, Implication} /** * Number-related constraints. @@ -40,6 +37,50 @@ object numeric: */ type LessEqual[V] = (Less[V] | StrictEqual[V]) DescribedAs ("Should be less than or equal to " + V) + /** + * Tests if the input is strictly positive. + */ + type Positive = Greater[0] DescribedAs "Should be strictly positive" + + /** + * Tests if the input is strictly negative. + */ + type Negative = Less[0] DescribedAs "Should be strictly negative" + + object Interval: + + /** + * Tests if the input is included in `(V1, V2)` + * + * @tparam V1 the lower bound, exclusive. + * @tparam V2 the upper bound, exclusive. + */ + type Open[V1, V2] = (Greater[V1] & Less[V2]) DescribedAs ("Should be included in (" + V1 + ", " + V2 + ")") + + /** + * Tests if the input is included in `(V1, V2]` + * + * @tparam V1 the lower bound, exclusive. + * @tparam V2 the upper bound, inclusive. + */ + type OpenClosed[V1, V2] = (Greater[V1] & LessEqual[V2]) DescribedAs ("Should be included in (" + V1 + ", " + V2 + "]") + + /** + * Tests if the input is included in `[V1, V2)` + * + * @tparam V1 the lower bound, inclusive. + * @tparam V2 the upper bound, exclusive. + */ + type ClosedOpen[V1, V2] = (GreaterEqual[V1] & Less[V2]) DescribedAs ("Should be included in [" + V1 + ", " + V2 + ")") + + /** + * Tests if the input is included in `[V1, V2]` + * + * @tparam V1 the lower bound, inclusive. + * @tparam V2 the upper bound, inclusive. + */ + type Closed[V1, V2] = (GreaterEqual[V1] & LessEqual[V2]) DescribedAs ("Should be included in [" + V1 + ", " + V2 + "]") + /** * Tests if the input is a multiple of V. * @@ -56,21 +97,41 @@ object numeric: */ final class Divide[V] + /** + * Tests if the input is even (a multiple of 2). + */ + type Even = Multiple[2] + + /** + * Tests if the input is odd (not a multiple of 2). + */ + type Odd = Not[Even] + + /** + * Tests if the input is not a representable number. + */ + final class NaN + + /** + * Tests if the input is whether `+infinity` or `-infinity`. + */ + final class Infinity + object Greater: - private trait GreaterConstraint[A, V <: A] extends Constraint[A, Greater[V]]: + private trait GreaterConstraint[A, V <: NumConstant] extends Constraint[A, Greater[V]]: override inline def message: String = "Should be greater than " + stringValue[V] - inline given [V <: Int]: GreaterConstraint[Int, V] with - override inline def test(value: Int): Boolean = value > constValue[V] + inline given [V <: NumConstant]: GreaterConstraint[Int, V] with + override inline def test(value: Int): Boolean = value > doubleValue[V] - inline given [V <: Long]: GreaterConstraint[Long, V] with - override inline def test(value: Long): Boolean = value > constValue[V] + inline given [V <: NumConstant]: GreaterConstraint[Long, V] with + override inline def test(value: Long): Boolean = value > doubleValue[V] - inline given [V <: Float]: GreaterConstraint[Float, V] with - override inline def test(value: Float): Boolean = value > constValue[V] + inline given [V <: NumConstant]: GreaterConstraint[Float, V] with + override inline def test(value: Float): Boolean = value > doubleValue[V] - inline given [V <: Double]: GreaterConstraint[Double, V] with - override inline def test(value: Double): Boolean = value > constValue[V] + inline given [V <: NumConstant]: GreaterConstraint[Double, V] with + override inline def test(value: Double): Boolean = value > doubleValue[V] given [V1, V2](using V1 > V2 =:= true): (Greater[V1] ==> Greater[V2]) = Implication() @@ -83,20 +144,20 @@ object numeric: given [V1, V2](using V1 >= V2 =:= true): (StrictEqual[V2] ==> Not[Greater[V1]]) = Implication() object Less: - private trait LessConstraint[A, V] extends Constraint[A, Less[V]]: + private trait LessConstraint[A, V <: NumConstant] extends Constraint[A, Less[V]]: override inline def message: String = "Should be less than " + stringValue[V] - inline given [V <: Int]: LessConstraint[Int, V] with - override inline def test(value: Int): Boolean = value < constValue[V] + inline given [V <: NumConstant]: LessConstraint[Int, V] with + override inline def test(value: Int): Boolean = value < doubleValue[V] - inline given [V <: Long]: LessConstraint[Long, V] with - override inline def test(value: Long): Boolean = value < constValue[V] + inline given [V <: NumConstant]: LessConstraint[Long, V] with + override inline def test(value: Long): Boolean = value < doubleValue[V] - inline given [V <: Float]: LessConstraint[Float, V] with - override inline def test(value: Float): Boolean = value < constValue[V] + inline given [V <: NumConstant]: LessConstraint[Float, V] with + override inline def test(value: Float): Boolean = value < doubleValue[V] - inline given [V <: Double]: LessConstraint[Double, V] with - override inline def test(value: Double): Boolean = value < constValue[V] + inline given [V <: NumConstant]: LessConstraint[Double, V] with + override inline def test(value: Double): Boolean = value < doubleValue[V] given [V1, V2](using V1 < V2 =:= true): (Less[V1] ==> Less[V2]) = Implication() @@ -109,35 +170,55 @@ object numeric: given [V1, V2](using V1 <= V2 =:= true): (StrictEqual[V2] ==> Not[Less[V1]]) = Implication() object Multiple: - private trait MultipleConstraint[A, V] extends Constraint[A, Multiple[V]]: + private trait MultipleConstraint[A, V <: NumConstant] extends Constraint[A, Multiple[V]]: override inline def message: String = "Should be a multiple of " + stringValue[V] - inline given [V <: Int]: MultipleConstraint[Int, V] with - override inline def test(value: Int): Boolean = value % constValue[V] == 0 + inline given [V <: NumConstant]: MultipleConstraint[Int, V] with + override inline def test(value: Int): Boolean = value % doubleValue[V] == 0 - inline given [V <: Long]: MultipleConstraint[Long, V] with - override inline def test(value: Long): Boolean = value % constValue[V] == 0 + inline given [V <: NumConstant]: MultipleConstraint[Long, V] with + override inline def test(value: Long): Boolean = value % doubleValue[V] == 0 - inline given [V <: Float]: MultipleConstraint[Float, V] with - override inline def test(value: Float): Boolean = value % constValue[V] == 0 + inline given [V <: NumConstant]: MultipleConstraint[Float, V] with + override inline def test(value: Float): Boolean = value % doubleValue[V] == 0 - inline given [V <: Double]: MultipleConstraint[Double, V] with - override inline def test(value: Double): Boolean = value % constValue[V] == 0 + inline given [V <: NumConstant]: MultipleConstraint[Double, V] with + override inline def test(value: Double): Boolean = value % doubleValue[V] == 0 given [A, V1 <: A, V2 <: A](using V1 % V2 =:= Zero[A]): (Multiple[V1] ==> Multiple[V2]) = Implication() object Divide: - private trait DivideConstraint[A, V] extends Constraint[A, Divide[V]]: + private trait DivideConstraint[A, V <: NumConstant] extends Constraint[A, Divide[V]]: override inline def message: String = "Should divide " + stringValue[V] - inline given [V <: Int]: DivideConstraint[Int, V] with - override inline def test(value: Int): Boolean = constValue[V] % value == 0 + inline given [V <: NumConstant]: DivideConstraint[Int, V] with + override inline def test(value: Int): Boolean = doubleValue[V] % value == 0 + + inline given [V <: NumConstant]: DivideConstraint[Long, V] with + override inline def test(value: Long): Boolean = doubleValue[V] % value == 0 + + inline given [V <: NumConstant]: DivideConstraint[Float, V] with + override inline def test(value: Float): Boolean = doubleValue[V] % value == 0 + + inline given [V <: NumConstant]: DivideConstraint[Double, V] with + override inline def test(value: Double): Boolean = doubleValue[V] % value == 0 + + object NaN: + private trait NaNConstraint[A] extends Constraint[A, NaN]: + override inline def message: String = "Should be an unrepresentable number" + + inline given NaNConstraint[Float] with + override inline def test(value: Float): Boolean = value.isNaN + + inline given NaNConstraint[Double] with + override inline def test(value: Double): Boolean = value.isNaN - inline given [V <: Long]: DivideConstraint[Long, V] with - override inline def test(value: Long): Boolean = constValue[V] % value == 0 + object Infinity: + private trait InfinityConstraint[A] extends Constraint[A, Infinity]: + override inline def message: String = "Should be -infinity or +infinity" - inline given [V <: Float]: DivideConstraint[Float, V] with - override inline def test(value: Float): Boolean = constValue[V] % value == 0 + inline given InfinityConstraint[Float] with + override inline def test(value: Float): Boolean = value.isInfinity - inline given [V <: Double]: DivideConstraint[Double, V] with - override inline def test(value: Double): Boolean = constValue[V] % value == 0 + inline given InfinityConstraint[Double] with + override inline def test(value: Double): Boolean = value.isInfinity diff --git a/main/src/io/github/iltotore/iron/package.scala b/main/src/io/github/iltotore/iron/package.scala index d6db7007..e6c87150 100644 --- a/main/src/io/github/iltotore/iron/package.scala +++ b/main/src/io/github/iltotore/iron/package.scala @@ -15,17 +15,6 @@ import scala.util.NotGiven export io.github.iltotore.iron.constraint.any.* -/** - * Union of all numerical primitives. - * This abstraction facilitates the creation of numerical constraints. - */ -type Number = Byte | Short | Int | Long | Float | Double - -/** - * Union of all integer primitives. This abstraction facilitates the creation of numerical constraints. - */ -type IntNumber = Byte | Short | Int | Long - /** * An Iron type (refined). * diff --git a/main/test/src/io/github/iltotore/iron/testing/NumericSuite.scala b/main/test/src/io/github/iltotore/iron/testing/NumericSuite.scala index 7d4b1baf..53db6c5b 100644 --- a/main/test/src/io/github/iltotore/iron/testing/NumericSuite.scala +++ b/main/test/src/io/github/iltotore/iron/testing/NumericSuite.scala @@ -30,6 +30,40 @@ object NumericSuite extends TestSuite: test - -1.assertRefine[LessEqual[0]] } + test("interval") { + test("open") { + test - 1.assertRefine[Interval.Open[0, 2]] + test - 0.assertNotRefine[Interval.Open[0, 2]] + test - 2.assertNotRefine[Interval.Open[0, 2]] + test - -1.assertNotRefine[Interval.Open[0, 2]] + test - 3.assertNotRefine[Interval.Open[0, 2]] + } + + test("openClosed") { + test - 1.assertRefine[Interval.OpenClosed[0, 2]] + test - 2.assertRefine[Interval.OpenClosed[0, 2]] + test - 0.assertNotRefine[Interval.OpenClosed[0, 2]] + test - -1.assertNotRefine[Interval.OpenClosed[0, 2]] + test - 3.assertNotRefine[Interval.OpenClosed[0, 2]] + } + + test("closedOpen") { + test - 1.assertRefine[Interval.ClosedOpen[0, 2]] + test - 0.assertRefine[Interval.ClosedOpen[0, 2]] + test - 2.assertNotRefine[Interval.ClosedOpen[0, 2]] + test - -1.assertNotRefine[Interval.ClosedOpen[0, 2]] + test - 3.assertNotRefine[Interval.ClosedOpen[0, 2]] + } + + test("closed") { + test - 1.assertRefine[Interval.Closed[0, 2]] + test - 2.assertRefine[Interval.Closed[0, 2]] + test - 0.assertRefine[Interval.Closed[0, 2]] + test - -1.assertNotRefine[Interval.Closed[0, 2]] + test - 3.assertNotRefine[Interval.Closed[0, 2]] + } + } + test("multiple") { test - 1.assertNotRefine[Multiple[2]] test - 2.assertRefine[Multiple[2]] @@ -40,4 +74,20 @@ object NumericSuite extends TestSuite: test - 2.assertRefine[Divide[2]] test - 3.assertNotRefine[Divide[2]] } + + test("nan") { + test - Float.NaN.assertRefine[NaN] + test - Double.NaN.assertRefine[NaN] + test - 0f.assertNotRefine[NaN] + test - 0d.assertNotRefine[NaN] + } + + test("infinity") { + test - Float.PositiveInfinity.assertRefine[Infinity] + test - Float.NegativeInfinity.assertRefine[Infinity] + test - Double.PositiveInfinity.assertRefine[Infinity] + test - Double.NegativeInfinity.assertRefine[Infinity] + test - 0f.assertNotRefine[Infinity] + test - 0d.assertNotRefine[Infinity] + } }