All Downloads are FREE. Search and download functionalities are using the official Maven repository.

coulomb.rational.rational.scala Maven / Gradle / Ivy

There is a newer version: 0.9.0-RC1
Show newest version
/*
 * Copyright 2022 Erik Erlandson
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package coulomb.rational

final class Rational private (val n: BigInt, val d: BigInt)
    extends Serializable:
    import Rational.canonical

    override def toString: String =
        if (d == 1) s"$n" else s"$n/$d"

    inline def +(rhs: Rational): Rational =
        canonical((n * rhs.d) + (rhs.n * d), d * rhs.d)

    inline def -(rhs: Rational): Rational =
        canonical((n * rhs.d) - (rhs.n * d), d * rhs.d)

    inline def *(rhs: Rational): Rational =
        canonical(n * rhs.n, d * rhs.d)

    inline def /(rhs: Rational): Rational =
        canonical(n * rhs.d, d * rhs.n)

    inline def unary_- : Rational =
        canonical(-n, d)

    def pow(e: Int): Rational =
        if (e < 0) then canonical(d.pow(-e), n.pow(-e))
        else if (e == 0) then canonical(1, 1)
        else if (e == 1) then this
        else canonical(n.pow(e), d.pow(e))

    def root(e: Int): Rational =
        import scala.math
        require(e != 0)
        if (e < 0) then canonical(d, n).root(-e)
        else if (e == 1) then this
        else if (n < 0) then
            require(e % 2 == 1)
            -((-this).root(e))
        else
            val nr = math.pow(n.toDouble, 1.0 / e.toDouble)
            val dr = math.pow(d.toDouble, 1.0 / e.toDouble)
            if ((nr == math.rint(nr)) && (dr == math.rint(dr))) then
                canonical(nr.toLong, dr.toLong)
            else Rational(nr / dr)

    inline def pow(e: Rational): Rational = this.pow(e.n.toInt).root(e.d.toInt)

    inline def toInt: Int = toDouble.toInt
    inline def toLong: Long = toDouble.toLong
    inline def toFloat: Float = toDouble.toFloat
    inline def toDouble: Double = n.toDouble / d.toDouble

    override def equals(rhs: Any): Boolean = rhs match
        case v: Rational => (n == v.n) && (d == v.d)
        case v: Int      => (n == v) && (d == 1)
        case v: Long     => (n == v) && (d == 1)
        case _           => false

    override def hashCode: Int = 29 * (37 * n.## + d.##)

    inline def <(rhs: Rational): Boolean = (n * rhs.d) < (rhs.n * d)
    inline def >(rhs: Rational): Boolean = rhs < this
    inline def <=(rhs: Rational): Boolean = !(this > rhs)
    inline def >=(rhs: Rational): Boolean = !(this < rhs)
end Rational

object Rational:
    import scala.math.*

    inline def apply(n: BigInt, d: BigInt): Rational = canonical(n, d)

    inline def apply(r: Rational): Rational = canonical(r.n, r.d)

    inline def apply(v: Int): Rational = canonical(v, 1)
    inline def apply(v: Long): Rational = canonical(v, 1)
    inline def apply(v: Float): Rational = apply(v.toDouble)

    def apply(v: Double): Rational =
        if (abs(v) == 0.0) then canonical(0, 1)
        else
            // IEEE double precision guaranteed 15 base-10 digits of precision
            val e = 15 - (floor(log10(abs(v))).toInt + 1)
            val (np10, dp10) = if (e < 0) then (-e, 0) else (0, e)
            val vi = v * scala.math.pow(10, e)
            val n = BigInt(vi.toLong) * BigInt(10).pow(np10)
            val d = BigInt(10).pow(dp10)
            canonical(n, d)

    // intended to be the single safe way to construct a canonical rational
    // every construction of a new Rational should reduce to some call to this method
    private[rational] def canonical(n: BigInt, d: BigInt): Rational =
        require(d != 0, "Rational denominator cannot be zero")
        if (n == 0)
            // canonical zero is 0/1
            new Rational(0, 1)
        else if (d < 0) then
            // canonical denominator is always positive
            canonical(-n, -d)
        else
            // canonical rationals are fully reduced
            val g = n.gcd(d)
            new Rational(n / g, d / g)

    val const0 = Rational(0, 1)
    val const1 = Rational(1, 1)
    val const2 = Rational(2, 1)

    given Conversion[Int, Rational] with
        inline def apply(v: Int): Rational = Rational(v)
    given Conversion[Long, Rational] with
        inline def apply(v: Long): Rational = Rational(v)
    given Conversion[Float, Rational] with
        inline def apply(v: Float): Rational = Rational(v)
    given Conversion[Double, Rational] with
        inline def apply(v: Double): Rational = Rational(v)

    given CanEqual[Rational, Rational] = CanEqual.derived
    given CanEqual[Rational, Int] = CanEqual.derived
    given CanEqual[Rational, Long] = CanEqual.derived
end Rational

/** Obtaining values from Rational type expressions */
object typeexpr:
    import scala.annotation.implicitNotFound

    inline def rational[E]: Rational = ${ meta.teToRational[E] }
    inline def bigInt[E]: BigInt = ${ meta.teToBigInt[E] }
    inline def double[E]: Double = ${ meta.teToDouble[E] }

    @implicitNotFound("type expr ${E} is not a non-negative Int")
    class NonNegInt[E](val value: Int)
    object NonNegInt:
        // interesting, this has to be 'transparent' to work with NotGiven
        transparent inline given ctx_NonNegInt[E]: NonNegInt[E] = ${
            meta.teToNonNegInt[E]
        }

    @implicitNotFound("type expr ${E} is not a positive Int")
    class PosInt[E](val value: Int)
    object PosInt:
        transparent inline given ctx_PosInt[E]: PosInt[E] = ${
            meta.teToPosInt[E]
        }

    @implicitNotFound("type expr ${E} is not an Int")
    class AllInt[E](val value: Int)
    object AllInt:
        transparent inline given ctx_AllInt[E]: AllInt[E] = ${ meta.teToInt[E] }

    object meta:
        import scala.quoted.*
        import scala.language.implicitConversions
        import coulomb.infra.meta.{
            rationalTE,
            bigintTE,
            ctx_RationalToExpr,
            typestr
        }

        def teToRational[E](using Quotes, Type[E]): Expr[Rational] =
            import quotes.reflect.*
            val rationalTE(v) = TypeRepr.of[E]: @unchecked
            Expr(v)

        def teToBigInt[E](using Quotes, Type[E]): Expr[BigInt] =
            import quotes.reflect.*
            val bigintTE(v) = TypeRepr.of[E]: @unchecked
            Expr(v)

        def teToDouble[E](using Quotes, Type[E]): Expr[Double] =
            import quotes.reflect.*
            val rationalTE(v) = TypeRepr.of[E]: @unchecked
            Expr(v.toDouble)

        def teToNonNegInt[E](using Quotes, Type[E]): Expr[NonNegInt[E]] =
            import quotes.reflect.*
            val rationalTE(v) = TypeRepr.of[E]: @unchecked
            if ((v.d == 1) && (v.n >= 0) && (v.n.isValidInt)) then
                '{ new NonNegInt[E](${ Expr(v.n.toInt) }) }
            else
                report.error(
                    s"type expr ${typestr(TypeRepr.of[E])} is not a non-negative Int"
                )
                '{ new NonNegInt[E](0) }

        def teToPosInt[E](using Quotes, Type[E]): Expr[PosInt[E]] =
            import quotes.reflect.*
            val rationalTE(v) = TypeRepr.of[E]: @unchecked
            if ((v.d == 1) && (v.n > 0) && (v.n.isValidInt)) then
                '{ new PosInt[E](${ Expr(v.n.toInt) }) }
            else
                report.error(
                    s"type expr ${typestr(TypeRepr.of[E])} is not a positive Int"
                )
                '{ new PosInt[E](0) }

        def teToInt[E](using Quotes, Type[E]): Expr[AllInt[E]] =
            import quotes.reflect.*
            val rationalTE(v) = TypeRepr.of[E]: @unchecked
            if ((v.d == 1) && (v.n.isValidInt)) then
                '{ new AllInt[E](${ Expr(v.n.toInt) }) }
            else
                report.error(
                    s"type expr ${typestr(TypeRepr.of[E])} is not an Int"
                )
                '{ new AllInt[E](0) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy