diff --git a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs
index fdfe1d497..dee989172 100644
--- a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs
+++ b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs
@@ -406,6 +406,22 @@ public static Interval ToEntity(this (Entity left, bool leftClosed, Entity right
///
public static Entity Factorize(this string expr) => expr.ToEntity().Factorize();
+ ///
+ /// Parses the given expression and writes it as a single fraction: one numerator over one
+ /// denominator, nothing cancelled. See .
+ ///
+ /// The expression as one fraction, or unchanged where it has no division in it
+ ///
+ ///
+ /// Console.WriteLine("a + b/c".AsSingleFraction());
+ ///
+ /// Prints
+ ///
+ /// (a * c + b) / c
+ ///
+ ///
+ public static Entity AsSingleFraction(this string expr) => expr.ToEntity().AsSingleFraction();
+
///
/// Subsitutes a variable by replacing all its occurances with the given value
///
diff --git a/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs
index 9fe022b27..e3fbc7626 100644
--- a/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs
+++ b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs
@@ -164,6 +164,96 @@ private sealed class NumericContentTransformation : Transformation
=> Functions.NumericContent.Extracted(input);
}
+ ///
+ /// Writes an expression as a single fraction, as
+ /// does: one numerator over one denominator, nothing divided inside either, and nothing
+ /// cancelled or multiplied out. Where there is no division the input comes back, and where
+ /// dividing by a fraction moves its denominator into the numerator the answer says that
+ /// denominator is nonzero.
+ ///
+ ///
+ /// Held in a nested class for the reason is.
+ ///
+ public static Transformation AsSingleFraction => SingleFractionHolder.Instance;
+
+ private static class SingleFractionHolder
+ {
+ [ConstantField]
+ internal static readonly Transformation Instance = new SingleFractionTransformation();
+ }
+
+ private sealed class SingleFractionTransformation : Transformation
+ {
+ public override string Name => "single-fraction";
+ public override TransformationRelation Relation => TransformationRelation.Equivalence;
+
+ // Sound: a sum or a product of quotients is defined exactly where each quotient is,
+ // and so is the fraction it is gathered into, since nothing is cancelled. Turning a
+ // quotient over is the one step that moves a denominator out of the way:
+ // (a/b)/(c/d) is (a d)/(b c), which has a value where d is zero and the expression
+ // has none, so the answer carries `provided not d = 0`.
+ public override Soundness Soundness => Soundness.Sound;
+
+ // The two halves are tidied each on its own -- the operands put in order and like
+ // terms collected, which is Simplify's own tidying pass without its search, so
+ // `t + 1 + t^2 + 1` is `2 + t + t^2` -- and the quotient is not, so that no factor of
+ // the numerator meets one of the denominator and cancels.
+ protected override Entity? ApplyCore(Entity input)
+ {
+ // A number is already what it is; there is nothing in it to gather.
+ if (input is Entity.Number)
+ return input;
+ // A rational number is a fraction too, as it is written: 2/3 + x/2 is (3 x + 4)/6,
+ // not (x + 4/3)/2. Inside a function's argument it folds back when its half is
+ // tidied, since the argument is not gathered.
+ var written = input.Replace(static node => node is Entity.Number.Rational { ERational: var value } and not Entity.Number.Integer
+ ? new Entity.Divf(Entity.Number.Integer.Create(value.Numerator), Entity.Number.Integer.Create(value.Denominator))
+ : node);
+ var carried = new List();
+ var (numerator, denominator) = Functions.SingleQuotient.OverLeastCommonDenominator(written, carried);
+ if (denominator == Entity.Number.Integer.One && carried.Count == 0)
+ return input;
+ var top = Tidied(numerator);
+ var bottom = Tidied(denominator);
+ var fraction = bottom == Entity.Number.Integer.One ? top : new Entity.Divf(top, bottom);
+
+ // Only what the new denominator does not already exclude: in (1/x)/(1/x), which
+ // is x/x, the x carried up is still in the denominator.
+ var nonzero = new List();
+ foreach (var factor in carried)
+ {
+ var tidied = Tidied(factor);
+ if (!AlreadyNonzero(tidied, bottom) && !nonzero.Contains(tidied))
+ nonzero.Add(tidied);
+ }
+ if (nonzero.Count == 0)
+ return fraction;
+ var condition = !nonzero[0].EqualTo(0);
+ for (var i = 1; i < nonzero.Count; i++)
+ condition &= !nonzero[i].EqualTo(0);
+ return new Entity.Providedf(fraction, condition);
+ }
+
+ // Simplify's tidying pass, and the neatening it ends on, which writes `x^2 + -1` as
+ // `x^2 - 1`.
+ private static Entity Tidied(Entity half)
+ => Functions.Simplificator.SimplifyChildren(half).Rewrite(RewriteRules.NumericNeat);
+
+ // Whether `denominator` being nonzero already says `factor` is: a nonzero number
+ // always is, and otherwise each factor of `factor` has to be one of the
+ // denominator's, up to a whole positive power, since a product vanishes exactly where
+ // one of its factors does and a power exactly where its base does.
+ private static bool AlreadyNonzero(Entity factor, Entity denominator)
+ {
+ var excluded = Entity.Mulf.LinearChildren(denominator).Select(Base).ToList();
+ return Entity.Mulf.LinearChildren(factor).All(piece =>
+ piece is Entity.Number.Complex { IsZero: false } || excluded.Contains(Base(piece)));
+
+ static Entity Base(Entity piece)
+ => piece is Entity.Powf(var @base, Entity.Number.Integer power) && power.EInteger.Sign > 0 ? @base : piece;
+ }
+ }
+
///
/// The rule-based half of , without the polynomial
/// layer.
diff --git a/Sources/AngouriMath/Docs/Contributing/Transformations.md b/Sources/AngouriMath/Docs/Contributing/Transformations.md
index 0cb49b7f6..711cb8743 100644
--- a/Sources/AngouriMath/Docs/Contributing/Transformations.md
+++ b/Sources/AngouriMath/Docs/Contributing/Transformations.md
@@ -55,6 +55,7 @@ on hashing or on which type loaded first.
Entity.Simplify(level) -> Transformation.SimplificationAtLevel(level) -> Simplificator.Simplify
Entity.Expand(level) -> Transformation.ExpansionAtLevel(level) -> Entity.ExpandOverSum
Entity.Factorize(level) -> Transformation.FactorizationAtLevel(level) -> RewriteRules, composed
+Entity.AsSingleFraction() -> Transformation.AsSingleFraction -> SingleQuotient.OverLeastCommonDenominator, each half tidied
Entity.Differentiate(x) -> Transformation.Differentiation(x) -> Entity.DifferentiateOnce
Entity.Integrate(x) -> Transformation.Integration(x) -> Integration.ComputeIndefiniteIntegral
Entity.Limit(x, to, side) -> Transformation.LimitAt(x, to, side) -> LimitFunctional.ComputeLimit
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
index 7d111ef5d..6db8d75a0 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
@@ -747,6 +747,39 @@ private static Entity CollectLikeTerms(Entity expanded)
public Entity Factorize(int level = 2)
=> Transformation.FactorizationAtLevel(level).ApplyOrKeep(this);
+ ///
+ /// This expression written as a single fraction: one numerator over one denominator, with
+ /// nothing divided inside either, as a + b/c is written (a c + b)/c. Any
+ /// expression, functions included, and nothing cancelled or multiplied out, so the factors
+ /// of the denominator stay as they were: 1/(t^2 + 1) + 1/(t + 1) is
+ /// (2 + t + t^2) / ((t^2 + 1)(t + 1)), and x/x stays x/x. The common
+ /// denominator is the least common multiple of the denominators as they are written, so
+ /// 1/x + 1/x^2 is (x + 1)/x^2; nothing is factorised to find it. A rational number
+ /// counts as the fraction it is written as, so 2/3 + x/2 is (4 + 3x)/6. An
+ /// expression with no division in it comes back as it was. Dividing by a fraction moves
+ /// its denominator into the numerator, and the expression has no value where that
+ /// denominator is zero, so the answer says so: 1/(1/x) is x provided not x = 0.
+ ///
+ ///
+ /// never does this, since one fraction is not always the
+ /// simpler form: 1/x + 1/y reads more easily than (x + y)/(x y). For a
+ /// rational function in lowest terms, the denominator multiplied out and monic, use
+ /// , which is a canonical form and
+ /// declines anything with a function in it.
+ /// #1239
+ ///
+ ///
+ ///
+ /// Console.WriteLine("sin(x) + 1/cos(x)".ToEntity().AsSingleFraction());
+ ///
+ /// Prints
+ ///
+ /// (1 + cos(x) * sin(x)) / cos(x)
+ ///
+ ///
+ public Entity AsSingleFraction()
+ => Transformation.AsSingleFraction.ApplyOrKeep(this);
+
///
/// Simplifies an equation ( e.g. (x - y) * (x + y) -> x^2 - y^2, but 3 * x + y * x = (3 + y) * x )
///
diff --git a/Sources/AngouriMath/Functions/SingleQuotient.cs b/Sources/AngouriMath/Functions/SingleQuotient.cs
index af3a81369..3788e2978 100644
--- a/Sources/AngouriMath/Functions/SingleQuotient.cs
+++ b/Sources/AngouriMath/Functions/SingleQuotient.cs
@@ -17,7 +17,7 @@ namespace AngouriMath.Functions
///
///
///
- /// This is what other systems call together or ratsimp, and it is deliberately
+ /// This is writing an expression as a single fraction, and it is deliberately
/// not part of . Putting a sum over a common
/// denominator makes some expressions worse — 1/x + 1/y is easier to read than
/// (x + y)/(x*y) — which is why every system that has it keeps it as an operation you
@@ -34,6 +34,12 @@ namespace AngouriMath.Functions
/// distribution and is integrated at once, and was declined for want of it.
///
///
+ /// The public entry is , through
+ /// , which
+ /// gathers through , reads a rational number as the
+ /// fraction it is written as, and tidies each half.
+ ///
+ ///
/// No cancellation. The two halves are returned as built, with no common factor taken
/// out: x/x comes back as (x, x) and not as (1, 1). Cancelling needs a
/// gcd, which needs to know what the expression is a polynomial in, and this runs
@@ -44,7 +50,7 @@ namespace AngouriMath.Functions
/// It always terminates and never grows without bound, because it recurses only into
/// the operands of the node it is given and each recursion is on a strictly smaller tree. What
/// it can do is make the tree bigger — combining a sum of n quotients multiplies the
- /// denominators — so is a transformation to ask for rather than one to apply
+ /// denominators — so is a transformation to ask for rather than one to apply
/// on the way past.
///
///
@@ -55,36 +61,60 @@ internal static class SingleQuotient
/// expression that had no division in it, which is the signal that nothing was combined.
///
internal static (Entity Numerator, Entity Denominator) Of(Entity expr)
+ => Of(expr, carried: null, leastCommon: false);
+
+ ///
+ /// The expression as one fraction, written the way it is by hand: a sum goes over the
+ /// least common multiple of its terms' denominators as they stand, each factor to the
+ /// highest power any of them has it and the whole numbers by their least common multiple,
+ /// so 1/x + 1/x^2 is (x + 1, x^2) and not (x^2 + x, x^3). Nothing is
+ /// factorised to find it: x^2 - 1 and x + 1 share no factor here. Each
+ /// denominator that turning a quotient over moves into the numerator is added to
+ /// , since the expression is undefined where one of those is
+ /// zero and the fraction need not be: 1/(1/x) is (x, 1), which has a value
+ /// at zero.
+ ///
+ ///
+ /// keeps the product of the denominators, for the callers above,
+ /// which divide out afterwards.
+ ///
+ internal static (Entity Numerator, Entity Denominator) OverLeastCommonDenominator(Entity expr, List carried)
+ => Of(expr, carried, leastCommon: true);
+
+ private static (Entity Numerator, Entity Denominator) Of(Entity expr, List? carried, bool leastCommon)
{
switch (expr)
{
case Divf(var dividend, var divisor):
{
- var (an, ad) = Of(dividend);
- var (bn, bd) = Of(divisor);
+ var (an, ad) = Of(dividend, carried, leastCommon);
+ var (bn, bd) = Of(divisor, carried, leastCommon);
// (an/ad) / (bn/bd) = (an * bd) / (ad * bn)
+ Carry(bd, carried);
return (Times(an, bd), Times(ad, bn));
}
case Mulf(var left, var right):
{
- var (an, ad) = Of(left);
- var (bn, bd) = Of(right);
+ var (an, ad) = Of(left, carried, leastCommon);
+ var (bn, bd) = Of(right, carried, leastCommon);
return (Times(an, bn), Times(ad, bd));
}
case Sumf(var augend, var addend):
{
- var (an, ad) = Of(augend);
- var (bn, bd) = Of(addend);
- return (Plus(Times(an, bd), Times(bn, ad)), Times(ad, bd));
+ var (an, ad) = Of(augend, carried, leastCommon);
+ var (bn, bd) = Of(addend, carried, leastCommon);
+ var (common, forA, forB) = Over(ad, bd, leastCommon);
+ return (Plus(Times(an, forA), Times(bn, forB)), common);
}
case Minusf(var minuend, var subtrahend):
{
- var (an, ad) = Of(minuend);
- var (bn, bd) = Of(subtrahend);
- return (Minus(Times(an, bd), Times(bn, ad)), Times(ad, bd));
+ var (an, ad) = Of(minuend, carried, leastCommon);
+ var (bn, bd) = Of(subtrahend, carried, leastCommon);
+ var (common, forA, forB) = Over(ad, bd, leastCommon);
+ return (Minus(Times(an, forA), Times(bn, forB)), common);
}
// A whole power distributes over the quotient, and a negative one turns it over.
@@ -92,9 +122,11 @@ internal static (Entity Numerator, Entity Denominator) Of(Entity expr)
// (a/b)^(1/2) is not sqrt(a)/sqrt(b) on the branch cut.
case Powf(var @base, Integer power):
{
- var (bn, bd) = Of(@base);
+ var (bn, bd) = Of(@base, carried, leastCommon);
if (bd == Integer.One && power.EInteger.Sign >= 0)
return (expr, Integer.One);
+ if (power.EInteger.Sign < 0)
+ Carry(bd, carried);
// The first power is the base itself: `A^(-1)` is `1/A`, not `1^1/A^1`,
// which nothing reading the factors of the denominator took for `A`.
if (power.EInteger.Equals(EInteger.FromInt32(-1)))
@@ -109,6 +141,102 @@ internal static (Entity Numerator, Entity Denominator) Of(Entity expr)
}
}
+ private static void Carry(Entity denominator, List? carried)
+ {
+ if (carried is not null && denominator != Integer.One)
+ carried.Add(denominator);
+ }
+
+ ///
+ /// A common denominator of and , with what each
+ /// is multiplied by to reach it: their product, or with
+ /// their least common multiple as written.
+ ///
+ private static (Entity Common, Entity ForA, Entity ForB) Over(Entity a, Entity b, bool leastCommon)
+ {
+ if (!leastCommon)
+ return (Times(a, b), b, a);
+ if (a == Integer.One)
+ return (b, b, Integer.One);
+ if (b == Integer.One || a == b)
+ return (a, Integer.One, b == Integer.One ? a : Integer.One);
+ var (aWhole, aFactors) = Factors(a);
+ var (bWhole, bFactors) = Factors(b);
+ if (aWhole.IsZero || bWhole.IsZero)
+ return (Times(a, b), b, a);
+ var whole = aWhole.Abs().Divide(aWhole.Abs().Gcd(bWhole.Abs())).Multiply(bWhole.Abs());
+ var common = new List<(Entity Base, EInteger Power)>(aFactors);
+ foreach (var (@base, power) in bFactors)
+ {
+ var at = common.FindIndex(factor => factor.Base == @base);
+ if (at < 0)
+ common.Add((@base, power));
+ else if (power.CompareTo(common[at].Power) > 0)
+ common[at] = (@base, power);
+ }
+ return (Product(whole, common, null),
+ Product(whole.Divide(aWhole), common, aFactors),
+ Product(whole.Divide(bWhole), common, bFactors));
+ }
+
+ ///
+ /// A denominator as a whole number times its other factors, each with the whole power it
+ /// is raised to: a factor written twice is one factor with the powers added, and a power
+ /// of a whole number is part of the whole number.
+ ///
+ private static (EInteger Whole, List<(Entity Base, EInteger Power)> Factors) Factors(Entity denominator)
+ {
+ var whole = EInteger.One;
+ var factors = new List<(Entity Base, EInteger Power)>();
+ foreach (var factor in Mulf.LinearChildren(denominator))
+ {
+ switch (factor)
+ {
+ case Integer integer:
+ whole = whole.Multiply(integer.EInteger);
+ continue;
+ case Powf(Integer number, Integer power) when power.EInteger.Sign > 0
+ && power.EInteger.CompareTo(EInteger.FromInt32(MaxFoldedPower)) <= 0:
+ whole = whole.Multiply(number.EInteger.Pow(power.EInteger));
+ continue;
+ }
+ var (@base, exponent) = factor is Powf(var raised, Integer raisedTo) && raisedTo.EInteger.Sign > 0
+ ? (raised, raisedTo.EInteger)
+ : (factor, EInteger.One);
+ var at = factors.FindIndex(known => known.Base == @base);
+ if (at < 0)
+ factors.Add((@base, exponent));
+ else
+ factors[at] = (@base, factors[at].Power.Add(exponent));
+ }
+ return (whole, factors);
+ }
+
+ ///
+ /// A power of a whole number is multiplied out into the whole number only up to this
+ /// exponent; past it the power stays a factor like any other, which is still correct.
+ ///
+ private const int MaxFoldedPower = 64;
+
+ ///
+ /// times each factor of to the power
+ /// that is short of it, or to its whole power where
+ /// is .
+ ///
+ private static Entity Product(EInteger whole, List<(Entity Base, EInteger Power)> common,
+ List<(Entity Base, EInteger Power)>? own)
+ {
+ Entity product = Integer.Create(whole);
+ foreach (var (@base, power) in common)
+ {
+ var ownPower = own?.Find(factor => factor.Base == @base).Power ?? EInteger.Zero;
+ var missing = power.Subtract(ownPower);
+ if (missing.Sign > 0)
+ product = Times(product, missing.Equals(EInteger.One) ? @base : MathS.Pow(@base, Integer.Create(missing)));
+ }
+ return product;
+ }
+
///
/// rewritten as a single quotient, or unchanged where it had no
/// division in it to combine.
diff --git a/Sources/Tests/UnitTests/Algebra/AsSingleFractionTest.cs b/Sources/Tests/UnitTests/Algebra/AsSingleFractionTest.cs
new file mode 100644
index 000000000..4f102fae8
--- /dev/null
+++ b/Sources/Tests/UnitTests/Algebra/AsSingleFractionTest.cs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using System.Linq;
+using AngouriMath;
+using AngouriMath.Core.Transformations;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Algebra
+{
+ ///
+ /// An expression written as a single fraction: one numerator over one denominator, nothing
+ /// divided inside either, nothing cancelled or multiplied out.
+ /// https://github.com/asc-community/AngouriMath/issues/1239
+ ///
+ [Trait("Area", "Algebra")]
+ public sealed class AsSingleFractionTest
+ {
+ /// The printed form is the point of the operation, so it is what these assert.
+ [Theory]
+ [InlineData("a + b/c", "(a * c + b) / c")]
+ [InlineData("1 + 2/(1+t^2)", "(t ^ 2 + 3) / (t ^ 2 + 1)")]
+ [InlineData("1/(t^2+1) + 1/(t+1)", "(2 + t + t ^ 2) / ((t ^ 2 + 1) * (t + 1))")]
+ [InlineData("sin(x) + 1/cos(x)", "(1 + cos(x) * sin(x)) / cos(x)")]
+ [InlineData("2/3 + x/2", "(4 + 3 * x) / 6")]
+ [InlineData("x^(-2) + 1", "(x ^ 2 + 1) / x ^ 2")]
+ public void WrittenAsOneFraction(string written, string fraction)
+ => Assert.Equal(fraction, written.AsSingleFraction().Stringize());
+
+ ///
+ /// Over the least common multiple of the denominators as they are written, as it is done
+ /// by hand: a factor two terms share is not multiplied in twice. Nothing is factorised to
+ /// find it, so x^2 - 1 and x + 1 share nothing.
+ ///
+ [Theory]
+ [InlineData("1/x + 1/x^2", "(x + 1) / x ^ 2")]
+ [InlineData("x/2 + x/4", "x * 3 / 4")]
+ [InlineData("1/6 + 1/4", "5 / 12")]
+ [InlineData("1/(x + 1) + 1/(x + 1)^2", "(x + 2) / (x + 1) ^ 2")]
+ [InlineData("a/(b*c) + d/(b*e)", "(a * e + c * d) / (b * c * e)")]
+ [InlineData("1/(x^2 - 1) + 1/(x + 1)", "(x + x ^ 2) / ((x ^ 2 - 1) * (x + 1))")]
+ public void OverTheLeastCommonDenominator(string written, string fraction)
+ => Assert.Equal(fraction, written.AsSingleFraction().Stringize());
+
+ ///
+ /// Dividing by a fraction moves its denominator into the numerator, where it no longer
+ /// stops the answer having a value, so the answer says it is nonzero -- unless the new
+ /// denominator still says so itself.
+ ///
+ [Theory]
+ [InlineData("1/(1/x)", "x provided not x = 0")]
+ [InlineData("(a/b)/(c/d)", "a * d / (b * c) provided not d = 0")]
+ [InlineData("(x/y)^(-2)", "y ^ 2 / x ^ 2 provided not y = 0")]
+ [InlineData("(1/x)/(1/x)", "x / x")]
+ public void TurningAFractionOverSaysItsDenominatorIsNonzero(string written, string fraction)
+ => Assert.Equal(fraction, written.AsSingleFraction().Stringize());
+
+ ///
+ /// Undefined where the expression is: at zero for the variable named, and one for every
+ /// other, each of these has no value, and neither has its single fraction.
+ ///
+ [Theory]
+ [InlineData("1/(1/x)", "x")]
+ [InlineData("(a/b)/(c/d)", "d")]
+ [InlineData("(1/x)^(-1)", "x")]
+ [InlineData("(x/y)^(-2)", "y")]
+ [InlineData("x/(y/x)", "x")]
+ [InlineData("(1/x)/(1/x)", "x")]
+ [InlineData("1/x + 1/x^2", "x")]
+ public void TheDomainIsKept(string written, string atZero)
+ {
+ var original = written.ToEntity();
+ var fraction = original.AsSingleFraction();
+ foreach (var variable in original.Vars)
+ {
+ Entity value = variable.Name == atZero ? 0 : 1;
+ original = original.Substitute(variable, value);
+ fraction = fraction.Substitute(variable, value);
+ }
+ Assert.Equal(MathS.NaN, original.Evaled);
+ Assert.Equal(MathS.NaN, fraction.Evaled);
+ }
+
+ /// Nothing cancels, and a function's argument is not gathered.
+ [Theory]
+ [InlineData("x/x", "x / x")]
+ [InlineData("sin(x/2) + 1/x", "(1 + sin(x / 2) * x) / x")]
+ public void NothingIsCancelledAndArgumentsAreLeft(string written, string fraction)
+ => Assert.Equal(fraction, written.AsSingleFraction().Stringize());
+
+ /// With no division in it, or a number, the expression comes back as it was.
+ [Theory]
+ [InlineData("x")]
+ [InlineData("x^2 + 1")]
+ [InlineData("1/2")]
+ public void WithNothingToGatherItComesBack(string written)
+ => Assert.Equal(written.ToEntity(), written.ToEntity().AsSingleFraction());
+
+ /// The same value everywhere both are defined.
+ [Theory]
+ [InlineData("a + b/c")]
+ [InlineData("1/(t^2+1) + 1/(t+1)")]
+ [InlineData("sin(x) + 1/cos(x)")]
+ [InlineData("2/3 + x/2")]
+ [InlineData("(a/b)/(c/d)")]
+ [InlineData("x/(y/x)")]
+ [InlineData("1/x + 1/x^2")]
+ [InlineData("a/(b*c) + d/(b*e)")]
+ [InlineData("1/(x + 1) + 1/(x + 1)^2")]
+ public void TheValueIsKept(string written)
+ {
+ var original = written.ToEntity();
+ var fraction = original.AsSingleFraction();
+ foreach (var (name, value) in new[] { ("a", 2), ("b", -3), ("c", 5), ("d", 11), ("e", 17), ("t", 3), ("x", 7), ("y", 13) })
+ {
+ original = original.Substitute(name, value);
+ fraction = fraction.Substitute(name, value);
+ }
+ Assert.Equal(original.EvalNumerical().RealPart.EDecimal.RoundToPrecision(PeterO.Numbers.EContext.ForPrecision(30)),
+ fraction.EvalNumerical().RealPart.EDecimal.RoundToPrecision(PeterO.Numbers.EContext.ForPrecision(30)));
+ }
+
+ [Fact]
+ public void TheTransformationIsTheMethod()
+ {
+ Entity written = "1 + 2/(1+t^2)";
+ Assert.Equal(written.AsSingleFraction(), Transformation.AsSingleFraction.ApplyOrKeep(written));
+ Assert.Equal("single-fraction", Transformation.AsSingleFraction.Name);
+ }
+ }
+}
diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt
index 8e82a22ab..b48d96118 100644
--- a/Sources/Tests/UnitTests/Common/PublicApi.txt
+++ b/Sources/Tests/UnitTests/Common/PublicApi.txt
@@ -343,6 +343,7 @@ AngouriMath.Core.Transformations.Soundness.value__ : System.Int32
AngouriMath.Core.Transformations.Transformation.Apply(AngouriMath.Entity) : AngouriMath.Core.Transformations.TransformationResult
AngouriMath.Core.Transformations.Transformation.ApplyCore(AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.Core.Transformations.Transformation.ApplyOrKeep(AngouriMath.Entity) : AngouriMath.Entity
+AngouriMath.Core.Transformations.Transformation.AsSingleFraction { } : AngouriMath.Core.Transformations.Transformation
AngouriMath.Core.Transformations.Transformation.Canonicalization { } : AngouriMath.Core.Transformations.Transformation
AngouriMath.Core.Transformations.Transformation.CanonicalizationOverGraph(AngouriMath.Core.Budgets.WorkBudget) : AngouriMath.Core.Transformations.Transformation
AngouriMath.Core.Transformations.Transformation.CanonicalizationOverGraph(AngouriMath.Core.Budgets.WorkBudget, AngouriMath.Core.Transformations.RewriteRuleGrowth) : AngouriMath.Core.Transformations.Transformation
@@ -2786,6 +2787,7 @@ AngouriMath.Entity.Arccotan() : AngouriMath.Entity
AngouriMath.Entity.Arcsec() : AngouriMath.Entity
AngouriMath.Entity.Arcsin() : AngouriMath.Entity
AngouriMath.Entity.Arctan() : AngouriMath.Entity
+AngouriMath.Entity.AsSingleFraction() : AngouriMath.Entity
AngouriMath.Entity.Canonicalize() : AngouriMath.Entity
AngouriMath.Entity.CanonicalizeAsRationalFunction() : AngouriMath.Entity
AngouriMath.Entity.Ceil() : AngouriMath.Entity
@@ -2934,6 +2936,7 @@ AngouriMath.Entity.op_Multiply(AngouriMath.Entity, AngouriMath.Entity) : Angouri
AngouriMath.Entity.op_Subtraction(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.Entity.op_UnaryNegation(AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.Entity.op_UnaryPlus(AngouriMath.Entity) : AngouriMath.Entity
+AngouriMath.Extensions.AngouriMathExtensions.AsSingleFraction(System.String) : AngouriMath.Entity
AngouriMath.Extensions.AngouriMathExtensions.Compile(System.String, AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol, System.Type, System.Collections.Generic.IEnumerable>) : TDelegate
AngouriMath.Extensions.AngouriMathExtensions.Compile(System.String, AngouriMath.Entity+Variable) : Func
AngouriMath.Extensions.AngouriMathExtensions.Compile(System.String, AngouriMath.Entity+Variable, AngouriMath.Entity+Variable) : Func