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());
+ }
+ }
+}