Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Sources/AngouriMath/Convenience/AngouriMathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,22 @@ public static Interval ToEntity(this (Entity left, bool leftClosed, Entity right
/// </example>
public static Entity Factorize(this string expr) => expr.ToEntity().Factorize();

/// <summary>
/// Parses the given expression and writes it as a single fraction: one numerator over one
/// denominator, nothing cancelled. See <see cref="Entity.AsSingleFraction"/>.
/// </summary>
/// <returns>The expression as one fraction, or unchanged where it has no division in it</returns>
/// <example>
/// <code>
/// Console.WriteLine("a + b/c".AsSingleFraction());
/// </code>
/// Prints
/// <code>
/// (a * c + b) / c
/// </code>
/// </example>
public static Entity AsSingleFraction(this string expr) => expr.ToEntity().AsSingleFraction();

/// <summary>
/// Subsitutes a variable by replacing all its occurances with the given value
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,96 @@ private sealed class NumericContentTransformation : Transformation
=> Functions.NumericContent.Extracted(input);
}

/// <summary>
/// Writes an expression as a single fraction, as <see cref="Entity.AsSingleFraction"/>
/// 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.
/// </summary>
/// <remarks>
/// Held in a nested class for the reason <see cref="NumericContentExtraction"/> is.
/// </remarks>
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<Entity>();
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<Entity>();
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;
}
}

/// <summary>
/// The rule-based half of <see cref="FactorizationAtLevel"/>, without the polynomial
/// layer.
Expand Down
1 change: 1 addition & 0 deletions Sources/AngouriMath/Docs/Contributing/Transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,39 @@ private static Entity CollectLikeTerms(Entity expanded)
public Entity Factorize(int level = 2)
=> Transformation.FactorizationAtLevel(level).ApplyOrKeep(this);

/// <summary>
/// This expression written as a single fraction: one numerator over one denominator, with
/// nothing divided inside either, as <c>a + b/c</c> is written <c>(a c + b)/c</c>. Any
/// expression, functions included, and nothing cancelled or multiplied out, so the factors
/// of the denominator stay as they were: <c>1/(t^2 + 1) + 1/(t + 1)</c> is
/// <c>(2 + t + t^2) / ((t^2 + 1)(t + 1))</c>, and <c>x/x</c> stays <c>x/x</c>. The common
/// denominator is the least common multiple of the denominators as they are written, so
/// <c>1/x + 1/x^2</c> is <c>(x + 1)/x^2</c>; nothing is factorised to find it. A rational number
/// counts as the fraction it is written as, so <c>2/3 + x/2</c> is <c>(4 + 3x)/6</c>. 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: <c>1/(1/x)</c> is <c>x provided not x = 0</c>.
/// </summary>
/// <remarks>
/// <see cref="Simplify(int)"/> never does this, since one fraction is not always the
/// simpler form: <c>1/x + 1/y</c> reads more easily than <c>(x + y)/(x y)</c>. For a
/// rational function in lowest terms, the denominator multiplied out and monic, use
/// <see cref="Transformation.RationalCanonicalization"/>, which is a canonical form and
/// declines anything with a function in it.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1239">#1239</a>
/// </remarks>
/// <example>
/// <code>
/// Console.WriteLine("sin(x) + 1/cos(x)".ToEntity().AsSingleFraction());
/// </code>
/// Prints
/// <code>
/// (1 + cos(x) * sin(x)) / cos(x)
/// </code>
/// </example>
public Entity AsSingleFraction()
=> Transformation.AsSingleFraction.ApplyOrKeep(this);

/// <summary>
/// Simplifies an equation ( e.g. (x - y) * (x + y) -> x^2 - y^2, but 3 * x + y * x = (3 + y) * x )
/// </summary>
Expand Down
Loading
Loading