From 2c74c073941cfc2b7a43d7e068bc1669fa3ecfee Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 30 Sep 2026 17:47:18 +0000 Subject: [PATCH] A union with an empty interval is the other set Two intervals were joined wherever one ended where the other began, before either was asked whether it was empty: [0; 1] \/ [1; 0] became { 1 }, [3; 1] \/ [1; 2] the empty [3; 2], and [a; b] \/ [b; a] became { b }. An empty interval now adds nothing, and two intervals with ends that are not numbers are joined only where neither can be empty -- an end at an infinity, or a single point closed at both ends -- so [a; b] \/ [b; c] is left as written. A closed point's union was computed and discarded; it is returned now. Closes #1634. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 19 +++++++ .../Entity/Omni/Sets/SetOperators.Union.cs | 50 +++++++++++++---- .../Sets/AnEmptyIntervalAddsNothingTest.cs | 54 +++++++++++++++++++ 3 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/Sets/AnEmptyIntervalAddsNothingTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5589f7269..809368dcf 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -654,6 +654,25 @@ reached 3^12 cases and 8 GB inside integration by parts | `(x < 0 and x = 0).Simplify()` | `False provided x in RR` | `False` — the condition was over-strong, one conjunct is false wherever `x` is | | `(x > 0 and x > 0).Evaled` | `x > 0` | `x > 0` (unchanged) | +### A union with an empty interval is the other set + +**Wrong answer fixed.** Two intervals were joined wherever one ended where the other began, before +either was asked whether it was empty. `[1; 0]` is empty, and it ends at 0 where `[0; 1]` begins, +so `[0; 1] \/ [1; 0]` was joined into `{ 1 }`, and `[3; 1] \/ [1; 2]` into `[3; 2]`, which is +empty. An empty interval now adds nothing. With ends that are not numbers either interval may be +empty -- `[a; b]` is, when `a` is above `b` -- so two such intervals are no longer joined unless an +end is infinite, and `[a; b] \/ [b; c]`, which was `[a; c]`, is left as written. +[#1634](https://github.com/asc-community/AngouriMath/issues/1634). Both columns measured on a +build, `v2.5.0` against this change. + +| `"….".ToEntity().Simplify()` of | Was (2.5.0) | Is | +|---|---|---| +| `[0; 1] \/ [1; 0]` | `{ 1 }` — wrong | `[0; 1]` | +| `[3; 1] \/ [1; 2]` | `[3; 2]`, which is empty — wrong | `[1; 2]` | +| `[a; b] \/ [b; a]` | `{ b }` — wrong | `[a; b] \/ [b; a]` | +| `[a; b] \/ [b; c]` | `[a; c]` — wrong where `a > b` or `b > c` | `[a; b] \/ [b; c]` | +| `(-oo; x] \/ [x; +oo)` | `RR` | `RR` (unchanged: neither can be empty) | + ### `ZZ*` and `ZZ+` are the non-negative and the positive integers `ZZ*` = `{0, 1, 2, ...}` and `ZZ+` = `{1, 2, 3, ...}` are special sets, spelled as MathWorld spells diff --git a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Union.cs b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Union.cs index 349c7c032..b5039d442 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Union.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Union.cs @@ -108,9 +108,28 @@ internal static Set UniteFiniteSetAndSet(FiniteSet finite, Set set) return sb.IsEmpty ? set : sb.ToFiniteSet().Unite(set); } - // TODO: it requires cleaning + /// + /// The union of two intervals: one interval where they overlap or touch, and as written + /// where they do not, or where that cannot be told. + /// + /// + /// An empty interval adds nothing -- one whose left end is above its right, or at it with + /// an end open -- so [0; 1] \/ [1; 0] is [0; 1]. Two intervals that touch are + /// one only when neither is empty. Touching was tested first, so [1; 0], touching + /// [0; 1] at 0, was joined to it into { 1 }, and [a; b] \/ [b; a] into + /// { b }. Ends that are not numbers do not say whether an interval is empty, unless + /// the ends are one expression closed at both or one end is infinite, as in + /// (-oo; x] \/ [x; +oo); otherwise the union is left as written. + /// https://github.com/asc-community/AngouriMath/issues/1634 + /// internal static Set UniteIntervalAndInterval(Interval A, Interval B) { + if (IsEmpty(A)) + return B; + if (IsEmpty(B)) + return A; + if (!IsNotEmpty(A) || !IsNotEmpty(B)) + return A.Unite(B); if (A.Left == B.Right && (A.LeftClosed || B.RightClosed)) return new Interval(B.Left, B.LeftClosed, A.Right, A.RightClosed); if (A.Right == B.Left && (A.RightClosed || B.LeftClosed)) @@ -120,14 +139,11 @@ A.Right is not Real aRight || B.Left is not Real bLeft || B.Right is not Real bRight) return A.Unite(B); - if (aLeft == aRight && A.LeftClosed && A.RightClosed) - UniteFiniteSetAndSet(new FiniteSet(aLeft), B); - if (bLeft == bRight && B.LeftClosed && B.RightClosed) - UniteFiniteSetAndSet(new FiniteSet(bLeft), A); - if (aLeft >= aRight) - return B; - if (bLeft >= bRight) - return A; + // Neither is empty, so ends that are equal are a point closed at both. + if (aLeft == aRight) + return UniteFiniteSetAndSet(new FiniteSet(aLeft), B); + if (bLeft == bRight) + return UniteFiniteSetAndSet(new FiniteSet(bLeft), A); if (aLeft == bRight && !A.LeftClosed && !B.RightClosed) return A.Unite(B); if (bLeft == aRight && !B.LeftClosed && !A.RightClosed) @@ -151,6 +167,22 @@ B.Left is not Real bLeft || return new Interval(left, leftClosed, right, rightClosed); } + /// Known to be empty: numeric ends out of order, or at one point with an end open. + private static bool IsEmpty(Interval interval) + => interval.Left is Real left && interval.Right is Real right + && (left > right || left == right && !(interval.LeftClosed && interval.RightClosed)); + + /// + /// Known not to be empty: numeric ends in order, the same end at both sides and closed, or + /// an end at an infinity, beyond any end the other side can have. + /// + private static bool IsNotEmpty(Interval interval) + => interval.Left is Real left && interval.Right is Real right + ? left < right || left == right && interval.LeftClosed && interval.RightClosed + : interval.Left == interval.Right + ? interval.LeftClosed && interval.RightClosed + : interval.Left == Real.NegativeInfinity || interval.Right == Real.PositiveInfinity; + internal static Set UniteCSetAndCSet(ConditionalSet intLeft, ConditionalSet intRight) { (intLeft, intRight) = MergeToOneVariable(intLeft, intRight); diff --git a/Sources/Tests/UnitTests/Core/Sets/AnEmptyIntervalAddsNothingTest.cs b/Sources/Tests/UnitTests/Core/Sets/AnEmptyIntervalAddsNothingTest.cs new file mode 100644 index 000000000..c5502ad06 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/Sets/AnEmptyIntervalAddsNothingTest.cs @@ -0,0 +1,54 @@ +// +// 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; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Core.Sets +{ + /// + /// An interval whose left end is above its right is empty, and a union with it is the other + /// set. Two intervals were joined wherever they touched, before either was asked whether it was + /// empty: [0; 1] \/ [1; 0] became { 1 } and [a; b] \/ [b; a] became { b }. + /// https://github.com/asc-community/AngouriMath/issues/1634 + /// + [Trait("Area", "Core")] + public sealed class AnEmptyIntervalAddsNothingTest + { + private static Entity IsIn(string value, string set) => $"{value} in ({set})".ToEntity().Simplify(); + + /// The union is decided at points on both sides of every end, not read off its printed shape. + [Theory] + [InlineData("[0; 1] \\/ [1; 0]", "0, 1/2, 1", "-1, 3/2")] + [InlineData("[1; 0] \\/ [0; 1]", "0, 1/2, 1", "-1, 3/2")] + [InlineData("[3; 1] \\/ [1; 2]", "1, 3/2, 2", "0, 5/2, 3")] + [InlineData("[0; 1] \\/ [1; 1)", "0, 1/2, 1", "-1, 3/2")] + [InlineData("[2; 2] \\/ [0; 1]", "0, 1, 2", "-1, 3/2, 3")] + [InlineData("[0; 1] \\/ [1; 2]", "0, 1, 2", "-1, 3")] + public void TheUnionHasTheMembersOfItsNonEmptyParts(string union, string members, string others) + { + var simplified = union.ToEntity().Simplify().Stringize(); + foreach (var member in members.Split(", ")) + Assert.Equal(Boolean.True, IsIn(member, simplified)); + foreach (var other in others.Split(", ")) + Assert.Equal(Boolean.False, IsIn(other, simplified)); + } + + /// + /// With ends that are not numbers, either interval may be empty, so touching is not joining: + /// the union is left as written. An end at an infinity leaves nothing to doubt. + /// + [Fact] + public void EndsThatAreNotNumbersAreNotJoinedWhereEitherMayBeEmpty() + { + Assert.IsType("[a; b] \\/ [b; a]".ToEntity().Simplify()); + Assert.IsType("[a; b] \\/ [b; c]".ToEntity().Simplify()); + Assert.Equal("RR", "(-oo; x] \\/ [x; +oo)".ToEntity().Simplify().Stringize()); + } + } +}