Skip to content

Commit

Permalink
feat: Add common numeric constraints (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltotore authored Dec 30, 2022
1 parent d600c10 commit 6b327bd
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 52 deletions.
23 changes: 22 additions & 1 deletion main/src/io/github/iltotore/iron/compileTime.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"

Expand Down
161 changes: 121 additions & 40 deletions main/src/io/github/iltotore/iron/constraint/numeric.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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
11 changes: 0 additions & 11 deletions main/src/io/github/iltotore/iron/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down
50 changes: 50 additions & 0 deletions main/test/src/io/github/iltotore/iron/testing/NumericSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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]
}
}

0 comments on commit 6b327bd

Please sign in to comment.