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
32 changes: 32 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ read first.
| **Silent** | `RewriteRules.ExpandFactorialDivisions.Rules[0].Growth` | `Collects` — guessed from string length | `Unknown`; whether it collects depends on the offsets |
| **Silent** | `RewriteStep.Soundness` on a rewrite whose rule declares a tier — `RewriteRules.SetOperator` on `A /\ A`, and every rewrite of the nineteen sets described from their data form | `SoundUnderAssumptions` — its rule set's tier, which is the minimum over every rule in the set | `Sound` — the rule's own |
| **Silent** | `DerivationStep.Soundness` | its rule set's tier | the weakest tier any rewrite that actually fired inside the step holds at |
| **Silent** | `"5 - (0; 1)".ToEntity().Simplify()`, and every interval subtracted from something | `(5; 4)` — a left end above its right, so the empty set | `(4; 5)` |
| **Silent** | `"4.5 in (5 - (0; 1))".ToEntity().Simplify()` | `False` | `True` |
| **Silent** | `"5 - [0; 1)".ToEntity().Simplify()` | `[5; 4)` — the openness left where it was | `(4; 5]` |
| **Silent** | `"arccotan(-1)".ToEntity().Simplify()`, and every negative argument the inverse-trigonometric table knows | `3/4 * pi` — the textbook range, and **not equal to `arccotan(-1)`**, whose value is `-pi/4` | `-1/4 * pi` |

### Every rule set describes the rules it runs
Expand Down Expand Up @@ -190,6 +193,35 @@ positive argument, a negative one and zero rather than recalling it.
is `[-pi/2, pi/2]`, so `pi/2 - arcsin(x)` holds for every argument. That is asserted too, so that a
later tidy-up cannot merge the two paths back together.

### Subtracting an interval turns it round

`Minusf` slid an interval's ends without swapping them, so subtracting one produced an interval whose
left end was above its right — which is empty:

```csharp
"5 - (0; 1)".ToEntity().Simplify() // was (5; 4), is (4; 5)
"4.5 in (5 - (0; 1))".ToEntity().Simplify() // was False, is True
```

The second line is the one that matters: a wrong answer reached through the operation an interval
exists for.

**The openness is the half that is easy to miss.** `5 - (0; 1]` is `[4; 5)` — the *excluded* 1 becomes
the excluded lower end 4, and the *included* 0 becomes the included upper end 5. Swapping the ends and
leaving the flags where they were would give `(4; 5]`: right about the width and wrong at both ends,
which no test on the printed form alone would catch. `IntervalSubtractionTest` asserts membership as
well for that reason.

An interval *minus* a number was always right and stays right — `(0; 1) - 5` is `(-5; -4)`, and
nothing turns round because nothing is being reflected.

**What this does not fix**, and the boundary is asserted rather than left implicit: `0 - x` is negated
by an earlier arm, so `0 - [0; 1)` is `-[0; 1)` and stops there. Negating an interval is multiplying
one, and `Mulf` has no interval case at all — `(0; 1) * 2` is left alone too. That is
[#322](https://github.com/asc-community/AngouriMath/issues/322)'s remaining half, and it needs a sign
analysis this does not: a negative multiplier turns the interval round exactly as subtraction does,
and a zero one collapses it to a point.

### An equation nothing settled is no longer answered with the empty set

`Solve` and `SolveEquation` returned an empty `FiniteSet` for two different things: an equation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,15 @@ protected override Entity InnerSimplify(bool isExact) =>
(var n1, Integer(0)) => n1,
(Integer(0), var n2) => -n2,
(Interval inter, var n2) when n2 is not Set => inter.New((inter.Left - n2).InnerSimplified(isExact), (inter.Right - n2).InnerSimplified(isExact)),
(var n2, Interval inter) when n2 is not Set => inter.New((n2 - inter.Left).InnerSimplified(isExact), (n2 - inter.Right).InnerSimplified(isExact)),
// Subtracting an interval turns it round, so the ends swap and their
// openness swaps with them. `5 - (0; 1]` is `[4; 5)`: the excluded 1
// becomes the excluded lower end 4, and the included 0 becomes the
// included upper end 5. Written as `New(n2 - Left, n2 - Right)` it came
// back as `(5; 4]` -- an interval whose left end is above its right, which
// is empty, so `4.5 in (5 - (0; 1))` answered False.
(var n2, Interval inter) when n2 is not Set => inter.New(
(n2 - inter.Right).InnerSimplified(isExact), inter.RightClosed,
(n2 - inter.Left).InnerSimplified(isExact), inter.LeftClosed),
_ => null
},
(@this, a, b) => ((Minusf)@this).New(a, b), isExact);
Expand Down
94 changes: 94 additions & 0 deletions Sources/Tests/UnitTests/Core/Sets/IntervalSubtractionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// 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.Extensions;
using Xunit;

namespace AngouriMath.Tests.Core.Sets
{
/// <summary>
/// Subtracting an interval turns it round, so its ends swap — and their openness swaps with
/// them.
/// </summary>
/// <remarks>
/// <para>
/// <c>5 - (0; 1)</c> came back as <c>(5; 4)</c>: an interval whose left end is above its right,
/// which is empty. So <c>4.5 in (5 - (0; 1))</c> answered <b>False</b> — a wrong answer reached
/// through the operation an interval exists for.
/// </para>
/// <para>
/// The openness is the half that is easy to miss. <c>5 - (0; 1]</c> is <c>[4; 5)</c>: the
/// <i>excluded</i> 1 becomes the excluded lower end 4, and the <i>included</i> 0 becomes the
/// included upper end 5. Swapping the ends and leaving the flags where they were would give
/// <c>(4; 5]</c>, which is wrong at both ends and right about the width — the shape of error
/// that a test on <c>Stringize</c> alone would not catch, which is why membership is asserted
/// here as well.
/// </para>
/// </remarks>
[Trait("Area", "Sets")]
public sealed class IntervalSubtractionTest
{
[Theory]
[InlineData("5 - (0; 1)", "(4; 5)")]
[InlineData("5 - [0; 1]", "[4; 5]")]
[InlineData("5 - (0; 1]", "[4; 5)")]
[InlineData("5 - [0; 1)", "(4; 5]")]
[InlineData("1 - (0; 1)", "(0; 1)")]
public void SubtractingAnIntervalTurnsItRound(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

/// <summary>
/// <b>Zero is not among them, and that is a different gap.</b> <c>0 - x</c> is negated by
/// an earlier arm, so <c>0 - [0; 1)</c> becomes <c>-[0; 1)</c> and stops there: negating an
/// interval is multiplying one, and <c>Mulf</c> has no interval case at all —
/// <c>(0; 1) * 2</c> is left alone too.
/// </summary>
/// <remarks>
/// Asserted as it is rather than left unmentioned, so that the boundary of this fix is
/// written down. Multiplying an interval is
/// <a href="https://github.com/asc-community/AngouriMath/issues/322">#322</a>'s remaining
/// half and needs a sign analysis this does not: a negative multiplier turns the interval
/// round exactly as subtraction does, and a zero one collapses it to a point.
/// </remarks>
[Fact]
public void MultiplyingAnIntervalIsStillNotDone()
{
Assert.Equal("-[0; 1)".ToEntity().Simplify(), "0 - [0; 1)".ToEntity().Simplify());
Assert.Equal("(0; 1) * 2".ToEntity(), "(0; 1) * 2".ToEntity().Simplify());
}

/// <summary>
/// An interval subtracted from a number keeps its width, which the reversed form did not:
/// a left end above the right is the empty set.
/// </summary>
[Theory]
[InlineData("4.5 in (5 - (0; 1))", true)]
[InlineData("4.1 in (5 - (0; 1))", true)]
[InlineData("0.5 in (5 - (0; 1))", false)]
[InlineData("5 in (5 - (0; 1))", false)]
[InlineData("4 in (5 - (0; 1))", false)]
[InlineData("5 in (5 - [0; 1))", true)]
[InlineData("4 in (5 - (0; 1])", true)]
public void MembershipIsWhatTheOrderIsFor(string expression, bool expected)
=> Assert.Equal(
expected ? Entity.Boolean.True : Entity.Boolean.False,
expression.ToEntity().Simplify());

/// <summary>
/// The direction that was already right, asserted so that fixing the other one cannot
/// break it: an interval <i>minus</i> a number just slides, and nothing turns round.
/// </summary>
[Theory]
[InlineData("(0; 1) - 5", "(-5; -4)")]
[InlineData("[0; 1) - 5", "[-5; -4)")]
[InlineData("(0; 1] - 5", "(-5; -4]")]
[InlineData("(0; 1) + 1", "(1; 2)")]
[InlineData("[0; 1) + 1", "[1; 2)")]
public void AnIntervalLessANumberOnlySlides(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());
}
}
Loading