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

com.phasmidsoftware.number.core.Number.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023. Phasmid Software
 */

package com.phasmidsoftware.number.core

import com.phasmidsoftware.number.core.FP.{optional, toTry}
import com.phasmidsoftware.number.core.Field.convertToNumber
import com.phasmidsoftware.number.core.Number.{inverse, negate}
import com.phasmidsoftware.number.core.Value.{fromDouble, fromInt, fromRational}
import com.phasmidsoftware.number.parse.NumberParser
import com.phasmidsoftware.number.parse.RationalParser.parseComponents
import scala.annotation.tailrec
import scala.language.{implicitConversions, postfixOps}
import scala.util._

/**
  * Trait to model numbers as a sub-class of Field and such that we can order Numbers.
  * That's to say that Numbers have linear domain and all belong, directly or indirectly, to the set R (real numbers).
  *
  * CONSIDER eliminate extending Field
  *
  * Every number has three properties:
  * * value: Value
  * * factor: Factor
  * * fuzz: (from extending Fuzz[Double]).
  */
trait Number extends Fuzz[Double] with Ordered[Number] with Numerical {

  /**
    * The value of this Number.
    *
    * @return the value.
    */
  def value: Value

  /**
    * The factor of this Number.
    * Ordinary numbers are of Scalar factor, angles have factor Radian, and natural logs have factor NatLog.
    *
    * @return the factor.
    */
  def factor: Factor

  /**
    * Method to determine if this is a valid Number.
    * An invalid number has a value of form Left(Left(Left(None)))
    *
    * @return true if this is a valid Number
    */
  def isValid: Boolean

  /**
    * Method to determine if this is an imaginary Number,
    * that's to say a number with negative value and Root2 as its factor.
    *
    * @return true if imaginary.
    */
  def isImaginary: Boolean

  /**
    * Method to make some trivial simplifications of this Number (only if exact).
    *
    * @return either this Number or a simplified Number.
    */
  def simplify: Number

  /**
    * Method to apply a function to this Number.
    *
    * @param f      a function Double=>Double.
    * @param dfByDx the first derivative of f.
    * @return a Try[Number] which is the result of applying f to this Number.
    */
  def applyFunc(f: Double => Double, dfByDx: Double => Double): Try[Number]

  /**
    * Method to get the value of this Number as an optional Double.
    *
    * @return an Some(Double) which is the closest possible value to the nominal value, otherwise None if this is invalid.
    */
  def toDouble: Option[Double]

  /**
    * An optional Double that corresponds to the value of this Number (but ignoring the factor).
    */
  def maybeDouble: Option[Double]

  /**
    * Method to determine if this Number is actually represented as an Integer.
    *
    * @return true if exact and rational.
    */
  def isInteger: Boolean

  /**
    * Method to determine if this Number is actually represented as a Rational.
    *
    * @return true if exact and rational.
    */
  def isRational: Boolean

  /**
    * Method to get the value of this Number as an (optional) Rational.
    * If this is actually a Double, it will be converted to a Rational according to the implicit conversion from Double to Rational.
    * See Rational.convertDouble(x).
    *
    * @return an Option of Rational.
    */
  def toRational: Option[Rational]

  /**
    * Method to get the value of this Number as an Int.
    *
    * @return an Option of Int. If this Number cannot be converted to an Int, then None will be returned.
    */
  def toInt: Option[Int]

  /**
    * Method to get the value of this Number as an Int.
    *
    * @return an Option of Long. If this Number cannot be converted to a Long, then None will be returned.
    */
  def toLong: Option[Long] = toRational map (_.toLong)

  /**
    * Method to get the value of this Number as an (optional) BigInt.
    * This will return Some(x) only if this is an Int, or a Rational with unit denominator.
    *
    * @return an Option of BigInt.
    */
  def toBigInt: Option[BigInt]

  /**
    * Method to get the value of this Number as an (optional) BigInt.
    * This will return Some(x) only if this is an Int, or a Rational with unit denominator.
    *
    * @return an Option of BigDecimal.
    */
  def toBigDecimal: Option[BigDecimal]

  /**
    * Method to determine if this Number is positive.
    * Use case: does the String representation not start with a "-"?
    *
    * CONSIDER evaluating toString instead.
    *
    * @return true if this Number is greater than or equal to 0.
    */
  def isPositive: Boolean

  /**
    * Negative of this Number.
    */
  def makeNegative: Number

  /**
    * Add this Number to n.
    *
    * @param n another Number.
    * @return the sum of this and n.
    */
  def doAdd(n: Number): Number

  /**
    * Subtract n from this Number
    *
    * @param n another Number.
    * @return the difference of this and n.
    */
  def doSubtract(n: Number): Number = doAdd(negate(n))

  /**
    * Multiply this Number by n.
    *
    * @param n another Number.
    * @return the product of this and n.
    */
  def doMultiply(n: Number): Number

  /**
    * Perform an exact scalar multiplication of this Number by the scale factor z.
    *
    * @param r a Rational.
    * @return a new Number which is this Number scaled by z.
    */
  def doMultiple(r: Rational): Number = GeneralNumber.times(this, r)

  /**
    * Perform an exact scalar multiplication of this Number by the scale factor z.
    *
    * @param x an Int.
    * @return a new Number which is this Number scaled by z.
    */
  def doMultiple(x: Int): Number = doMultiple(Rational(x))

  /**
    * Divide this Number by n.
    *
    * @param n another Number.
    * @return this quotient of this and n, i.e. this/n.
    */
  def doDivide(n: Number): Number

  /**
    * Yields the inverse of this Number.
    * This Number is first normalized so that its factor is Scalar, since we cannot directly invert Numbers with other
    * factors.
    *
    * CONSIDER allowing logarithmic numbers to be inverted simply by changing the sign of the value.
    */
  def doInvert: Number = Number.inverse(convertToNumber(normalize))

  /**
    * Raise this Number to the power p.
    *
    * @param p a Number.
    * @return this Number raised to the power of p.
    */
  def doPower(p: Number): Number

  // NOTE Following are methods defined in Field.

  /**
    * Add x to this Number and return the result.
    *
    * @param x the addend.
    * @return the sum.
    */
  def add(x: Field): Field = x match {
    case Real(n) if n.isImaginary => ComplexCartesian.fromImaginary(n) doAdd Complex(this)
    case Real(n) => Real(doAdd(n))
    case c@BaseComplex(_, _) => c.add(this.asComplex)
  }

  /**
    * Multiply this Number by x and return the result.
    *
    * @param x the multiplicand.
    * @return the product.
    */
  def multiply(x: Field): Field = (this, x) match {
    case (Number.zero, _) | (_, Constants.zero) => Constants.zero
    case (Number.one, _) => x
    case (_, Constants.one) => Real(this)
    case (Number.i, Constants.pi) | (Number.pi, Constants.i) => Constants.iPi
    case (n, Constants.i) => n.asComplex.rotate
    case (Number.i, _) => x.multiply(ComplexCartesian(0, 1))
    case (_, Real(n)) => doMultiply(n).normalize
    case (_, c@BaseComplex(_, _)) => c.multiply(this.asComplex)
  }

  /**
    * Divide this Number by x and return the result.
    *
    * @param x the divisor.
    * @return the quotient.
    */
  def divide(x: Field): Field = x match {
    case Real(n) => Real(doDivide(n))
    case c@BaseComplex(_, _) => c.divide(x)
  }

  /**
    * Change the sign of this Number.
    */
  def unary_- : Field = Real(makeNegative)

  def power(p: Number): Number = p match {
    case Number.zero => Number.one
    case Number.one => this
    case Number.negOne => inverse(this)
    case Number.two => this doMultiply this
    case _ => doPower(p)
  }

  /**
    * Raise this Number to the power p.
    *
    * @param p a Field.
    * @return this Number raised to power p.
    */
  def power(p: Field): Field = p match {
    case Real(n) => Real(doPower(n))
    case ComplexCartesian(x, y) => ComplexPolar(doPower(x), y) // CONSIDER is this correct?
    case _ => throw NumberException("logic error: power not supported for non-Number powers")
  }

  /**
    * Yields the inverse of this Number.
    * This Number is first normalized so that its factor is Scalar, since we cannot directly invert Numbers with other
    * factors.
    *
    * CONSIDER allowing logarithmic numbers to be inverted simply by changing the sign of the value.
    */
  def invert: Field = Real(doInvert)

  /**
    * Yields the square root of this Number.
    * If possible, the result will be exact.
    */
  def sqrt: Number

  /**
    * Method to determine the sine of this Number.
    * The result will be a Number with Scalar factor.
    *
    * @return the sine of this.
    */
  def sin: Number

  /**
    * Method to determine the cosine of this Number.
    * The result will be a Number with Scalar factor.
    *
    * @return the cosine.
    */
  def cos: Number

  /**
    * Method to determine the tangent of this Number.
    * The result will be a Number with Scalar factor.
    *
    * @return the tangent
    */
  def tan: Number = (value, factor) match { // CONSIDER modulating first.
    case (Left(Right(r)), Radian) => r match {
      case Rational(Rational.bigOne, Rational.bigFour) | Rational(Rational.bigFive, Rational.bigFour) => Number.one
      case Rational(Rational.bigThree, Rational.bigFour) | Rational(Rational.bigSeven, Rational.bigFour) => negate(Number.one)
      case _ => sin doDivide cos
    }
    case _ => sin doDivide cos
  }

  /**
    * Calculate the angle whose opposite length is y and whose adjacent length is this.
    *
    * @param y the opposite length
    * @return the angle defined by x = this, y = y
    */
  def atan(y: Number): Number

  /**
    * Method to determine the natural log of this Number.
    * The result will be a Number with Scalar factor.
    *
    * @return the natural log of this.
    */
  def log: Number

  /**
    * Method to raise e to the power of this number.
    * The result will be a Number with NatLog factor.
    *
    * @return the e to the power of this.
    */
  def exp: Number

  /**
    * Method to determine the sense of this number: negative, zero, or positive.
    *
    * @return an Int which is negative, zero, or positive according to the magnitude of this.
    */
  def signum: Int

  /**
    * Method to determine the sense of this number: negative, zero, or positive.
    * If this FuzzyNumber cannot be distinguished from zero with p confidence, then
    *
    * @param p the confidence desired.
    * @return an Int which is negative, zero, or positive according to the magnitude of this.
    */
  def signum(p: Double): Int

  /**
    * Method to yield the absolute value of this Number.
    *
    * @return this if its positive, else - this.
    */
  def abs: Number

  /**
    * @return Some(this).
    */
  def asNumber: Option[Number] = Some(this)

  /**
    * Method to return this Number as a Complex.
    *
    * @return Complex(this) as appropriate.
    */
  def asComplex: Complex = if (isImaginary)
    ComplexCartesian.fromImaginary(this)
  else
    ComplexPolar(this)

  /**
    * Method to create a new version of this, but with factor f.
    * NOTE: the result will have the same absolute magnitude as this.
    * In other words,  in the case where f is not factor, the numerical value of the result's value will be different
    * from this value.
    *
    * @param f the new factor for the result.
    * @return a Number based on this and factor.
    */
  def scale(f: Factor): Number

  /**
    * Perform a fuzzy comparison where we only require p confidence to know that this and other are effectively the same.
    *
    * @param other the Number to be compared with.
    * @param p     the confidence expressed as a fraction of 1 (0.5 would be a typical value).
    * @return -1, 0, 1 as usual.
    */
  def fuzzyCompare(other: Number, p: Double): Int

  /**
    * Return optional Fuzziness of Box shape, such that
    * this would be considered just within the resulting tolerance.
    *
    * @param other another Number: the ideal or target value.
    * @return an optional relative Fuzziness.
    */
  def asComparedWith(other: Number): Option[Fuzziness[Double]]

  /**
    * Evaluate a dyadic operator on this and other, using either plus, times, ... according to the value of op.
    * NOTE: this and other must have been aligned by type so that they have the same structure.
    *
    * @param other the other operand, a Number.
    * @param f     the factor to apply to the result.
    * @param op    the appropriate DyadicOperation.
    * @return a new Number which is result of applying the appropriate function to the operands this and other.
    */
  def composeDyadic(other: Number, f: Factor)(op: DyadicOperation): Option[Number]

  /**
    * Evaluate a monadic operator on this.
    *
    * @param f  the factor to apply to the result.
    * @param op the appropriate MonadicOperation.
    * @return a new Number which is result of applying the appropriate function to the operand this.
    */
  def transformMonadic(f: Factor)(op: MonadicOperation): Option[Number]

  /**
    * Evaluate a query operator on this.
    *
    * @param op the appropriate QueryOperation.
    * @return a T.
    */
  def query[T](op: QueryOperation[T], defaultVal: => T): T

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Both the value and the factor will be changed.
    *
    * @param v the value.
    * @param f the factor.
    * @return either a Number.
    */
  def make(v: Value, f: Factor): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the factor will change.
    * This method does not need to be followed by a call to specialize.
    *
    * TESTME
    *
    * @param f the factor.
    * @return either a Number.
    */
  def make(f: Factor): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value will change.
    * This method should be followed by a call to specialize.
    *
    * @param v the value.
    * @return either a Number.
    */
  def make(v: Value): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value will change.
    * This method should be followed by a call to specialize.
    *
    * @param v the value.
    * @param f Factor.
    * @return either a Number.
    */
  def make(v: Int, f: Factor): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value will change.
    * This method should be followed by a call to specialize.
    *
    * @param v the value.
    * @return either a Number.
    */
  def make(v: Int): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value and the factor will change.
    * This method should be followed by a call to specialize.
    *
    * @param r a Rational.
    * @param f Factor.
    * @return either a Number.
    */
  def make(r: Rational, f: Factor): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value will change.
    * This method should be followed by a call to specialize.
    *
    * @param v the value.
    * @return either a Number.
    */
  def make(v: Rational): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value and factor will change.
    * This method should be followed by a call to specialize.
    *
    * @param v the value (a Double).
    * @param f Factor.
    * @return either a Number.
    */
  def make(v: Double, f: Factor): Number

  /**
    * Make a copy of this Number, given the same degree of fuzziness as the original.
    * Only the value and factor will change.
    * This method should be followed by a call to specialize.
    *
    * @param v  the value (a Double).
    * @param f  Factor.
    * @param fo optional fuzz.
    * @return either a Number.
    */
  def make(v: Double, f: Factor, fo: Option[Fuzziness[Double]]): Number

  /**
    * Make a copy of this Number, with the same value and factor but with a different value of fuzziness.
    *
    * TESTME
    *
    * @param fo the (optional) fuzziness.
    * @return a Number.
    */
  def make(fo: Option[Fuzziness[Double]]): Number

  /**
    * Return a Number which uses the most restricted type possible.
    * A Number based on a Double will yield a Number based on a Rational (if the conversion is exact).
    * A Number based on a Rational will yield a Number based on a BigInt (if there is a unit denominator).
    * A Number based on a BigInt will yield a Number based on a Int (if it is sufficiently small).
    *
    * CONSIDER do we really need this in Number?
    *
    * @return a Number with the same magnitude as this.
    */
  def specialize: Number

  /**
    * Method to ensure that the value is within some factor-specific range.
    * In particular, Radian=based numbers are modulated to the range 0..2
    *
    * @return this or an equivalent Number.
    */
  def modulate: Number

  /**
    * @param p the confidence desired. Ignored if isZero is true.
    * @return true if this Number is equivalent to zero with at least p confidence.
    */
  def isProbablyZero(p: Double): Boolean
}

object Number {

  /**
    * NOTE: this unapply method does not match on the fuzz of a Number.
    *
    * @param arg a Number to be unapplied.
    * @return optional value and factor.
    */
  def unapply(arg: Number): Option[(Value, Factor)] = Some(arg.value, arg.factor)

  /**
    * Exact value of 0
    */
  val zero: Number = 0
  /**
    * Exact value of -0
    */
  val negZero: Number = Rational.negZero
  /**
    * Exact value of 1
    */
  val one: Number = 1
  /**
    * Exact value of -1
    */
  val negOne: Number = -1
  /**
    * Exact value of 2
    */
  val two: Number = 2
  /**
    * Exact value of 1/2
    */
  val half: Number = Rational.half
  /**
    * Exact value of 10
    */
  val ten: Number = Rational.ten
  /**
    * Exact value of pi
    */
  val pi: Number = Number(1, Radian)
  /**
    * Exact value of 𝛑
    */
  //noinspection NonAsciiCharacters
  val `𝛑`: Number = pi
  /**
    * Exact value of 2 pi
    */
  val twoPi: Number = Number(2, Radian)
  /**
    * Exact value of pi/2
    */
  val piBy2: Number = Number(Rational.half, Radian)
  /**
    * Exact value of -pi
    */
  val minusPi: Number = negate(pi)
  /**
    * Exact value of zero radians
    */
  val zeroR: Number = Number(0, Radian)
  /**
    * Exact value of e
    */
  val e: Number = ExactNumber(1, NatLog)
  /**
    * Exact value of i
    */
  val i: Number = ExactNumber(-1, Root2)
  /**
    * Exact value of the Number √2 (not Complex)
    */
  val root2: Number = Number(2, Root2)
  /**
    * Exact value of √3
    */
  val root3: Number = Number(3, Root2)
  /**
    * Exact value of √5
    */
  val root5: Number = Number(5, Root2)

  /**
    * Implicit converter from Expression to Number.
    *
    * @param x the Expression to be converted.
    * @return the equivalent Number.
    * @throws ExpressionException if x cannot be converted to a Number.
    */
  //noinspection Annotator
  implicit def convertExpression(x: Expression): Number = x.materialize.asNumber match {
    case Some(n) => n
    case None => throw ExpressionException(s"Expression $x cannot be converted implicitly to a Number")
  }

  /**
    * Implicit converter from Int to Number.
    *
    * @param x the Int to be converted.
    * @return the equivalent Number.
    */
  implicit def convertInt(x: Int): Number = Number(x)

//  implicit def convertToReal(x: Number): Real = Real(x)

//  implicit def convertToField(x: Number): Field = Real(x)

  /**
    * Implicit class which takes a Double, and using method ~ and an Int parameter,
    * yields a Number with the appropriate degree of fuzziness.
    * This class provides an alternative to having to parse a fuzzy number from a String.
    *
    * @param x a Double.
    */
  implicit class FuzzOps(x: Double) {
    /**
      * Method to yield a (scalar) Number, whose value is x, and whose fuzziness is Gaussian with standard deviation
      * defined by y.
      * The magnitude of the fuzziness is determined by the number of decimal places of x.
      * However, if the decimal part of the number ends in a zero, you should use the method in FuzzStringOps instead.
      *
      * @param y a two-digit Int.
      * @return a Number with absolute, Gaussian fuzziness, whose std. dev. is y.
      */
    def ~(y: Int): Number = if (y >= 10 && y < 100) {
      val p = y * math.pow(10.0, -BigDecimal(x).scale)
      Number(x, Scalar, Some(AbsoluteFuzz(implicitly[Valuable[Double]].fromDouble(p), Gaussian)))
    }
    else
      throw NumberException(s"The ~ operator for defining fuzz for numbers must be followed by two digits: " + y)
  }

  /**
    * Implicit class which takes a String, and using method ~ and an Int parameter,
    * yields a Number with the appropriate degree of fuzziness.
    * This class provides an alternative to having to parse a fuzzy number from a String.
    * Really, the only function it provides is the ability to put the error bounds after the exponent.
    * It is parallel to FuzzOps except for two differences:
    * the input is a String, and this is is so that trailing zeros in the fractional part don't get ignored,
    * thus messing up the fuzziness;
    * And, secondly, the result of ~ is a Try[Number], not just a Number.
    *
    * @param w a String.
    */
  implicit class FuzzStringOps(w: String) {
    /**
      * Method to yield a (scalar) Number, whose value is x, and whose fuzziness is Gaussian with standard deviation
      * defined by y.
      * The magnitude of the fuzziness is determined by the number of decimal places of x.
      *
      * @param n a two-digit Int.
      * @return a Try[Number] with absolute, Gaussian fuzziness, whose std. dev. is y.
      */
    def ~(n: Int): Try[Number] =
      for {
        x <- parse(w) // We don't strictly need this now we also have components
        components <- parseComponents(w)
        f <- toTry(components._3, Failure(NumberException(s"no fractional part: " + w)))
        exp = components._4.getOrElse("0")
        e <- toTry(implicitly[Numeric[Int]].parseString(exp), Failure(NumberException(s"Logic error: " + exp)))
        y <- toTry(optional[Int](x => x >= 10 && x < 100)(n), Failure(NumberException(s"The ~ operator for defining fuzz for numbers must be followed by two digits: " + n)))
        p = y * math.pow(10, e - f.length)
      } yield x.make(Some(AbsoluteFuzz(implicitly[Valuable[Double]].fromDouble(p), Gaussian)))
  }

  /**
    * Implicit class to operate on Numbers introduced as integers.
    *
    * CONSIDER generalizing this to inputs of Values (or Rationals, Doubles).
    *
    * @param x an Int to be treated as a Number.
    */
  implicit class NumberOps(x: Int) {

    /**
      * Add this x (a Number) and yield a Number.
      *
      * @param y the addend, a Number.
      * @return a Number whose value is x + y.
      */
    def +(y: Number): Number = Number(x) doAdd y

    /**
      * Multiply x by y (a Number) and yield a Number.
      *
      * @param y the multiplicand, a Number.
      * @return a Number whose value is x * y.
      */
    def *(y: Number): Number = Number(x) doMultiply y

    /**
      * Divide x by y (a Number) and yield a Number.
      *
      * @param y the divisor, a Number.
      * @return a Number whose value is x / y.
      */
    def /(y: Number): Number = convertToNumber(Number(x) multiply y.invert)

    /**
      * Divide x by y (an Int) and yield a Number.
      * NOTE: the colon is necessary in order to coerce the left hand operand to be a Number.
      *
      * @param y the divisor, an Int.
      * @return a Number whose value is x / y.
      */
    def :/(y: Int): Number = /(y)

    /**
      * Raise x to the power of y (an Int) and yield a Number.
      *
      * @param y the exponent, an Int.
      * @return a Number whose value is x / y.
      */
    def ^(y: Int): Number = x ^ y

    /**
      * Raise x to the power of y (an Rational) and yield a Number.
      *
      * @param y the exponent, a Rational.
      * @return a Number whose value is x / y.
      */
    def ^(y: Rational): Number = x ^ y
  }

  /**
    * Method to construct a new Number from value, factor and fuzz, according to whether there is any fuzziness.
    *
    * CONSIDER modulate the result so that, in the case of a multiple of Radian, we restrict the range to 0 to 2pi immediately.
    * However, note that this will change the behavior such that it is no longer possible to have the constant 2pi.
    *
    * @param value  the value of the Number, expressed as a nested Either type.
    * @param factor the scale factor of the Number: valid scales are: Scalar, Radian, and NatLog.
    * @param fuzz   the fuzziness of this Number, wrapped in Option.
    * @return a Number.
    */
  def create(value: Value, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = (fuzz match {
    case None => ExactNumber(value, factor)
    case _ => FuzzyNumber(value, factor, fuzz)
  }).specialize

  /**
    * CONSIDER why do we need this method?
    *
    * NOTE not all double values should be given fuzz.
    *
    * @param x      a Double.
    * @param factor a Factor.
    * @return a Number formed from x and factor using standard double precision fuzziness.
    */
  def createFromDouble(x: Double, factor: Factor): Number = apply(x, factor, Some(Fuzziness.doublePrecision))

  def createFromDouble(x: Double): Number = createFromDouble(x, Scalar)

  /**
    * Method to construct a new Number from value, factor and fuzz, according to whether there is any fuzziness.
    *
    * @param value  the value of the Number, expressed as a nested Either type.
    * @param factor the scale factor of the Number: valid scales are: Scalar, Radian, and NatLog.
    * @return a Number.
    */
  def create(value: Value, factor: Factor): Number = create(value, factor, None)

  /**
    * Method to construct a new Number from value, factor and fuzz, according to whether there is any fuzziness.
    *
    * @param value      the value of the Number, expressed as a nested Either type.
    * @param actualFuzz the fuzziness of this Number.
    * @return a Number.
    */
  def create(value: Value, actualFuzz: Fuzziness[Double]): Number = create(value, Scalar, Some(actualFuzz))

  /**
    * Method to construct a new Number from value, factor and fuzz, according to whether there is any fuzziness.
    *
    * @param value the value of the Number, expressed as a nested Either type.
    * @return a Number.
    */
  def create(value: Value): Number = create(value, Scalar)

  /**
    * Method to construct a Number from a String.
    * This is by far the best way of creating the number that you really want.
    *
    * @param x the String representation of the value.
    * @return a Number based on x.
    */
  def apply(x: String, factor: Factor): Number = parse(x) match {
    // CONSIDER we should perhaps process n (e.g. to modulate a Radian value)
    case Success(n) => n.make(factor)
    case Failure(e) => throw NumberExceptionWithCause(s"apply(String, Factor): unable to parse $x", e)
  }

  /**
    * Method to construct a Number from a String.
    * This is by far the best way of creating the number that you really want.
    *
    * @param x the String representation of the value.
    * @return a Number based on x.
    */
  def apply(x: String): Number = parse(x) match {
    // CONSIDER we should perhaps process n (e.g. to modulate a Radian value)
    case Success(n) => n
    case Failure(e) => throw NumberExceptionWithCause(s"apply(String, Factor): unable to parse $x", e)
  }

  /**
    * Method to construct a Number from an Int.
    *
    * @param x      the Int value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Int, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = create(fromInt(x), factor, fuzz)

  /**
    * Method to construct a Number from a BigInt.
    *
    * @param x      the BigInt value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: BigInt, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = apply(Rational(x), factor, fuzz)

  /**
    * Method to construct a Number from a Rational.
    * NOTE: this method is invoked indirectly by parse(String).
    *
    * @param x      the BigInt value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Rational, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = create(fromRational(x), factor, fuzz)

  /**
    * Method to construct a Number from a BigDecimal.
    *
    * @param x      the BigDecimal value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: BigDecimal, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = Number(Rational(x), factor, fuzz)

  /**
    * Method to construct a Number from an optional Double.
    *
    * @param xo     an optional Double.
    * @param factor the appropriate factor
    * @return a Number based on xo.
    */
  def apply(xo: Option[Double], factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = create(fromDouble(xo), factor, fuzz)

  /**
    * Method to construct a Number from a Double.
    *
    * @param x      the Double value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Double, factor: Factor, fuzz: Option[Fuzziness[Double]]): Number = x match {
    case Double.NaN => Number(None, factor, fuzz)
    case _ => Number(Some(x), factor, fuzz)
  }

  /**
    * Method to construct a Number from an Int.
    *
    * @param x      the Int value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Int, factor: Factor): Number = Number(x, factor, None)

  /**
    * Method to construct a Number from a BigInt.
    *
    * @param x      the BigInt value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: BigInt, factor: Factor): Number = Number(x, factor, None)

  /**
    * Method to construct a Number from a Rational.
    *
    * @param x      the BigInt value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Rational, factor: Factor): Number = Number(x, factor, None)

  /**
    * Method to construct a Number from a BigDecimal.
    *
    * @param x      the BigDecimal value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: BigDecimal, factor: Factor): Number = Number(x, factor, None)

  /**
    * Method to construct a Number from an optional Double.
    *
    * @param xo     an optional Double.
    * @param factor the appropriate factor
    * @return a Number based on xo.
    */
  def apply(xo: Option[Double], factor: Factor): Number = Number(xo, factor, None)

  /**
    * Method to construct a Number from a Double.
    *
    * @param x      the Double value.
    * @param factor the appropriate factor
    * @return a Number based on x.
    */
  def apply(x: Double, factor: Factor): Number = Number(x, factor, None)

  /**
    * Method to construct a unit Number with explicit factor.
    *
    * @param factor the appropriate factor
    * @return a unit Number with the given factor.
    */
  def apply(factor: Factor): Number = Number(1, factor)

  /**
    * Method to construct a Number from an Int.
    *
    * @param x the Int value.
    * @return a Number based on x.
    */
  def apply(x: Int): Number = Number(x, Scalar)

  /**
    * Method to construct a Number from a BigInt.
    *
    * @param x a BigInt value.
    * @return a Number based on x.
    */
  def apply(x: BigInt): Number = Number(x, Scalar)

  /**
    * Method to construct a Number from a Rational.
    *
    * @param x a Rational value.
    * @return a Number based on x.
    */
  def apply(x: Rational): Number = Number(x, Scalar)

  /**
    * Method to construct a Number from a BigDecimal.
    *
    * @param x the BigDecimal value.
    * @return a Number based on x.
    */
  def apply(x: BigDecimal): Number = Number(x, Scalar)

  /**
    * Method to construct a Number from an optional Double.
    *
    * @param xo an optional Double.
    * @return a Number based on xo.
    */
  def apply(xo: Option[Double]): Number = Number(xo, Scalar)

  /**
    * Method to construct a Number from a Double.
    *
    * @param x the Double value.
    * @return a Number based on x.
    */
  def apply(x: Double): Number = Number(x, Scalar)

  /**
    * Method to construct an invalid Number.
    *
    * @return a invalid Number.
    */
  def apply(): Number = NaN

  /**
    * Invalid number.
    */
  val NaN: Number = Number(None)

  private val numberParser = NumberParser

  /**
    * Method to parse a String and yield a Try[Number].
    *
    * NOTE: this method indirectly invokes apply(Rational, Factor, Option of Fuzz[Double] )
    *
    * @param w the String to be parsed.
    * @return a Number.
    */
  def parse(w: String): Try[Number] = {
    val ny: Try[Number] = numberParser.parseNumber(w) map (_.specialize)
    ny flatMap (n => if (n.isValid) Success(n) else Failure(NumberException(s"parse: cannot parse $w as a Number")))
  }

  /**
    * Following are the definitions required by Ordering[Number]
    */
  trait NumberIsOrdering extends Ordering[Number] {
    /**
      * When we do a compare on NatLog numbers, they are in the same order as Scalar numbers (i.e. monotonically increasing).
      * It's not necessary to convert exact numbers to fuzzy numbers for this purpose, we simply
      * pretend that the NatLog numbers are Scalar numbers.
      *
      * @param x the first Number.
      * @param y the second Number.
      * @return an Int representing the order.
      */
    def compare(x: Number, y: Number): Int = {
      if (x == Number.NaN && y == Number.NaN) 0
      else if (x == Number.NaN || y == Number.NaN) throw NumberException("cannot compare NaN with non-NaN")
      else if (x.factor == NatLog && y.factor == NatLog)
        compare(x.make(Scalar), y.make(Scalar)) // TESTME why do we need to convert to Scalar?
      else {
        // CONSIDER invoking the compare method in GeneralNumber.
        GeneralNumber.plus(x, Number.negate(y)).signum
      }
    }
  }

  implicit object NumberIsOrdering extends NumberIsOrdering

  /**
    * Following are the definitions required by Numeric[Number]
    */
  trait NumberIsNumeric extends Numeric[Number] with NumberIsOrdering {
    def plus(x: Number, y: Number): Number = GeneralNumber.plus(x, y)

    def minus(x: Number, y: Number): Number = GeneralNumber.plus(x, negate(y))

    def times(x: Number, y: Number): Number = GeneralNumber.times(x, y)

    def negate(x: Number): Number = Number.negate(x)

    def fromInt(x: Int): Number = Number(x)

    def parseString(str: String): Option[Number] = parse(str).toOption

    def toInt(x: Number): Int = toLong(x).toInt

    def toLong(x: Number): Long = x match {
      case z: GeneralNumber => z.maybeRational match {
        case Some(r) => r.toLong
        case None => x.maybeDouble match {
          case Some(z) => Math.round(z)
          case None => throw NumberException("toLong: this is invalid")
        }
      }
    }

    def toDouble(x: Number): Double = x.maybeDouble match {
      case Some(y) => y
      case None => throw NumberException("toDouble: this is invalid")
    }

    def toFloat(x: Number): Float = toDouble(x).toFloat
  }

  /**
    * CONSIDER inlining this method or making it private.
    *
    * @param x the first number.
    * @param y the second number.
    * @return the order.
    */
  def doCompare(x: Number, y: Number): Int = NumberIsOrdering.compare(x, y)

  /**
    * Following are the definitions required by Fractional[Number]
    */
  trait NumberIsFractional extends Fractional[Number] with NumberIsNumeric {
    def div(x: Number, y: Number): Number = GeneralNumber.times(x, y doInvert)
  }

  implicit object NumberIsFractional extends NumberIsFractional with NumberIsNumeric with NumberIsOrdering

  // CONSIDER some of the following should probably be moved to GeneralNumber

  def prepare(no: Option[Number]): Number = no.getOrElse(Number())

  def prepareWithSpecialize(no: Option[Number]): Number = prepare(no).specialize

  /**
    * Method to deal with a Scale factor change.
    *
    * TODO: this will work for FuzzyNumber but only if the fuzz is relative, and even then perhaps not for NatLog conversions.
    *
    * @param n      the Number to be scaled.
    * @param factor the factor to which it should be converted.
    * @return the resulting Number (equivalent in value, but with a potentially different scale factor).
    */
  def scale(n: Number, factor: Factor): Number = (n.factor, factor) match {
    case (a, b) if a == b => n
    case (NatLog, Scalar) => prepare(n.transformMonadic(factor)(MonadicOperationExp))
    case (Scalar, NatLog) => prepare(n.transformMonadic(factor)(MonadicOperationLog))
    case (Root(_), Scalar) if Value.signum(n.value) < 0 => Number.NaN
    case (Root2, Scalar) => prepare(n.transformMonadic(factor)(MonadicOperationSqrt))
    case (NatLog, PureNumber(_)) | (PureNumber(_), NatLog) | (Logarithmic(_), Root(_)) => scale(scale(n, Scalar), factor)
    case (Scalar, Logarithmic(_)) => scale(scale(n, NatLog), factor)
    case (Scalar, Root(f)) => convertScalarToRoot(n, factor, f)
    case (Root(f), NatLog) => convertRootToNatLog(n, factor, f)
    case (PureNumber(_), PureNumber(_)) => prepare(n.factor.convert(n.value, factor) map (v => n.make(v, factor)))
    case (Logarithmic(_), Logarithmic(_)) => prepare(n.factor.convert(n.value, factor) map (v => n.make(v, factor)))
    case (Root(_), Root(_)) => prepare(n.factor.convert(n.value, factor) map (v => n.make(v, factor)))
    case (Logarithmic(_), PureNumber(_)) | (Root(_), Logarithmic(_)) | (Root(_), PureNumber(_)) => scale(scale(n, NatLog), factor)
    case _ => throw NumberException(s"Number.scale: scaling between ${n.factor} and $factor factors is not supported")
  }

  /**
    * Method to change the sign of this Number.
    * The meaning of "change the sign" is in terms of pure numbers (Scalar, Radian).
    * For any other factor, we convert x into Scalar form (which will most likely introduce fuzziness).
    * Imaginary numbers cannot be negated--they must first be converted to Complex form and then negated.
    *
    * @param x a Number to be negated.
    * @return -x unless the negative cannot be represented, in which case a NumberException will be thrown.
    */
  @tailrec
  def negate(x: Number): Number = x.factor match {
    case p@PureNumber(_) => prepare(x.transformMonadic(p)(MonadicOperationNegate))
    case Root(_) if Value.signum(x.value) < 0 => throw NumberException(s"cannot negate imaginary number: $x")
    case _ => negate(x.scale(Scalar))
  }

  def inverse(x: Number): Number = x.factor match {
    case Scalar => prepare(x.transformMonadic(Scalar)(MonadicOperationInvert))
    case f@Root(_) => prepare(x.transformMonadic(f)(MonadicOperationInvert))
    case _ => negate(x.scale(Scalar))
  }

  /**
    * CONSIDER move this to GeneralNumber as an instance method.
    *
    * @param x a Number.
    * @return -1, 0, or 1 according to its sign.
    */
  def signum(x: Number): Int = x match {
    case z: GeneralNumber => z.query(QueryOperationSignum, 0)
  }

  /**
    * Implement sin of a Number.
    * See [[https://en.wikipedia.org/wiki/Sine_and_cosine]].
    *
    * CONSIDER implementing this (and cos) as part of exp method (providing a Complex parameter, of course).
    *
    * CONSIDER implementing this (and cos) by using MonadicOperationSin throughout. But NOTE that said operation will need enhancement before it can work identically.
    *
    * CONSIDER returning an Expression rather than a Number. That would enable an exact result for 1/12 and 5/12 pi.
    *
    * @param x a Number, typically in Radians, but if not, then will be converted.
    * @return a Scalar Number which represents the sine of x.
    */
  def sin(x: Number): Number =
  // TODO much of the logic here is a repeat of what's in transformMonadic.
    x.scale(Radian).transformMonadic(Radian)(MonadicOperationModulate(-1, 1, circular = true)) match {
      case Some(z) =>
        if (z.signum >= 0) {
          lazy val oneOverRoot2 = Number(Rational.half, Root2)
          lazy val rootThreeQuarters = Number(Rational(3, 4), Root2)
          lazy val rootSix = Number(6, Root2)
          val z = x.scale(Radian)
          z.doMultiply(12).toInt match {
            case Some(3) | Some(9) => oneOverRoot2  // pi/4 and 3pi/4
            case Some(4) | Some(8) => rootThreeQuarters // pi/3 and 2pi/3
            case Some(1) | Some(11) => rootSix doSubtract root2 doDivide 4 // pi/12 and 11pi/12 would be nice for this to be an Expression
            case Some(5) | Some(7) => rootSix doAdd root2 doDivide 4 // 5pi/12 and 7pi/12 ditto
            case _ => prepareWithSpecialize(z.transformMonadic(Scalar)(MonadicOperationSin)) // this takes proper care of 0, 2, 6, 10, 12.
          }
        } else negate(sin(negate(x)))

      case None => throw NumberException(s"Number.sin: logic error")
    }

  // CONSIDER checking here for x being zero
  def atan(x: Number, y: Number): Number = doAtan(y doDivide x, x.signum)

  /**
    * Yield the natural log of x.
    * If the factor is NatLog, then we force the factor to be Scalar and simplify.
    * Otherwise, if the factor is Scalar, we first convert it to a NatLog number, then call log recursively.
    * Otherwise, we convert to a Scalar number and call log recursively.
    *
    * @param x a Number.
    * @return the natural log of x.
    */
  @tailrec
  def log(x: Number): Number = x.factor match {
    case NatLog => x.make(Scalar).simplify
    case Scalar => log(x.scale(NatLog))
    case _ => log(x.scale(Scalar))
  }

  /**
    * Yield the exponential of x i.e. e to the power of x.
    * If the factor is Scalar, then we force the factor to be NatLog and simplify.
    * Otherwise, we convert to a Scalar number and call exp recursively.
    *
    * @param x a Number whose factor is NatLog.
    * @return the value of e raised to the power of x.
    */
  @tailrec
  def exp(x: Number): Number = x.factor match {
    case Scalar => x.make(NatLog).simplify
    case _ => exp(x.scale(Scalar))
  }

  /**
    * Method to yield the square root of a Number.
    *
    * @param x the Number whose square root we need.
    * @return the square root of x.
    */
  def sqrt(x: Number): Number = x.factor match {
    case Scalar => x.make(Root2).simplify
    case _ => x.power(Number.half)
  }

  def √(x: Int): Number = x.sqrt

  /**
    * This method returns a Number equivalent to x but with the value in an explicit factor-dependent range.
    * Only Radian is currently fixed within a range (-1 -> 1).
    *
    * @param x the Number to operate on.
    * @return either x or a number equivalent to x with value in defined range.
    */
  def modulate(x: Number): Number = x.factor match {
    case f@Radian => prepare(x.transformMonadic(f)(MonadicOperationModulate(-1, 1, circular = true)))
    case _ => x
  }

  @tailrec
  private def doAtan(number: Number, sign: Int): Number = number.factor match {
    case Scalar => prepareWithSpecialize(number.transformMonadic(Radian)(MonadicOperationAtan(sign))).modulate
    case Root(2) =>
      val ro = number.toRational
      val ry: Try[Rational] = ro map (_.abs) match {
        case Some(Rational(Rational.bigThree, Rational.bigOne)) => Success(Rational(1, 3))
        case Some(Rational(Rational.bigOne, Rational.bigThree)) => Success(Rational(1, 6))
        case None => Failure(NumberException("input to atan is not rational"))
        case _ => Failure(NumberException("rational is not matched"))
      }
      (for (flip <- ro map (_.signum < 0); z <- MonadicOperationAtan(sign).modulateAngle(ry, flip).toOption) yield z) match {
        case Some(r) => Number(r, Radian)
        case None => doAtan(number.scale(Scalar), sign)
      }
    case _ => throw NumberException("number.factor is not matched")
  }

  private def convertScalarToRoot(n: Number, factor: Factor, f: Double) =
    n.doPower(ExactNumber(Value.fromDouble(Some(f)), Scalar)).make(factor)

  private def convertRootToNatLog(n: Number, factor: Factor, f: Double) = {
    val yo = for (x <- n.maybeDouble; z = math.log(x)) yield z / f
    n.make(Value.fromDouble(yo), factor).specialize
  }
}

case class NumberException(str: String) extends Exception(str)

case class NumberExceptionWithCause(str: String, e: Throwable) extends Exception(str, e)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy