diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 7a602ae02..6c17f41c4 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -884,6 +884,28 @@ but 0, and `log`'s own condition already excluded it. A base that is small but n
| `"log(x, 2)".ToEntity().Compile("x").Call(0)` | `(-0, -0)` | `(NaN, 0)` |
| `"log(x, 1)".ToEntity().Simplify()` | `0 provided not x = 1` | `0 provided not x = 0 and not x = 1` |
+### A difference of two intervals keeps the parts on either side
+
+**Silent: `A \ B` for two intervals is the part of `A` below `B` and the part above it, each holding the end `B`
+leaves.** It was one interval, from an end of `A` to the far end of `B`, where `B` lay inside `A`. Where `B` covered
+an end of `A`, the end of the cut was read from `B`'s other end. And an end of `A` that `B` left out was lost.
+Membership follows the set, so `5 in ([0; 10] \ (2; 3))` was `False`. Ends such as `sqrt(2)` are compared by their
+values, where the difference was left as written. With ends that have no value, `[a; b] \ (a; b)` is left as written,
+since it is `{ a, b }` where `a < b` and nothing where `a > b`
+([#2290](https://github.com/asc-community/AngouriMath/issues/2290)).
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"[0; 10] \\ (2; 3)".ToEntity().Simplify()` | `[0; 3]` | `[0; 2] \/ [3; 10]` |
+| `"[0; 10] \\ [-1; 3)".ToEntity().Simplify()` | `(3; 10]` | `[3; 10]` |
+| `"[0; 10] \\ (0; 3)".ToEntity().Simplify()` | `[3; 10]` | `{ 0 } \/ [3; 10]` |
+| `"[0; 10] \\ (0; 10)".ToEntity().Simplify()` | `[0; 10]` | `{ 0, 10 }` |
+| `"(-oo; +oo) \\ [0; 1)".ToEntity().Simplify()` | `(-oo; 1)` | `(-oo; 0) \/ [1; +oo)` |
+| `"[0; sqrt(2)] \\ [1; 2]".ToEntity().Simplify()` | `[0; sqrt(2)] \ [1; 2]` | `[0; 1)` |
+| `"(a; b) \\ [a; b]".ToEntity().Simplify()` | `(a; b)` | `{ }` |
+| `"[a; b] \\ (a; b)".ToEntity().Simplify()` | `[a; b]` | `[a; b] \ (a; b)` |
+| `"5 in ([0; 10] \\ (2; 3))".ToEntity().Simplify()` | `False` | `True` |
+
### `re` and `im` are the real and the imaginary part
**Not silent: `re(…)` and `im(…)`, and `real(…)` and `imag(…)`, are now the real and the imaginary part, where they were
diff --git a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.SetSubtraction.cs b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.SetSubtraction.cs
index 4f98c61bb..2ec5e719c 100644
--- a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.SetSubtraction.cs
+++ b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.SetSubtraction.cs
@@ -26,42 +26,67 @@ internal static Set SetSubtractSetAndFiniteSet(Set set, FiniteSet finite)
private static bool Implies(this bool assumption, bool conclusion)
=> !assumption || conclusion;
+ ///
+ /// What is left of with taken out: the part of
+ /// below and the part above it, each an
+ /// interval, a point or nothing.
+ ///
+ ///
+ /// Each part ends where begins, and holds that end where
+ /// does not: [0; 10] \ (2; 3) is [0; 2] \/ [3; 10],
+ /// and [0; 10] \ (0; 3) is { 0 } \/ [3; 10]. One interval from an end of
+ /// to the far end of was answered for the first,
+ /// [0; 3], the end of the cut was read from the other end of
+ /// , and an end of that left
+ /// was lost. The ends are compared by their values, as the intersection compares them,
+ /// and kept as written. Where an end has no value the difference is left as it is, unless
+ /// the ends are the same and holds every end
+ /// holds; then it holds all of . Where holds an
+ /// end that does not, what is left depends on the order of the ends:
+ /// [a; b] \ (a; b) is { a, b } where a < b and nothing where
+ /// a > b.
+ /// https://github.com/asc-community/AngouriMath/issues/2290
+ ///
internal static Set SetSubtractIntervalAndInterval(Interval A, Interval B)
{
if (A == B)
return Empty;
- if (A.Left == B.Left && A.Right == B.Right)
- return new Interval(A.Left, A.LeftClosed && !B.LeftClosed, A.Right, A.RightClosed && !B.RightClosed);
- if (A.Left is not Real aLeft ||
- A.Right is not Real aRight ||
- B.Left is not Real bLeft ||
- B.Right is not Real bRight)
+ if (A.Left == B.Left && A.Right == B.Right && A.LeftClosed.Implies(B.LeftClosed) && A.RightClosed.Implies(B.RightClosed))
+ return Empty;
+ if (A.Left.Evaled is not Real { IsNaN: false } aLeft ||
+ A.Right.Evaled is not Real { IsNaN: false } aRight ||
+ B.Left.Evaled is not Real { IsNaN: false } bLeft ||
+ B.Right.Evaled is not Real { IsNaN: false } bRight)
return A.SetSubtract(B);
-
- if (aLeft > aRight)
+ if (aLeft > aRight || aLeft == aRight && !(A.LeftClosed && A.RightClosed))
return Empty;
- if (bLeft > bRight)
- return A;
- if (aLeft == bRight)
- return B.RightClosed ? A.New(A.Left, false, A.Right, A.RightClosed) : A;
- if (aRight == bLeft)
- return B.LeftClosed ? A.New(A.Left, A.LeftClosed, A.Right, false) : A;
- if (aRight < bLeft || aLeft > bRight)
+ if (bLeft > bRight || bLeft == bRight && !(B.LeftClosed && B.RightClosed))
return A;
- if (
- (bLeft < aLeft || bLeft == aLeft && A.LeftClosed.Implies(B.LeftClosed))
- &&
- (bRight > aRight || bRight == aRight && A.RightClosed.Implies(B.RightClosed))
- )
- return Empty;
- if (aLeft >= bLeft && aRight >= bRight)
- return new Interval(bRight, aLeft == bLeft ? A.LeftClosed && !B.LeftClosed : !B.LeftClosed,
- aRight, aRight == bRight ? A.RightClosed && !B.RightClosed : A.RightClosed);
- if (aLeft <= bLeft && aRight <= bRight)
- return new Interval(aLeft, aLeft == bLeft ? A.LeftClosed && !B.LeftClosed : A.LeftClosed,
- bLeft, aRight == bRight ? A.RightClosed && !B.RightClosed : !B.RightClosed);
- return new Interval(aLeft, aLeft == bLeft ? A.LeftClosed && !B.LeftClosed : !B.LeftClosed,
- bRight, aRight == bRight ? A.RightClosed && !B.RightClosed : A.RightClosed);
+ // Below B: from A's left end to whichever comes first, A's right end or B's left end.
+ var (belowEnd, belowEndValue, belowEndClosed) =
+ aRight < bLeft ? (A.Right, aRight, A.RightClosed) :
+ bLeft < aRight ? (B.Left, bLeft, !B.LeftClosed) :
+ (A.Right, aRight, A.RightClosed && !B.LeftClosed);
+ // Above B: from whichever comes last, A's left end or B's right end, to A's right end.
+ var (aboveStart, aboveStartValue, aboveStartClosed) =
+ aLeft > bRight ? (A.Left, aLeft, A.LeftClosed) :
+ bRight > aLeft ? (B.Right, bRight, !B.RightClosed) :
+ (A.Left, aLeft, A.LeftClosed && !B.RightClosed);
+ var below = Part(A.Left, aLeft, A.LeftClosed, belowEnd, belowEndValue, belowEndClosed);
+ var above = Part(aboveStart, aboveStartValue, aboveStartClosed, A.Right, aRight, A.RightClosed);
+ return (below, above) switch
+ {
+ (null, null) => Empty,
+ ({ } low, null) => low,
+ (null, { } high) => high,
+ (FiniteSet { Count: 1 } low, FiniteSet { Count: 1 } high) => new FiniteSet(low.First(), high.First()),
+ ({ } low, { } high) => low.Unite(high),
+ };
+
+ static Set? Part(Entity left, Real leftValue, bool leftClosed, Entity right, Real rightValue, bool rightClosed)
+ => leftValue < rightValue ? new Interval(left, leftClosed, right, rightClosed)
+ : leftValue == rightValue && leftClosed && rightClosed ? new FiniteSet(left)
+ : null;
}
internal static Set SetSubtractCSetAndCSet(ConditionalSet intLeft, ConditionalSet intRight)
diff --git a/Sources/Tests/UnitTests/Core/Sets/IntervalAndInterval.cs b/Sources/Tests/UnitTests/Core/Sets/IntervalAndInterval.cs
index 97015dd67..003ab6165 100644
--- a/Sources/Tests/UnitTests/Core/Sets/IntervalAndInterval.cs
+++ b/Sources/Tests/UnitTests/Core/Sets/IntervalAndInterval.cs
@@ -6,6 +6,8 @@
//
using AngouriMath;
+using System.Collections.Generic;
+using System.Linq;
using Xunit;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Set;
@@ -86,6 +88,96 @@ public void TwoRaysMeetWhateverTheirEnds(string intersection, string expected)
[Fact] public void Subtraction5() => TestInt(A.SetSubtract(F), Interval(2, true, 3, false));
[Fact] public void Subtraction6() => TestInt(F.SetSubtract(A), Interval(5, false, 6, true));
+ ///
+ /// What is left of an interval with another taken out is the part below it and the part
+ /// above it, and each part holds the end the second one leaves. One interval from an end
+ /// of the first to the far end of the second used to be answered, with its ends read from
+ /// the wrong side. https://github.com/asc-community/AngouriMath/issues/2290
+ ///
+ [Theory]
+ [InlineData("[0; 10] \\ (2; 3)", "[0; 2] \\/ [3; 10]")]
+ [InlineData("[0; 10] \\ [2; 3]", "[0; 2) \\/ (3; 10]")]
+ [InlineData("(0; 10) \\ (2; 3)", "(0; 2] \\/ [3; 10)")]
+ [InlineData("[0; 10] \\ [-1; 3)", "[3; 10]")]
+ [InlineData("[0; 10] \\ (-1; 3]", "(3; 10]")]
+ [InlineData("[0; 10] \\ (5; 12]", "[0; 5]")]
+ [InlineData("[0; 10] \\ [5; 12)", "[0; 5)")]
+ [InlineData("[0; 10] \\ (0; 3)", "{ 0 } \\/ [3; 10]")]
+ [InlineData("[0; 10] \\ (7; 10)", "[0; 7] \\/ { 10 }")]
+ [InlineData("[0; 10] \\ (0; 10)", "{ 0, 10 }")]
+ [InlineData("[0; 10] \\ [0; 10)", "{ 10 }")]
+ [InlineData("[0; 10] \\ [5; 5]", "[0; 10] \\ { 5 }")]
+ [InlineData("[0; 10] \\ (5; 5)", "[0; 10]")]
+ [InlineData("[0; sqrt(2)] \\ [1; 2]", "[0; 1)")]
+ [InlineData("(-oo; +oo) \\ [0; 1)", "(-oo; 0) \\/ [1; +oo)")]
+ public void ADifferenceKeepsThePartsOnEitherSide(string difference, string expected)
+ => Assert.Equal(MathS.FromString(expected).Simplify(), MathS.FromString(difference).Simplify());
+
+ ///
+ /// The difference holds a point exactly where the first interval does and the second does
+ /// not, over every order of the four ends, with each end open or closed, and with either
+ /// interval reversed or at a single point. Two intervals that each have more than one point
+ /// are subtracted to a set without a difference in it; one that has a point or none is a
+ /// listed set by then, and the difference may stay.
+ /// https://github.com/asc-community/AngouriMath/issues/2290
+ ///
+ [Fact]
+ public void ADifferenceHoldsWhatTheFirstHoldsAndTheSecondDoesNot()
+ {
+ var points = Enumerable.Range(-1, 9).Select(k => (Entity)Entity.Number.Rational.Create(k, 2)).ToArray();
+ var failures = new List();
+ foreach (var aLeft in Enumerable.Range(0, 4))
+ foreach (var aRight in Enumerable.Range(0, 4))
+ foreach (var bLeft in Enumerable.Range(0, 4))
+ foreach (var bRight in Enumerable.Range(0, 4))
+ foreach (var flags in Enumerable.Range(0, 16))
+ {
+ var a = Interval(aLeft, (flags & 1) != 0, aRight, (flags & 2) != 0);
+ var b = Interval(bLeft, (flags & 4) != 0, bRight, (flags & 8) != 0);
+ var difference = a.SetSubtract(b).InnerSimplified;
+ if (difference is not Set left || left is SetMinusf && aLeft < aRight && bLeft < bRight)
+ {
+ failures.Add($"{a} \\ {b} is not decided: {difference}");
+ continue;
+ }
+ foreach (var point in points)
+ {
+ var expected = Holds(a, point) && !Holds(b, point);
+ if (!left.TryContains(point, out var contains) || contains != expected)
+ failures.Add($"{point} in {a} \\ {b} = {left}: expected {expected}");
+ }
+ }
+ Assert.Empty(failures);
+
+ static bool Holds(Interval interval, Entity point)
+ {
+ var value = (Entity.Number.Real)point.EvalNumerical();
+ var (left, right) = ((Entity.Number.Real)interval.Left.EvalNumerical(), (Entity.Number.Real)interval.Right.EvalNumerical());
+ return (left < value || interval.LeftClosed && left == value) && (value < right || interval.RightClosed && value == right);
+ }
+ }
+
+ ///
+ /// Ends that have no value say whether anything is left only when they are the same and
+ /// the second interval holds every end the first holds. [a; b] \ (a; b) is
+ /// { a, b } where a < b and nothing where a > b, so it stays as
+ /// it is, and it used to be answered [a; b].
+ /// https://github.com/asc-community/AngouriMath/issues/2290
+ ///
+ [Theory]
+ [InlineData("[a; b] \\ (a; b)")]
+ [InlineData("[a; b] \\ [a; b)")]
+ [InlineData("(a; b] \\ (a; b)")]
+ public void ADifferenceOfSymbolicIntervalsWithAnEndLeftStays(string difference)
+ => Assert.IsType(MathS.FromString(difference).InnerSimplified);
+
+ [Theory]
+ [InlineData("(a; b) \\ [a; b]")]
+ [InlineData("[a; b) \\ [a; b]")]
+ [InlineData("(a; b] \\ (a; b]")]
+ public void ADifferenceOfSymbolicIntervalsThatCoversIsEmpty(string difference)
+ => Assert.Equal(Set.Empty, MathS.FromString(difference).InnerSimplified);
+
///
/// An interval whose ends are reversed holds nothing: it meets nothing, and adds nothing to
/// a union. The meeting used to be the other interval.