diff --git a/README.md b/README.md index d6b02b4b0..ef9fb4681 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Number is organized into multiple modules: #### Current Version This version is 1.10.1 -The latest published version is 1.9.3 (see [HISTORY](docs/HISTORY.md)). +The latest published version is 1.10.5 (see [HISTORY](docs/HISTORY.md)). That should match the version of the latest release on Maven Central in the badge at the top. To use Number in your project, add the following dependency: @@ -59,7 +59,7 @@ libraryDependencies += "com.phasmidsoftware" %% "number" % "1.9.3" com.phasmidsoftware number_3 - 1.9.3 + 1.10.5 ``` diff --git a/build.sbt b/build.sbt index a125ad796..b79acf025 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / organization := "com.phasmidsoftware" -ThisBuild / version := "1.10.4" +ThisBuild / version := "1.10.5" val scalaVersionNumber = "3.7.3" val catsVersion = "2.13.0" @@ -21,6 +21,9 @@ ThisBuild / libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % logbackClassicVersion % "runtime" ) +// CONSIDER removing this (for performance reasons) if publishLocal runs fine without it. +Global / concurrentRestrictions += Tags.limitAll(1) + // ============================================================================ // COMPILER OPTIONS - Moderate settings focused on catching + operator issues // ============================================================================ diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala index 4cf80c8c8..cfd2764ee 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala @@ -198,7 +198,7 @@ case object MonadicOperationExp extends MonadicOperation { * This function operates within a `Try` monad to handle potential failures. */ private lazy val expRat: Rational => Try[Rational] = { - case r if r.isInfinity && r.signum < 0 => + case r if r.isInfinite && r.signum < 0 => Success(Rational.zero) case r => fail("can't do exp Rational=>Rational for non-zero parameter")(r) @@ -845,7 +845,7 @@ case object QueryOperationIsInfinite extends QueryOperation[Boolean] { */ def getFunctions: BooleanQueryFunctions = new QueryFunctions[Boolean] { val fInt: Int => Try[Boolean] = tryF[Int, Boolean](_ => false) - val fRat: Rational => Try[Boolean] = tryF[Rational, Boolean](x => x.isInfinity) + val fRat: Rational => Try[Boolean] = tryF[Rational, Boolean](x => x.isInfinite) val fDouble: Double => Try[Boolean] = tryF[Double, Boolean](x => x == Double.PositiveInfinity || x == Double.NegativeInfinity) } } diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala index 0ab9374ab..6f97a65a1 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala @@ -265,7 +265,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @return true if this NumberLike object is exact in the context of No factor, else false. */ lazy val isExact: Boolean = - !(isInfinity || isNaN) + !(isInfinite || isNaN) /** * Determines if the Rational object represents "not a number" (NaN). @@ -421,17 +421,17 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @note Throws RationalException if the Rational number is infinity. */ lazy val forceToBigDecimal: BigDecimal = - if (!isInfinity) + if (!isInfinite) BigDecimal(n) / BigDecimal(d) else throw RationalException(s"cannot convert infinity to BigDecimal: $this") /** - * Method to determine if the given Rational represents an infinity. + * Method to determine if the given Rational represents an infinity (positive or negative). * * @return true if the denominator is zero; false otherwise. */ - lazy val isInfinity: Boolean = + lazy val isInfinite: Boolean = d == bigZero && n != bigZero /** @@ -531,7 +531,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @return the mediant of this and other. */ def mediant(other: Rational): Rational = - if (signum >= 0 && other.signum >= 0 && !isInfinity && !other.isInfinity) + if (signum >= 0 && other.signum >= 0 && !isInfinite && !other.isInfinite) Rational(n + other.n, d + other.d) else NaN @@ -615,7 +615,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { "NaN" case _ if isZero && d < 0 => "-0" - case _ if isInfinity => + case _ if isInfinite => (if (n > 0) "" else "-") + "∞" case _ if isWhole => toBigInt.toString @@ -1329,7 +1329,7 @@ object Rational { if (x.isWhole) Success(x.n) else - Failure(RationalException(s"toBigInt: $x is " + (if (x.isInfinity) + Failure(RationalException(s"toBigInt: $x is " + (if (x.isInfinite) "infinite" else "not whole"))) /** @@ -1356,7 +1356,7 @@ object Rational { */// CONSIDER making this private or moving back into RationalSpec def hasCorrectRatio(r: Rational, top: BigInt, bottom: BigInt): Boolean = { val _a = r * bottom - val result = bottom == 0 || _a.isInfinity || (_a.isWhole && _a.toBigInt == top) + val result = bottom == 0 || _a.isInfinite || (_a.isWhole && _a.toBigInt == top) if (!result) throw RationalException(s"incorrect ratio: r=${r.n}/${r.d}, top=$top, bottom=$bottom, _a=${_a}, gcd=${top.gcd(bottom)}") result diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala index f69c6a501..de1832eb8 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala @@ -92,7 +92,7 @@ abstract class BaseComplex(val real: Number, val imag: Number) extends Complex { * @return the sum. */ def add(x: Field): Field = - sum(narrow(x, polar = false)) + sum(narrow(x, polar = false)) // CONSIDER dealing with infinity. /** * Method to add this to the given parameter (a Cartesian). diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala index 81515fc82..6b35eb719 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala @@ -148,6 +148,16 @@ object Constants { */ lazy val iPi: Complex = ComplexCartesian(0, Number.pi) + /** + * Represents a constant Field value of "Not a Number" (NaN). + * NaN is used to signify an undefined or unrepresentable value, especially + * in numerical computations that result in invalid operations like division by zero + * or operations involving undefined values. + * + * This value is of type Real and wraps the predefined NaN value from the Number type. + */ + lazy val NaN: Field = Real(Number.NaN) + /** * Represents the square root of 2 as a field constant. * CONSIDER making this a Complex. diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala index 3ef3d2901..e81bc4002 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala @@ -299,6 +299,12 @@ trait Number extends Fuzz[Double] with Ordered[Number] with Numerical { * @return the result of the multiplication as a Field */ def multiply(x: Field): Field = (this, x) match { + case (Constants.zero, _) if x.isInfinite => + Constants.NaN + case (_, Constants.zero) if this.isInfinite => + Constants.NaN + case (a, b) if a.isInfinite || b.isInfinite => + Constants.infinity case (Number.zero, _) | (_, Constants.zero) => Constants.zero case (Number.one, _) => @@ -834,6 +840,13 @@ object Number { * Exact value of i */ lazy val i: Number = ExactNumber(-1, SquareRoot) + + /** + * Represents the mathematical concept of infinity as a lazy value of type `Number`. + * The value is constructed using an exact representation of infinity via the `ExactNumber` class. + */ + lazy val `∞`: Number = ExactNumber(Rational.infinity) + /** * Exact value of the Number √2 (not Complex) */ diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala index 02de07948..8e0d94fc8 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala @@ -114,6 +114,10 @@ case class Real(x: Number) extends Field { * @return the sum. */ def add(y: Field): Field = y match { + case field if this.isInfinite && field.isInfinite && signum != field.signum => + Constants.NaN + case field if this.isInfinite || field.isInfinite => + Constants.infinity case multivariate: Multivariate => multivariate.add(this) case Real(r) => diff --git a/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala b/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala index 6e5c98ce1..48cc06112 100644 --- a/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala +++ b/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala @@ -20,7 +20,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat private val zeroSymbol = Symbol("zero") private val toBigIntSymbol = Symbol("toBigInt") private val wholeSymbol = Symbol("whole") - private val infinitySymbol = Symbol("infinity") + private val infinitySymbol = Symbol("infinite") private val unitySymbol = Symbol("unity") private val narrowSymbol = Symbol("narrow") private val normalizeSymbol = Symbol("normalize") @@ -34,7 +34,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat } it should "yield infinity for 1, 0" in { val r = new Rational(BigInt(1), 0L) - r.isInfinity shouldBe true + r.isInfinite shouldBe true } it should "yield NaN for 0, 0" in { new Rational(BigInt(0), 0L).isNaN shouldBe true @@ -101,18 +101,18 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat } it should "work for 1, 0" in { val r = Rational(1, 0) - r.isInfinity shouldBe true + r.isInfinite shouldBe true r shouldBe Rational.infinity r.render shouldBe "∞" } it should "work for -1, 0" in { val r = Rational(-1, 0) - r.isInfinity shouldBe true + r.isInfinite shouldBe true r.render shouldBe "-∞" } it should "work for -1, -2" in { val r = Rational(-1, -2) - r.isInfinity shouldBe false + r.isInfinite shouldBe false r shouldBe Rational.half } it should "work for -2624712818L, -1" in { @@ -829,7 +829,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat behavior of "r-interpolator" it should "work for -1/0" in { val r = r"-1/0" - r.isInfinity shouldBe true + r.isInfinite shouldBe true r shouldBe Rational(-1, 0) } it should "work for 1/-2147483648" in { diff --git a/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala b/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala index 817fd4505..eac3fad8a 100644 --- a/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala +++ b/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala @@ -1,21 +1,21 @@ package com.phasmidsoftware.number.core.numerical -import com.phasmidsoftware.number.core.inner.PureNumber +import com.phasmidsoftware.number.core.inner.{PureNumber, Rational} +import com.phasmidsoftware.number.core.numerical.Real.RealIsFractional.mkNumericOps import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should class FieldSpec extends AnyFlatSpec with should.Matchers { behavior of "Field" -// it should "isExact" in { -// val x = (ConstPi / 2).materialize -// x.isExact shouldBe true -// } -// it should "multiply i by itself correctly" in { -// val z = Expression(Constants.i) * Constants.i -// val result = z.materialize -// result shouldBe Constants.minusOne -// } + it should "isExact" in { + val x = (Constants.pi / Real(2)) + x.isExact shouldBe true + } + it should "multiply i by itself correctly" in { + val z = Constants.i * Constants.i + z shouldBe ComplexCartesian(-1, 0) + } it should "add" in { val one = Number.one val result = one `add` Constants.i @@ -31,4 +31,29 @@ class FieldSpec extends AnyFlatSpec with should.Matchers { x should matchPattern { case Real(FuzzyNumber(Left(Left(Some(0.6931471805599453))), PureNumber, _)) => } x.render shouldBe "0.69314718055994530[89]" } + it should "add infinity to infinity" in { + val result = Constants.infinity `add` Constants.infinity + result shouldBe Constants.infinity + } + it should "add infinity to negative infinity" in { + val result = Constants.infinity `add` Real(Rational(-1).invert) + result shouldBe Constants.infinity + } + it should "add infinity to a finite number" in { + val result = Constants.infinity `add` Real(1) + result shouldBe Constants.infinity + } + it should "multiply infinity by zero" in { + val result = Constants.infinity `multiply` Constants.infinity + result shouldBe Constants.infinity + } + it should "multiply infinity by a finite number" in { + val result = Constants.infinity `multiply` Real(1) + result shouldBe Constants.infinity + } + it should "divide infinity by infinity" in { + val result = Constants.infinity `divide` Constants.infinity + result shouldBe Constants.NaN + } + } diff --git a/docs/HISTORY.md b/docs/HISTORY.md index a3ccdf807..3e73916e3 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,7 +1,7 @@ # Migration and Version History ## Versions - +* Version 1.10.5: Fixed various issues (mostly regarding infinity) exposed by the EML expansion. * Version 1.10.1: Fixed issue with over-precise Rational numbers. * Version 1.10.0: Improved the Mermaid diagrams, see [docs/diagrams/index.md](docs/diagrams/index.md). * Version 1.9.3: diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala index 5cb410e97..677dba86e 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala @@ -129,7 +129,7 @@ case class Aggregate(function: ExpressionBiFunction, xs: Seq[Expression]) extend case Product => x => if x < 1 then 1 / x else x case _ => - throw new IllegalArgumentException("complementaryTermsEliminatorAggregate: Power function not supported") + throw new IllegalArgumentException("eliminateComplementaryTerms: Power function not supported") } // NOTE this ordering is really only appropriate when f is Sum. diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala index 4f433c5bd..185fcbcef 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala @@ -5,7 +5,7 @@ package com.phasmidsoftware.number.expression.expr import com.phasmidsoftware.number.algebra.core.* -import com.phasmidsoftware.number.algebra.eager.{Complex, Eager, Real} +import com.phasmidsoftware.number.algebra.eager.{Complex, Eager, InversePower, Real} import com.phasmidsoftware.number.core.inner.Factor import com.phasmidsoftware.number.expression.expr.Expression.em @@ -148,6 +148,16 @@ case class Noop(w: String) extends AtomicExpression { def evaluate(context: Context): Option[Eager] = throw new UnsupportedOperationException(s"Can''t evaluate: $this") + /** + * Represents a lazy evaluation result of this `Noop` expression as-is, without any modification or processing. + * + * This method is overridden and fixed to always return `None`, indicating that + * the `Noop` expression does not inherently evaluate to any meaningful result. + * + * @return an `Option[Eager]`, which is always `None`. + */ + override lazy val evaluateAsIs: Option[Eager] = None + /** * Method to render this Renderable in a presentable manner. * @@ -207,5 +217,6 @@ case class Noop(w: String) extends AtomicExpression { } object Noop { + def apply: Noop = Noop("NaN") val TEST_STRING = "Error tester" } diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala index 948fa1c20..ebf27b3ed 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala @@ -8,7 +8,7 @@ import cats.implicits.catsSyntaxEq import com.phasmidsoftware.number.algebra.core.* import com.phasmidsoftware.number.algebra.eager import com.phasmidsoftware.number.algebra.eager.Eager.eagerToField -import com.phasmidsoftware.number.algebra.eager.{Angle, Complex, Eager, InversePower, IsInteger, NaturalExponential, QuadraticSolution, RationalNumber, Structure, WholeNumber} +import com.phasmidsoftware.number.algebra.eager.{Angle, Complex, Eager, InversePower, IsInteger, QuadraticSolution, RationalNumber, Structure, WholeNumber} import com.phasmidsoftware.number.core.inner.{Factor, PureNumber, Radian, Rational} import com.phasmidsoftware.number.core.numerical import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar, Number, Real, Complex as CoreComplex} @@ -109,8 +109,17 @@ case class BiFunction(a: Expression, b: Expression, f: ExpressionBiFunction) ext */ lazy val structuralMatcher: em.AutoMatcher[Expression] = em.Matcher[Expression, Expression]("BiFunction:structuralMatcher") { // NOTE: Canonical ordering — swap commutative operands if out of order. + case BiFunction(Noop(_), _, _) | BiFunction(_, Noop(_), _) => + em.Match(Noop.apply) + case BiFunction(Infinity, UniFunction(Infinity, Negate), Sum) | BiFunction(UniFunction(Infinity, Negate), Infinity, Sum) => + em.Match(Noop.apply) case IsCommutative(x, y, f) if summon[Ordering[Expression]].gt(x, y) => - em.Match(BiFunction(y, x, f)) + em.Match(BiFunction(y, x, f)).flatMap(structuralMatcher) + case BiFunction(Zero, Infinity, Product) | BiFunction(Infinity, Zero, Product) => + em.Match(Noop.apply) + // NOTE: any other BiFunction involving Infinity results in Infinity + case BiFunction(Infinity, _, _) | BiFunction(_, Infinity, _) => + em.Match(Infinity) case BiFunction(a, b, Sum) if a == b => em.Match(a * Two) case BiFunction(BiFunction(w, x, Power), BiFunction(y, z, Power), Product) if w == y => @@ -435,7 +444,7 @@ case class BiFunction(a: Expression, b: Expression, f: ExpressionBiFunction) ext case (E, ValueExpression(Complex(c), _)) if c.isImaginary && c.modulus == numerical.Number.pi => em.Match(MinusOne) case (E, ValueExpression(v: eager.Number, _)) => - em.Match(Literal(NaturalExponential(v))) + em.Match(UniFunction(v, Exp)) case (E, UniFunction(x, Ln)) => em.Match(x) case (E, BiFunction(x, E, Log)) => diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala index 2ccd65b1b..523660069 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala @@ -101,10 +101,10 @@ trait Expression extends Lazy with Approximate { /** * Evaluates this `Expression` in the context of `AnyContext` without simplification or factor-based conversion. - * This allows obtaining a direct evaluation of the `Expression` as a `Field`, if possible. + * This allows obtaining a direct evaluation of the `Expression` as a `Eager`, if possible. * NOTE: no simplification or factor-based conversion occurs here. * - * @return an `Option[Field]` containing the evaluated `Field` if evaluation is successful, or `None` otherwise. + * @return either a `Some[Eager]` containing the value if evaluation is successful, or `None` otherwise. */ lazy val evaluateAsIs: Option[Eager] = evaluate(AnyContext) @@ -400,7 +400,7 @@ object Expression { * @return an Expression representing the lazy product of this expression and the given expression. */ infix def times(y: Expression): Expression = y match { - case IsZero(_) => + case IsZero(_) if x != Infinity => Zero case IsUnity(_) => x @@ -627,7 +627,7 @@ object Expression { case Eager.infinity => Infinity case _ => - Literal(x) + Literal(x) // CONSIDER invoking ValueExpression instead } @deprecated("Use puremath or lazymath string interpolators instead", "1.6.5") @@ -918,11 +918,16 @@ object Expression { */ given Ordering[Expression] with def compare(x: Expression, y: Expression): Int = (x, y) match - case (a: AtomicExpression, b: AtomicExpression) => atomRank(a) - atomRank(b) - case (_: AtomicExpression, _: CompositeExpression) => -1 - case (_: CompositeExpression, _: AtomicExpression) => 1 - case (a: CompositeExpression, b: CompositeExpression) => a.depth - b.depth - case (a, b) => a.compare(b) // This case should never occur + case (a: AtomicExpression, b: AtomicExpression) => + atomRank(a) - atomRank(b) + case (_: AtomicExpression, _: CompositeExpression) => + -1 + case (_: CompositeExpression, _: AtomicExpression) => + 1 + case (a: CompositeExpression, b: CompositeExpression) => + a.depth - b.depth + case (a, b) => + a.compare(b) // This case should never occur private def atomRank(e: AtomicExpression): Int = e match case Noop(_) => 0 diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala index a6134cb9d..b14ed95bb 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala @@ -434,7 +434,7 @@ class ExpressionMatchers(using val matchLogger: MatchLogger) extends MatchersExt // CONSIDER the following: // Match(Aggregate(f, list)) `map` (_.simplify) else - Miss(s"complementaryTermsEliminatorAggregate: $a", a) + Miss(s"matchOrMiss: $a", a) /** * Matches an expression to its additive identity based on its components. diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala index 56aa213e2..18f3a84f2 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala @@ -144,7 +144,13 @@ case class UniFunction(x: Expression, f: ExpressionMonoFunction) extends Composi */ lazy val identitiesMatcher: em.AutoMatcher[Expression] = em.Matcher("UniFunction:identitiesMatcher") { + // Noop special case + case UniFunction(Noop(_), _) => + em.Match(Noop.apply) + // Zero special cases + case UniFunction(Zero, Exp) => + em.Match(One) case UniFunction(Zero, f@Odd()) if f != Reciprocal => // NOTE if the function has odd parity, then f(0) = 0. em.Match(Zero) case UniFunction(Zero, Cosh | Cosine) if f != Reciprocal => @@ -165,6 +171,8 @@ case class UniFunction(x: Expression, f: ExpressionMonoFunction) extends Composi em.Match(MinusOne) // Match Log and Exp cases + case UniFunction(Zero, Ln) => + em.Match(UniFunction(Infinity, Negate)) case UniFunction(IsEuler(Euler(r, θ)), Ln) => em.Match((UniFunction(r, Ln) + (I * θ)).simplify) // XXX Take care of the cases whereby the inverse of a log expression is a log expression with operand and base swapped. diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala index 48614c789..9a64ccd59 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala @@ -224,6 +224,8 @@ object ValueExpression { /** * Creates a `ValueExpression` instance by wrapping the provided `Valuable` object. + * NOTE: client code should not normally invoke this method directly. + * Prefer to use the `Expression.apply` method for creating `ValueExpression` instances. * * @param v the `Eager` instance to be encapsulated within a `ValueExpression`. * @return a `ValueExpression` representing the provided `Eager` object, with its rendered representation. @@ -409,7 +411,7 @@ object Literal { One case numerical.Number.zero => Zero - case numerical.Number.pi => + case numerical.Number.pi | numerical.Number.`𝛑` => Pi case numerical.Number.two => Two @@ -419,7 +421,10 @@ object Literal { Half case numerical.Number.e => E + case numerical.Number.`∞` => + Infinity case _ => + // XXX take care that we do not recurse infinitely here. ValueExpression(Scalar(x)) } diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala index 9ebaf60aa..f99feb7c6 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala @@ -320,8 +320,8 @@ class CompositeExpressionSpec extends AnyFlatSpec with Matchers { val simplified = doubleSum.simplify simplified shouldBe a[BiFunction] simplified.asInstanceOf[BiFunction].f shouldBe Product - simplified.asInstanceOf[BiFunction].a shouldBe Two - simplified.asInstanceOf[BiFunction].b shouldBe x + simplified.asInstanceOf[BiFunction].a shouldBe x + simplified.asInstanceOf[BiFunction].b shouldBe Two } it should "evaluate (named) constant sum completely" in { diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala new file mode 100644 index 000000000..dff1426be --- /dev/null +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala @@ -0,0 +1,14 @@ +// src/test/scala/com/phasmidsoftware/number/expression/ExpressionMatchers.scala +package com.phasmidsoftware.number.expression.expr + +import org.scalatest.matchers.{MatchResult, Matcher, should} + +trait ExpressionMatcherPatterns { + self: should.Matchers => + def simplifyAs(expected: Expression): Matcher[Expression] = + (left: Expression) => MatchResult( + left.simplify == expected, + s"${left.render} did not simplify to ${expected.render}, got ${left.simplify.render}", + s"${left.render} simplified to ${expected.render}" + ) +} \ No newline at end of file diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala index 74b826465..6a50f350e 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala @@ -513,7 +513,7 @@ class ExpressionMatchersSpec extends AnyFlatSpec with should.Matchers with Befor } it should "work for Ln Zero" in { val x = expr.UniFunction(Zero, Ln) - x.simplify shouldBe Expression(infinity.negate) + x.simplify shouldBe UniFunction(Infinity, Negate) } it should "work for Ln One" in { val x = expr.UniFunction(One, Ln) diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala index 2094a1606..2efa9c7a9 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala @@ -12,9 +12,10 @@ import com.phasmidsoftware.number.algebra.core.FuzzyEq.~= import com.phasmidsoftware.number.algebra.eager.* import com.phasmidsoftware.number.algebra.eager.Eager.eagerToField import com.phasmidsoftware.number.algebra.eager.RationalNumber.half -import com.phasmidsoftware.number.core.inner.{NatLog, Rational} +import com.phasmidsoftware.number.algebra.eager.Real.∞ +import com.phasmidsoftware.number.core.inner.Rational import com.phasmidsoftware.number.core.numerical -import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar, ExactNumber} +import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar} import com.phasmidsoftware.number.expression.algebraic.QuadraticEquation import com.phasmidsoftware.number.expression.core.FuzzyEquality import com.phasmidsoftware.number.expression.expr @@ -26,7 +27,7 @@ import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should -class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter with FuzzyEquality { +class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter with FuzzyEquality with ExpressionMatcherPatterns { type OldExpression = com.phasmidsoftware.number.expression.expr.Expression type OldNumber = com.phasmidsoftware.number.core.numerical.Number @@ -89,6 +90,26 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte } } + behavior of "infinity" + + def simplifiesTo(e: Expression, expected: Expression): org.scalatest.Assertion = + e.simplify shouldBe expected + + it should "obey the Infinity rules" in { + Expression(∞) shouldBe Infinity + simplifiesTo(-Infinity + Infinity, Noop.apply) + simplifiesTo(Infinity + -Infinity, Noop.apply) + simplifiesTo(Zero * Infinity, Noop.apply) + simplifiesTo(Infinity * Zero, Noop.apply) + } + + it should "obey the Noop rules" in { + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(-Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + } + behavior of "parse" // private val syp: ShuntingYardParser.type = ShuntingYardParser @@ -460,11 +481,11 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte } it should "aggregate 2" in { val target = (One * Pi * Two * MinusOne).simplify - target shouldBe -(Pi * 2) + target shouldBe ∅ * -2 * Pi } it should "evaluate e * e" in { val expression: Expression = E * E - expression.simplify shouldBe Literal(Eager(numerical.Real(ExactNumber(2, NatLog)))) + expression.simplify shouldBe UniFunction(2, Exp) } // TODO Issue #140 it should "evaluate phi * phi" in { @@ -511,7 +532,7 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte val y: em.MatchResult[Expression] = x.structuralMatcher(x) y shouldBe em.Match(BiFunction(E, ValueExpression(2), Power)) val simplified = y.get.simplify - simplified shouldBe Literal(Eager(numerical.Real(ExactNumber(2, NatLog)))) + simplified shouldBe UniFunction(2, Exp) } it should "evaluate phi * phi" in { val phi = expr.Root(QuadraticEquation.goldenRatioEquation, 0) diff --git a/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala b/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala index a2ff62919..fa1287575 100644 --- a/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala +++ b/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala @@ -66,7 +66,7 @@ class ExpressionParserSpec extends AnyFlatSpec with should.Matchers { it should "lazymath e" in { lazymath"""\e""" shouldBe E lazymath"""\mathrm{e}""" shouldBe E - lazymath"""\e^2""" shouldBe Literal(NaturalExponential(WholeNumber(2)), Some("e^2")) + lazymath"""\e^2""" shouldBe UniFunction(2, Exp) } it should "puremath functions" in { puremath"""\sin(\pi)""" shouldBe UniFunction(Pi, Sine) diff --git a/src/main/scala/com/phasmidsoftware/number/core/Random.sc b/src/main/scala/com/phasmidsoftware/number/core/Random.sc deleted file mode 100644 index e69de29bb..000000000 diff --git a/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala b/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala index fdb424293..aeaa9d5d7 100644 --- a/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala +++ b/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala @@ -13,8 +13,8 @@ import com.phasmidsoftware.number.algebra.eager.Angle.𝛑 import com.phasmidsoftware.number.algebra.eager.Eager.{e, half, minusOne, negInfinity, one, pi, two, zero} import com.phasmidsoftware.number.core import com.phasmidsoftware.number.core.inner.Rational -import com.phasmidsoftware.number.core.inner.Rational.infinity import com.phasmidsoftware.number.core.numerical +import com.phasmidsoftware.number.core.numerical.{Box, RelativeFuzz} import com.phasmidsoftware.number.expression.expr import com.phasmidsoftware.number.expression.expr.{Expression, *} import org.scalactic.Equality @@ -245,6 +245,17 @@ class TopSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter { lazymath"\ln{\e}" shouldBe One math"\ln{\e}" shouldBe one } + it should "work for Ln e^x" in { + lazymath"\ln{\e^2}" shouldBe Two + lazymath"\e^{\ln{2}}" shouldBe Two + } + it should "work for e^{e^e-1}" in { + lazymath"\e^{(-1 + \e^\e)}" shouldBe UniFunction(BiFunction(-1, UniFunction(E, Exp), Sum), Exp) + lazymath"\e^{(\e^\e + -1)}" shouldBe UniFunction(BiFunction(UniFunction(E, Exp), -1, Sum), Exp) + math"\e^{(-1 + \e^\e)}" shouldBe Real(1403194.8655310909, Some(RelativeFuzz(1.0715681594829235E-14, Box))) + math"\e^{(\e^\e + -1)}" shouldBe Real(1403194.8655310909, Some(RelativeFuzz(1.0715681594829235E-14, Box))) + } + it should "work for Sine 0, etc." in { lazymath"\sin(0)" shouldBe Zero math"\sin(0)" shouldBe zero