diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 39dbaceb9..8a458fe47 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -90,6 +90,7 @@ read first.
| | `"sum(N! / (k! * (N - k)!) * x^k, k, 0, N)".ToEntity().Simplify()`, and every binomial sum with a power or a cosine or sine of the index as its weight | `sum(N! / (k! * (N - k)!) * x ^ k, k, 0, N)` — left as written | `piecewise((1 + x) ^ N provided N >= 0, 0)`; with `cos(k * pi / 3)`, `piecewise(2 ^ N * cos(pi / 6) ^ N * cos(N * pi / 6) provided N >= 0, 0)` |
| | `"ln(4/3) + ln(16/9) / 2".ToEntity().Simplify()`, and every sum of logarithms of rational literals one of which is a perfect power of another | `ln(4/3) + ln(16/9) / 2` — left as written | `2 * ln(4/3)`; the integral it came from, `integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)`, answers `ln(16/9)` |
| | `"sum(x^k, k, 0, +oo)".ToEntity().Simplify()`, and every summation of a power with the index in the exponent times something free of the index, to a bound or to `+oo` | left as written | `1 / (1 - x) provided abs(x) < 1`; `sum(2^(-k), k, 0, +oo)` is `2`, `sum(x^k, k, 0, n)` is a piecewise with the ratio 1 and the empty range as cases of their own, and `integral(2^(-floor(x)), x, 0, +oo)` is `2` |
+| | `"sum(2^k, k, 0, +oo)".ToEntity().Simplify()`, and every summation to `+oo` whose terms do not tend to zero and whose limit has a sign | left as written | `+oo`, or `-oo` where the limit is negative; `sum(k, k, 1, +oo)` is `+oo` and `integral(floor(x)^floor(x), x, 1, +oo)` is `+oo`. A vanishing limit, a limit that does not exist, and a pole in the index are each still left as written |
| **Loud** | `"divides".ToEntity()`, and every expression with a name spelt `divides` in it | the variable `divides`; `2 divides` was `2 * divides` and `3 divides 12` was `3 * divides ^ 12` | a parse error — `divides` is the keyword of the new statement `a divides b`, which reads `3 divides 12` as the statement that 12 is a multiple of 3 |
| **Silent** | `"card(x)".ToEntity()` and `"#x"`, and every expression with a name spelt `card` followed by a parenthesis, or a `#` | `card * x` — a variable times the parenthesis; `#` was a parse error; `card({ 1, 2, 3 })` evaluated to the set `{ card, card * 2, card * 3 }` | `#x`, the number of elements of the set `x` (`card( )` accepted, `#` canonical); `#{ 1, 2, 3 }` is `3` |
| **Silent** | `"integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)".ToEntity().Simplify()`, and every definite integral of a piecewise whose conditions mention the variable | `1` — the first case's antiderivative at both ends | `1/2` — split at the case boundaries; `piecewise(x provided x < 1, x^2)` from 0 to 2 was `piecewise(2 provided x < 1, 8/3)`, with the integration variable still in it, and is `17/6` |
@@ -1132,6 +1133,39 @@ columns measured on a build, `60545afa` against this change.
| `"ln(16/9)"`, `ln(64)`, `ln(8) - 3 * ln(2)`, `log(3, 81) / 4` | `ln(16/9)`, `ln(64)`, `0`, `1` | the same |
| `RewriteRules.Power.Rules.Count`, and `RewriteRules.All` by growth | `31`; 124 / 49 / 31 / 123 | `32`; 124 / 49 / 32 / 123 |
+### A series whose terms do not vanish is answered `+oo` rather than left as written
+
+`sum(2^k, k, 0, +oo)` and `sum(k, k, 1, +oo)` were left as written. They have no finite value, and
+saying nothing was the only thing missing: the **nth-term test** settles them. Terms that do not
+tend to zero mean the series diverges, and the *sign* of the limit says which infinity — where the
+terms tend to a positive `L` they are eventually all above `L / 2`, so the partial sums pass every
+bound. A negative limit gives `-oo` the same way, and the finitely many terms before that point are
+finite and cannot change it.
+
+**A limit of zero is declined, and that is the point of the test rather than a gap in it.** It is
+exactly the case the nth-term test says nothing about: `sum(1/k, k, 1, +oo)` diverges and
+`sum(1/k^2, k, 1, +oo)` converges, and the terms of both tend to 0. A limit that does not exist is
+declined too — `sum((-1)^k, k, 0, +oo)` has no value rather than an infinite one, and telling "the
+limit does not exist" apart from "the limit was not computed" is not something to infer from a
+failed computation. **And a summand that can fail to exist at some index is declined**, because one
+undefined term makes the sum undefined rather than infinite: `sum(k / (k - 5), k, 0, +oo)` has terms
+tending to 1, and the term at `k = 5` does not exist. Rather than hunt for poles, the index is
+allowed only where none can arise, so a division by anything containing it is refused outright.
+
+This runs last, after every closed form, so a series that converges is summed rather than tested.
+Asked for on the review of [#1218](https://github.com/asc-community/AngouriMath/pull/1218), where
+`integral(floor(x)^floor(x), x, 1, +oo)` splits into `sum(n^n, n, 1, +oo)` and was left as written
+for want of it. Part of [#1212](https://github.com/asc-community/AngouriMath/issues/1212). Both
+columns measured on a build, `102911be` against this change.
+
+| | Was | Is |
+|---|---|---|
+| `"sum(2^k, k, 0, +oo)".ToEntity().Simplify()`, `sum(k, k, 1, +oo)`, `sum(k^2 + 1, k, 0, +oo)`, `sum(n^n, n, 1, +oo)` | left as written | `+oo` |
+| `"sum(-k, k, 1, +oo)".ToEntity().Simplify()`, `sum(-2^k, k, 0, +oo)` | left as written | `-oo` |
+| `"integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify()` | left as written | `+oo` |
+| `"sum(1/k, k, 1, +oo)"`, `sum(1/k^2, k, 1, +oo)`, `sum((-1)^k, k, 0, +oo)`, `sum(k / (k - 5), k, 0, +oo)` | left as written | the same — a vanishing limit, a limit that does not exist, and a pole in the index are each declined |
+| `"sum(2^(-k), k, 0, +oo)"`, `sum(x^k / k!, k, 0, +oo)`, `sum(2^k, k, 0, 5)` | `2`, `e^x`, `63` | the same — what converges is summed, and a finite range is a finite sum |
+
### A geometric series is summed in closed form
`sum(x^k, k, 0, +oo)` was left as written, and so was every other sum of a power with the index in
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 92095833f..4eff38e84 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -225,6 +225,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Calculus/ExtremumTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+[AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs]
+file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
+[Tests/UnitTests/Calculus/DivergentSeriesTest.cs]
+file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
[Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md
index 8e15ffcb3..2233a05d4 100644
--- a/Sources/AngouriMath/Docs/Usage/Syntax.md
+++ b/Sources/AngouriMath/Docs/Usage/Syntax.md
@@ -259,6 +259,14 @@ concrete that condition is decidable and the answer is a number. A bound that is
a whole one still stays as written, since the index runs over the integers and
`sum(k, k, 1, 5/2)` is `1 + 2` rather than the polynomial at `5/2`.
+A sum **to `+oo`** is answered where it converges — the geometric, exponential and binomial series
+have closed forms — and, failing that, by the **nth-term test**: terms that do not tend to zero
+mean no finite value, and a limit with a sign says which infinity, so `sum(2^k, k, 0, +oo)` and
+`sum(k, k, 1, +oo)` are `+oo`. Terms that *do* tend to zero are left as written, because that is
+the case the test says nothing about — `sum(1/k, k, 1, +oo)` diverges and `sum(1/k^2, k, 1, +oo)`
+converges and neither is decided by the size of a term. A summand that can fail to exist at some
+index is left alone too, since one undefined term makes the sum undefined rather than infinite.
+
A `product` gets the same treatment over the narrower class its shape allows: a **monomial** in
the index, since a product has no linearity to take a sum of terms apart with. So
`product(k, k, 1, n)` is `factorial(n)`, `product(k ^ 2, k, 1, n)` is `factorial(n) ^ 2`, and
diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs
new file mode 100644
index 000000000..7c9e3d1e8
--- /dev/null
+++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/DivergentSeries.cs
@@ -0,0 +1,197 @@
+//
+// 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 static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Functions
+{
+ ///
+ /// A summation to +oo whose terms do not tend to zero, which therefore has no finite
+ /// value: sum(2^k, k, 0, +oo) and sum(n^n, n, 1, +oo) are +oo.
+ ///
+ ///
+ ///
+ /// The nth-term test: if the terms do not tend to zero the series diverges. That alone
+ /// says a series has no sum; it does not say what to answer instead. What settles the answer is
+ /// the sign of the limit — where the terms tend to a positive L, they are
+ /// eventually all above L / 2, so the partial sums pass every bound and the value is
+ /// +oo; a negative limit gives -oo the same way. The finitely many terms before
+ /// that point are finite and cannot change it.
+ ///
+ ///
+ /// A limit of zero is declined, and that is the whole difficulty of the test. It is
+ /// exactly the case the nth-term test says nothing about: sum(1/k) diverges and
+ /// sum(1/k^2) converges, and their terms both tend to 0. Answering either from this
+ /// reader would be a guess. So would a limit that does not exist — sum((-1)^k) has no
+ /// value rather than an infinite one — and that is left as written too, since telling "the
+ /// limit does not exist" apart from "the limit was not computed" is not something to infer
+ /// from a failed computation.
+ ///
+ ///
+ /// The summand must have no pole in the index, and this is checked before the limit is
+ /// asked for. A single undefined term makes the whole sum undefined, not infinite:
+ /// sum(k / (k - 5), k, 0, +oo) has terms tending to 1, and answering +oo would
+ /// be wrong because the term at k = 5 does not exist. Rather than hunt for poles, the
+ /// index is allowed to occur only where none can arise — under +, -, *,
+ /// and powers of the three shapes lists. Division by
+ /// anything containing the index, a factorial of it, a logarithm of it and the rest are
+ /// declined outright. That check is also what keeps this cheap: it runs first, so a summand
+ /// this cannot speak about never reaches the limit engine.
+ ///
+ ///
+ /// Last in the chain, after every closed form, so that a series which does converge is
+ /// summed rather than tested. Asked for on the review of
+ /// #1218, where
+ /// integral(floor(x)^floor(x), x, 1, +oo) splits into sum(n^n, n, 1, +oo) and was
+ /// left as written for want of this; part of
+ /// #1212.
+ ///
+ ///
+ internal static class DivergentSeries
+ {
+ ///
+ /// +oo or -oo where the terms tend to a non-zero limit of that sign, and
+ /// wherever the test does not settle the question.
+ ///
+ internal static Entity? ClosedForm(Entity expression, Entity var, Entity from, Entity to)
+ {
+ if (var is not Variable index)
+ return null;
+ // To +oo only: a finite range is a finite sum, and the expansion has had it already.
+ if (to.Evaled is not Real { IsFinite: false, IsNaN: false, IsNegative: false })
+ return null;
+ // A concrete whole lower bound, so that the range is a genuine tail of the integers.
+ // A symbolic one could be -oo, where there is no first term to be finite.
+ if (from.Evaled is not Integer start || !start.EInteger.CanFitInInt32())
+ return null;
+ // A condition that holds for every index in range says nothing about the sum and is
+ // dropped; one that does not is a term that may fail to exist, and the pole check
+ // below then declines it as the Providedf it still is.
+ var summand = WithoutConditionsThatHold(expression.InnerSimplified, index, start.EInteger.ToInt32Checked());
+ if (!HasNoPoleInTheIndex(summand, index, start))
+ return null;
+
+ var limit = summand.Limit(index, Real.PositiveInfinity).Evaled;
+ return limit switch
+ {
+ // Tends to +oo or -oo: the terms pass every bound, so the sums do.
+ Real { IsFinite: false, IsNaN: false } infinite => infinite,
+ // Tends to a non-zero L: eventually every term has L's sign and half its size.
+ Real { IsFinite: true } value when !IsZero(value) =>
+ value.IsNegative ? Real.NegativeInfinity : Real.PositiveInfinity,
+ // A zero limit says nothing, and anything else was not settled.
+ _ => null
+ };
+ }
+
+ ///
+ /// Whether the index can only occur where no term can fail to exist: under +,
+ /// - and *, and in a power that is one of three safe shapes — a whole
+ /// non-negative exponent (k ^ 3), a base free of the index and not zero
+ /// (2 ^ k), or the index itself raised to something over a range that starts at 1
+ /// or above (n ^ n). Anything else containing the index is declined.
+ ///
+ private static bool HasNoPoleInTheIndex(Entity expression, Variable index, Integer start)
+ {
+ // Free of the index is free of poles *in the index*, whatever else it is.
+ if (!expression.ContainsNode(index))
+ return true;
+ return expression switch
+ {
+ Variable => true,
+ Sumf(var left, var right) => Both(left, right),
+ Minusf(var left, var right) => Both(left, right),
+ Mulf(var left, var right) => Both(left, right),
+ // k ^ 3: a whole non-negative exponent is a repeated product.
+ Powf(var @base, Integer { IsNegative: false }) => Go(@base),
+ // 2 ^ k: a base that does not vary and is not zero.
+ Powf(var @base, var exponent) when !@base.ContainsNode(index)
+ && @base.Evaled is Complex value && !IsZero(value) => Go(exponent),
+ // n ^ n: the base is the index, which over a range starting at 1 or above is
+ // positive, so the power exists at every term of it.
+ Powf(var @base, var exponent) when @base == index && start.EInteger.Sign > 0
+ => Go(exponent),
+ _ => false
+ };
+
+ bool Go(Entity part) => HasNoPoleInTheIndex(part, index, start);
+ bool Both(Entity left, Entity right) => Go(left) && Go(right);
+ }
+
+ ///
+ /// The summand with any attached condition removed that is true for every index in the
+ /// range, and left alone otherwise.
+ ///
+ ///
+ /// integral(n ^ n, t, 0, 1) is n ^ n provided not n = 0 or n > 0, because
+ /// 0 ^ 0 is the one power that has to say what it assumes — so the summand the step
+ /// split hands over carries a condition that is simply true from 1 upwards.
+ /// has a sibling of this specialised to the clauses a
+ /// division by a factorial carries; the two vocabularies do not overlap, and a third
+ /// reader needing one would be the point at which to merge them rather than now.
+ ///
+ private static Entity WithoutConditionsThatHold(Entity summand, Variable index, int start)
+ {
+ while (summand is Providedf(var body, var condition) && HoldsOverTheRange(condition, index, start))
+ summand = body;
+ return summand;
+ }
+
+ ///
+ /// Whether a condition is true for every whole index >= start. Only comparisons
+ /// of the index with zero are read, since those are what a power attaches; anything else
+ /// is answered , which costs a declined summation and never a wrong
+ /// sum.
+ ///
+ private static bool HoldsOverTheRange(Entity condition, Variable index, int start)
+ => condition switch
+ {
+ Greaterf(var argument, var zero)
+ when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a > 0 => true,
+ GreaterOrEqualf(var argument, var zero)
+ when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a >= 0 => true,
+ // Never zero because it is always above it; the other direction is not read.
+ Notf(Equalsf(var argument, var zero))
+ when IsExactlyZero(zero) && TryReadShift(argument, index, out var a) && start + a > 0 => true,
+ Andf(var left, var right)
+ => HoldsOverTheRange(left, index, start) && HoldsOverTheRange(right, index, start),
+ Orf(var left, var right)
+ => HoldsOverTheRange(left, index, start) || HoldsOverTheRange(right, index, start),
+ _ => false,
+ };
+
+ private static bool IsExactlyZero(Entity what) => what.Evaled is Integer { IsZero: true };
+
+ ///
+ /// as index + shift for a whole shift: the index
+ /// itself, or the index plus or minus a whole number, in either order. Read off the tree,
+ /// because k - k simplifies to a conditional zero rather than to nothing
+ /// (#1174).
+ ///
+ private static bool TryReadShift(Entity argument, Variable index, out int shift)
+ {
+ shift = 0;
+ if (argument == index)
+ return true;
+ switch (argument)
+ {
+ case Sumf(var left, Integer right) when left == index && right.EInteger.CanFitInInt32():
+ shift = right.EInteger.ToInt32Checked();
+ return true;
+ case Sumf(Integer left, var right) when right == index && left.EInteger.CanFitInInt32():
+ shift = left.EInteger.ToInt32Checked();
+ return true;
+ case Minusf(var left, Integer right) when left == index && right.EInteger.CanFitInInt32():
+ shift = -right.EInteger.ToInt32Checked();
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
index c42961068..926b0276e 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
@@ -239,6 +239,9 @@ protected override Entity InnerSimplify(bool isExact) =>
?? Functions.ExponentialSeries.ClosedForm(Expression, Var, From, To)
?? Functions.BinomialSum.ClosedForm(Expression, Var, From, To)
?? Functions.GeometricSeries.ClosedForm(Expression, Var, From, To)
+ // Last: a series that converges is summed above, and only what none of them
+ // answered is asked whether it diverges.
+ ?? Functions.DivergentSeries.ClosedForm(Expression, Var, From, To)
?? this;
///
diff --git a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs
index 881660680..3cc233518 100644
--- a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs
@@ -102,16 +102,16 @@ public void ASymbolicBoundIsLeftAsWritten()
Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify());
}
- // The split leaves a geometric series, which is summed in closed form. A sum the closed
- // forms do not answer yet leaves the integral as written rather than an unevaluated sum:
- // the second one is sum(n^n, n, 1, +oo), whose terms do not tend to zero, so it is +oo --
- // the answer a divergence test would give, and there is none yet. Not a verdict that it
- // cannot be answered; the day it is, this line moves to the value.
+ // The split leaves a summation, and whatever answers that answers the integral. The first
+ // is a geometric series; the second is sum(n^n, n, 1, +oo), whose terms do not tend to
+ // zero. This line used to assert the integral stayed as written, with a comment saying it
+ // would move to the value the day a divergence test existed. See DivergentSeriesTest.
[Fact]
- public void TheSumTheSplitLeavesIsAnsweredOrTheIntegralStays()
+ public void TheSumTheSplitLeavesIsWhatAnswersTheIntegral()
{
Assert.Equal("2".ToEntity(), "integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify());
- Assert.IsType("integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify());
+ Assert.Equal(Number.Real.PositiveInfinity,
+ "integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify());
}
// Not split and not integrated either: a floor of something other than the variable
diff --git a/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs b/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs
new file mode 100644
index 000000000..322eacf6a
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/DivergentSeriesTest.cs
@@ -0,0 +1,106 @@
+//
+// 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 AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// A summation to +oo whose terms do not tend to zero has no finite value, and where
+ /// the limit has a sign the answer is +oo or -oo rather than nothing. The
+ /// nth-term test, asked for on the review of
+ /// https://github.com/asc-community/AngouriMath/pull/1218.
+ ///
+ public sealed class DivergentSeriesTest
+ {
+ [Theory]
+ [InlineData("sum(2^k, k, 0, +oo)")]
+ [InlineData("sum(n^n, n, 1, +oo)")]
+ [InlineData("sum(1, k, 0, +oo)")]
+ [InlineData("sum(k, k, 1, +oo)")]
+ [InlineData("sum(k^2 + 1, k, 0, +oo)")]
+ [InlineData("sum(3 * 2^k, k, 0, +oo)")]
+ [InlineData("sum(k^3 - k, k, 2, +oo)")]
+ public void TermsThatDoNotVanishSumToPositiveInfinity(string sum)
+ => Assert.Equal(Number.Real.PositiveInfinity, sum.ToEntity().Simplify());
+
+ [Theory]
+ [InlineData("sum(-1, k, 0, +oo)")]
+ [InlineData("sum(-k, k, 1, +oo)")]
+ [InlineData("sum(-2^k, k, 0, +oo)")]
+ public void ANegativeLimitSumsToNegativeInfinity(string sum)
+ => Assert.Equal(Number.Real.NegativeInfinity, sum.ToEntity().Simplify());
+
+ // The case the nth-term test says nothing about, and the reason it must not guess:
+ // both of these have terms tending to 0, one diverges and one converges.
+ [Theory]
+ [InlineData("sum(1/k, k, 1, +oo)")]
+ [InlineData("sum(1/k^2, k, 1, +oo)")]
+ [InlineData("sum(1/k^3, k, 1, +oo)")]
+ public void AVanishingLimitIsLeftAsWritten(string sum)
+ => Assert.IsType(sum.ToEntity().Simplify());
+
+ // A limit that does not exist means no value rather than an infinite one, and telling
+ // "does not exist" from "was not computed" is not something to infer from a failure.
+ [Theory]
+ [InlineData("sum((-1)^k, k, 0, +oo)")]
+ [InlineData("sum(sin(k), k, 1, +oo)")]
+ public void ALimitThatDoesNotExistIsLeftAsWritten(string sum)
+ => Assert.IsType(sum.ToEntity().Simplify());
+
+ // A single undefined term makes the sum undefined, not infinite. The terms of the first
+ // tend to 1, so a reader that only looked at the limit would answer +oo and be wrong
+ // about the term at k = 5.
+ [Theory]
+ [InlineData("sum(k / (k - 5), k, 0, +oo)")]
+ [InlineData("sum(k / (k - 5), k, 0, n)")]
+ [InlineData("sum(1 / (k - 5) + k, k, 0, +oo)")]
+ public void APoleInTheIndexIsLeftAsWritten(string sum)
+ => Assert.IsType(sum.ToEntity().Simplify());
+
+ // What converges is summed by a closed form, not tested for divergence.
+ [Theory]
+ [InlineData("sum(2^(-k), k, 0, +oo)", "2")]
+ [InlineData("sum(1 / 2^k, k, 1, +oo)", "1")]
+ [InlineData("sum(x^k / k!, k, 0, +oo)", "e^x")]
+ public void WhatConvergesIsStillSummed(string sum, string expected)
+ => Assert.Equal(expected.ToEntity().Simplify(), sum.ToEntity().Simplify());
+
+ // A finite range is a finite sum whatever the terms do.
+ [Theory]
+ [InlineData("sum(2^k, k, 0, 5)", "63")]
+ [InlineData("sum(k, k, 1, 100)", "5050")]
+ public void AFiniteRangeIsUnaffected(string sum, string expected)
+ => Assert.Equal(expected.ToEntity(), sum.ToEntity().Simplify());
+
+ // A symbolic lower bound could be -oo, where there is no first term to be finite.
+ [Fact]
+ public void ASymbolicLowerBoundIsLeftAsWritten()
+ => Assert.IsType("sum(2^k, k, a, +oo)".ToEntity().Simplify());
+
+ // The condition a power attaches is dropped where it holds over the range: the split
+ // hands over `n ^ n provided not n = 0 or n > 0`, which from 1 upwards is just n ^ n.
+ [Fact]
+ public void AConditionThatHoldsOverTheRangeIsDropped()
+ => Assert.Equal(Number.Real.PositiveInfinity,
+ "sum(n^n provided not n = 0 or n > 0, n, 1, +oo)".ToEntity().Simplify());
+
+ // A condition that does *not* hold over the range is a term that may fail to exist.
+ [Fact]
+ public void AConditionThatDoesNotHoldIsLeftAsWritten()
+ => Assert.IsType("sum(k^2 provided k > 100, k, 1, +oo)".ToEntity().Simplify());
+
+ // The integral that asked for this: #1218's review. The split leaves sum(n^n, n, 1, +oo).
+ [Fact]
+ public void TheStepIntegralThatAskedForThisIsInfinite()
+ => Assert.Equal(Number.Real.PositiveInfinity,
+ "integral(floor(x)^floor(x), x, 1, +oo)".ToEntity().Simplify());
+ }
+}
diff --git a/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs b/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs
index 3e5c007f8..0936a1955 100644
--- a/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/GeometricSeriesTest.cs
@@ -88,8 +88,9 @@ public void ASymbolicLowerBoundIsAnsweredToo(string a, string n, string expected
// Not this family: a ratio at or beyond 1 to +oo, a polynomial beside the power, an
// exponent that is not linear in the index, a base with the index in it.
+ // `sum(2^k, k, 0, +oo)` was here: this reader still declines it, a ratio at or beyond 1
+ // having no sum, but the nth-term test now answers it `+oo`. See DivergentSeriesTest.
[Theory]
- [InlineData("sum(2^k, k, 0, +oo)")]
[InlineData("sum((-1)^k, k, 0, +oo)")]
[InlineData("sum(k * (1/2)^k, k, 0, +oo)")]
[InlineData("sum(2^(k^2), k, 0, 200)")]
diff --git a/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs
index a43bd9e2e..4ac5f20e9 100644
--- a/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs
@@ -356,8 +356,8 @@ public void AnInclusiveRangeAndItsIdentity(string written, string expected) =>
/// to 5/2 would be 35/8, which answers a different question.
///
[Theory]
+ // `sum(k, k, 1, +oo)` used to be here and is now `+oo`, the terms not tending to zero.
[InlineData("sum(k, k, 1, 5/2)")]
- [InlineData("sum(k, k, 1, +oo)")]
[InlineData("sum(k ^ k, k, 1, n)")]
public void ARangeThatIsNotAnIntegerOneStaysAsWritten(string written) =>
Assert.Equal(written.ToEntity(), written.ToEntity().Simplify());
diff --git a/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs b/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs
index a09677e62..cc03c973e 100644
--- a/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs
+++ b/Sources/Tests/UnitTests/Core/ClosedFormSummationTest.cs
@@ -131,9 +131,10 @@ public void ARangeTooLongToWriteOutIsStillAnswered(string expression, string exp
/// 5/2 is 35/8. Neither is a rounding of the other.
///
[Theory]
+ // `sum(k, k, 1, +oo)` used to be here and is now `+oo`: the nth-term test answers an
+ // infinite range this closed form still declines. See DivergentSeriesTest.
[InlineData("sum(k, k, 1, 5/2)")]
[InlineData("sum(k, k, 1, 1.5)")]
- [InlineData("sum(k, k, 1, +oo)")]
[InlineData("sum(k, k, -oo, n)")]
public void ABoundThatIsNotAWholeNumberIsLeftAlone(string expression)
=> Assert.IsType(expression.ToEntity().Simplify());