From 9b5b9cba268802b4dc7b449f597879b446958dc7 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 31 Oct 2026 18:35:59 +0000 Subject: [PATCH] A difference of two intervals keeps the parts on either side A \ B for two intervals is now the part of A below B and the part above it, each an interval, a point or nothing, and each holding the end of B that B does not. The last three returns of SetSubtractIntervalAndInterval answered one interval from an end of A to the far end of B where B lay inside A, so [0; 10] \ (2; 3) was [0; 3]. Where B covered an end of A, they read the end of the cut from B's other end, so [0; 10] \ [-1; 3) was (3; 10]. And an end of A that B left was folded into a flag, so [0; 10] \ (0; 3) lost the point 0. Membership followed: 5 in ([0; 10] \ (2; 3)) was False. The ends are compared by their values, as the intersection compares them, and kept as written, so [0; sqrt(2)] \ [1; 2] is [0; 1) where it was left as written. Ends with no value decide only when they are the same and B holds every end A holds: (a; b) \ [a; b] is empty, where it was (a; b). [a; b] \ (a; b) is { a, b } where a < b and nothing where a > b, so it is left as written, where it was [a; b]. IntervalAndInterval checks the issue's table and a few more, and that the difference holds a point exactly where A does and B does not, over every order of four ends from 0 to 3 with each end open or closed, 4096 pairs at nine points each. Nineteen of the new tests fail on master. 2.6.0 answers as master does. The suite, the trio, F#, AOT and the eight harnesses pass. Fixes #2290. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 22 +++++ .../Omni/Sets/SetOperators.SetSubtraction.cs | 83 +++++++++++------ .../Core/Sets/IntervalAndInterval.cs | 92 +++++++++++++++++++ 3 files changed, 168 insertions(+), 29 deletions(-) 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.