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
44 changes: 44 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ read first.
| **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]` |
| | `"3 * [2; 3]".ToEntity().Simplify()`, and every interval scaled by a constant | `3 * [2; 3]` — left alone | `[6; 9]` |
| | `"[2; 3] / 2".ToEntity().Simplify()` | `[2; 3] / 2` | `[1; 3/2]` |
| **Silent** | `"[2; 3) * (-1)".ToEntity().Simplify()` | `[2; 3) * (-1)` | `(-3; -2]` — reflected, ends and openness both |
| | `"0 - [0; 1)".ToEntity().Simplify()` | `-[0; 1)` | `(-1; 0]` |
| **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 @@ -222,6 +226,46 @@ one, and `Mulf` has no interval case at all — `(0; 1) * 2` is left alone too.
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 interval scaled by a constant is an interval

`Sumf` and `Minusf` had interval cases and `Mulf` and `Divf` had none, so `(0; 1) + 1` answered
`(1; 2)` while `(0; 1) * 2` was handed back. Three rows of `Core/Sets/Arithmetics` recorded that
asymmetry as the expected behaviour, directly beneath the two addition rows that answer.

```csharp
"3 * [2; 3]".ToEntity().Simplify() // was 3 * [2; 3], is [6; 9]
"[2; 3] / 2".ToEntity().Simplify() // was [2; 3] / 2, is [1; 3/2]
```

**A negative factor reflects the interval**, so its ends swap and their openness swaps with them —
exactly as subtracting one does:

| | Is |
|---|---|
| `[2; 3) * (-1)` | `(-3; -2]` |
| `(2; 3] / (-1)` | `[-3; -2)` |
| `0 - [0; 1)` | `(-1; 0]` |

The last of those is not a subtraction at all: `0 - x` is negated by an earlier arm, so it reaches
`Mulf` as `-1 * [0; 1)` and is answered there. It is the case the subtraction fix above had to leave
out, and it comes back for free.

**An unknown sign is answered by not answering.** `(0; 1) * k` for a symbolic `k` is one interval
when `k` is positive and the reflected one when it is negative, so picking either would be choosing
which; it is left unevaluated, which is what an unevaluated node means.

Two boundaries, asserted rather than left to be discovered. `(0; 1) * 0` still answers the number `0`
rather than the set `{ 0 }` — that arm is over every `Entity` and not only intervals, and moving it
would change matrices and finite sets with it. And `2 / (0; 1)` is left alone: a constant over an
interval straddling zero is two unbounded pieces rather than one interval, so there is no `Interval`
to answer with.

This is [#322](https://github.com/asc-community/AngouriMath/issues/322)'s arithmetic half. Its body
says it "will be possible once we implement quantifiers"; scaling needs none — it is monotone in the
factor's sign and in nothing else. Applying a non-monotonic function to an interval, which is the
other half, is still open: `ln((0; 1))` is `(-oo; 0)` because `ln` increases, but `sin((0; 7))` needs
to know where the turning points are.

### 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 @@ -12,6 +12,43 @@ namespace AngouriMath
{
partial record Entity
{
/// <summary>
/// <paramref name="interval"/> scaled by <paramref name="factor"/>, or
/// <see langword="null"/> where the factor's sign is not known.
/// </summary>
/// <remarks>
/// <para>
/// Scaling is monotone in the factor's sign and in nothing else, so this is the whole of
/// it: a positive factor carries both ends where they were, and a negative one
/// <b>reflects the interval</b> — the ends swap, and their openness swaps with them,
/// exactly as subtracting an interval does. <c>(0; 1] * -2</c> is <c>[-2; 0)</c>: the
/// included 1 becomes the included lower end -2, and the excluded 0 becomes the excluded
/// upper end 0.
/// </para>
/// <para>
/// <b>An unknown sign is answered by not answering.</b> <c>(0; 1) * k</c> for a symbolic
/// <c>k</c> is one interval when <c>k</c> is positive and the reflected one when it is
/// negative, and picking either would be choosing which. It is left alone, which is what
/// an unevaluated node means.
/// </para>
/// <para>
/// Zero never reaches here: <c>Mulf</c> answers a multiplication by zero before this is
/// consulted, and gives the number <c>0</c> rather than the set <c>{ 0 }</c>. That is
/// older than this and is left as it is — the arm it comes from is over every
/// <see cref="Entity"/> and not only intervals.
/// </para>
/// </remarks>
private static Entity? ScaledInterval(Interval interval, Entity factor, bool isExact)
=> factor.Evaled is Real { IsFinite: true } real && !real.IsZero
? real.IsPositive
? interval.New(
(interval.Left * factor).InnerSimplified(isExact), interval.LeftClosed,
(interval.Right * factor).InnerSimplified(isExact), interval.RightClosed)
: interval.New(
(interval.Right * factor).InnerSimplified(isExact), interval.RightClosed,
(interval.Left * factor).InnerSimplified(isExact), interval.LeftClosed)
: null;

/// <summary>
/// The value of <paramref name="expr"/> where it has one, with the condition it holds
/// under, or <see langword="null"/> where there is no value to read.
Expand Down Expand Up @@ -104,6 +141,10 @@ protected override Entity InnerSimplify(bool isExact) =>
(var n1, Integer(1)) => n1,
(Integer(1), var n2) => n2,
(var n1, var n2) when n1 == n2 => new Powf(n1, 2).InnerSimplified(isExact),
// After the zero arms above, which answer a multiplication by zero for
// every Entity and not only for an interval.
(Interval inter, var n2) when n2 is not Set => ScaledInterval(inter, n2, isExact),
(var n2, Interval inter) when n2 is not Set => ScaledInterval(inter, n2, isExact),
_ => null
},
(@this, a, b) => ((Mulf)@this).New(a, b), isExact);
Expand Down Expand Up @@ -132,6 +173,12 @@ n0.Evaled is Complex c
(var n1, Powf(Rational radicand, Rational exponent)) when
RootExtraction.PullOutOfDenominator(radicand, exponent) is { } rationalized
=> (n1 * rationalized).InnerSimplified(isExact),
// An interval over a constant is that constant's reciprocal scaling it. Only
// this direction: a constant over an interval that straddles zero is two
// unbounded pieces rather than one interval, so it is not an Interval at all
// and is left alone rather than answered wrongly.
(Interval inter, var n2) when n2 is not Set
=> ScaledInterval(inter, (1 / n2).InnerSimplified(isExact), isExact),
_ => null
},
(@this, a, b) => ((Divf)@this).New(a, b), isExact);
Expand Down
13 changes: 10 additions & 3 deletions Sources/Tests/UnitTests/Core/Sets/Arithmetics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ public sealed class Arithmetics
[Theory]
[InlineData("[2; 3] + 3", "[5; 6]")]
[InlineData("3 + [2; 3]", "[5; 6]")]
[InlineData("3 * [2; 3]", "3 * [2; 3]")]
[InlineData("[2; 3] * 3", "[2; 3] * 3")]
[InlineData("[2; 3] / 2", "[2; 3] / 2")]
// These three recorded the absence of the capability -- `3 * [2; 3]` expecting itself back
// -- sitting directly under the two addition rows that answer. The asymmetry was `Sumf`
// and `Minusf` having interval cases while `Mulf` and `Divf` had none.
[InlineData("3 * [2; 3]", "[6; 9]")]
[InlineData("[2; 3] * 3", "[6; 9]")]
[InlineData("[2; 3] / 2", "[1; 3/2]")]
// And the half that is not just arithmetic: a negative factor reflects the interval, so
// the ends swap and their openness swaps with them.
[InlineData("[2; 3) * (-1)", "(-3; -2]")]
[InlineData("(2; 3] / (-1)", "[-3; -2)")]
[InlineData("{ 1, 2, 3 } + 10", "{ 11, 12, 13 }")]
[InlineData("10 + { 1, 2, 3 }", "{ 11, 12, 13 }")]
[InlineData("{ 1, 2, 3 } * 10", "{ 10, 20, 30 }")]
Expand Down
130 changes: 130 additions & 0 deletions Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// 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>
/// Scaling an interval by a constant, which is
/// <a href="https://github.com/asc-community/AngouriMath/issues/322">#322</a>'s remaining half.
/// </summary>
/// <remarks>
/// <para>
/// <c>Sumf</c> and <c>Minusf</c> had interval cases and <c>Mulf</c> and <c>Divf</c> had none, so
/// <c>(0; 1) + 1</c> answered <c>(1; 2)</c> while <c>(0; 1) * 2</c> was left alone. The issue's
/// body says it "will be possible once we implement quantifiers"; it needs none — scaling is
/// monotone in the factor's sign and in nothing else.
/// </para>
/// <para>
/// <b>A negative factor reflects the interval</b>, so the ends swap and their openness swaps
/// with them, exactly as subtracting one does. That is the half a test on the printed form
/// alone would not catch: swapping the ends and leaving the flags where they were gives the
/// right width and the wrong bracket at both ends, so membership is asserted too.
/// </para>
/// </remarks>
[Trait("Area", "Sets")]
public sealed class IntervalScalingTest
{
[Theory]
[InlineData("(0; 1) * 2", "(0; 2)")]
[InlineData("2 * (0; 1)", "(0; 2)")]
[InlineData("(0; 1] * 2", "(0; 2]")]
[InlineData("[0; 1) * 2", "[0; 2)")]
[InlineData("(0; 1) * 1/2", "(0; 1/2)")]
[InlineData("(0; 1) * pi", "(0; pi)")]
public void APositiveFactorCarriesBothEndsWhereTheyWere(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

/// <summary>
/// The ends swap, and so does their openness: <c>(0; 1] * -2</c> is <c>[-2; 0)</c> — the
/// included 1 becomes the included lower end, and the excluded 0 the excluded upper one.
/// </summary>
[Theory]
[InlineData("(0; 1) * (-2)", "(-2; 0)")]
[InlineData("(-2) * (0; 1)", "(-2; 0)")]
[InlineData("(0; 1] * (-2)", "[-2; 0)")]
[InlineData("[0; 1) * (-2)", "(-2; 0]")]
public void ANegativeFactorReflectsIt(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

[Theory]
[InlineData("(2; 4) / 2", "(1; 2)")]
[InlineData("(2; 4) / (-2)", "(-2; -1)")]
[InlineData("(0; 1] / (-2)", "[-1/2; 0)")]
public void DividingByAConstantIsScalingByItsReciprocal(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

/// <summary>
/// <b>Negating an interval is multiplying it, so this comes for free — and it is the case
/// #1117 had to leave out.</b> <c>0 - x</c> is negated by an earlier arm, so
/// <c>0 - [0; 1)</c> became <c>-[0; 1)</c> and stopped there; with <c>Mulf</c> answering,
/// it reaches <c>(-1; 0]</c>.
/// </summary>
[Theory]
[InlineData("-(0; 1)", "(-1; 0)")]
[InlineData("-[0; 1)", "(-1; 0]")]
[InlineData("0 - [0; 1)", "(-1; 0]")]
[InlineData("0 - (0; 1]", "[-1; 0)")]
public void NegatingAnIntervalReflectsItToo(string expression, string expected)
=> Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());

/// <summary>
/// Membership, which is what the order and the brackets are for.
/// </summary>
[Theory]
[InlineData("1 in ((0; 1) * 2)", true)]
[InlineData("2 in ((0; 1) * 2)", false)]
[InlineData("2 in ((0; 1] * 2)", true)]
[InlineData("-1 in ((0; 1) * (-2))", true)]
[InlineData("-2 in ((0; 1] * (-2))", true)]
[InlineData("-2 in ((0; 1) * (-2))", false)]
[InlineData("0 in ((0; 1] * (-2))", false)]
[InlineData("1.5 in ((2; 4) / 2)", true)]
[InlineData("3 in ((2; 4) / 2)", false)]
public void MembershipIsWhatTheReflectionIsFor(string expression, bool expected)
=> Assert.Equal(
expected ? Entity.Boolean.True : Entity.Boolean.False,
expression.ToEntity().Simplify());

/// <summary>
/// <b>An unknown sign is answered by not answering.</b> <c>(0; 1) * k</c> is one interval
/// when <c>k</c> is positive and the reflected one when it is negative; picking either
/// would be choosing which, so it is left alone — which is what an unevaluated node means.
/// </summary>
[Theory]
[InlineData("(0; 1) * k")]
[InlineData("(0; 1) / k")]
public void AnUnknownSignIsLeftAlone(string expression)
=> Assert.Equal(expression.ToEntity(), expression.ToEntity().Simplify());

/// <summary>
/// Two boundaries of this change, asserted so that they are written down rather than
/// discovered.
/// </summary>
/// <remarks>
/// <para>
/// <c>(0; 1) * 0</c> answers the number <c>0</c> rather than the set <c>{ 0 }</c>. That is
/// older than this and untouched: the arm it comes from is over every <c>Entity</c>, not
/// only intervals, and moving it would change matrices and sets alike.
/// </para>
/// <para>
/// <c>2 / (0; 1)</c> is left alone. A constant over an interval that straddles zero is two
/// unbounded pieces rather than one interval, so there is no <c>Interval</c> to answer
/// with — and answering only the non-straddling case would make the shape of the result
/// depend on the endpoints in a way this does not attempt.
/// </para>
/// </remarks>
[Fact]
public void TheTwoBoundariesOfThis()
{
Assert.Equal("0".ToEntity(), "(0; 1) * 0".ToEntity().Simplify());
Assert.Equal("2 / (0; 1)".ToEntity(), "2 / (0; 1)".ToEntity().Simplify());
}
}
}
21 changes: 10 additions & 11 deletions Sources/Tests/UnitTests/Core/Sets/IntervalSubtractionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,22 @@ 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.
/// <b>Zero goes the other way round, and it works for a different reason.</b> <c>0 - x</c>
/// is negated by an earlier arm, so <c>0 - [0; 1)</c> becomes <c>-[0; 1)</c> — a
/// multiplication rather than a subtraction, and it reaches <c>(-1; 0]</c> through
/// <c>Mulf</c>'s interval case.
/// </summary>
/// <remarks>
/// Asserted as it is rather than left unmentioned, so that the boundary of this fix is
/// written down. Multiplying an interval is
/// This test asserted the opposite until that case existed: multiplying an interval was
/// <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.
/// half, and the boundary of the subtraction fix was written down here so that it would be
/// found rather than discovered. <c>IntervalScalingTest</c> is where scaling is held now.
/// </remarks>
[Fact]
public void MultiplyingAnIntervalIsStillNotDone()
public void NegatingAnIntervalGoesThroughMultiplication()
{
Assert.Equal("-[0; 1)".ToEntity().Simplify(), "0 - [0; 1)".ToEntity().Simplify());
Assert.Equal("(0; 1) * 2".ToEntity(), "(0; 1) * 2".ToEntity().Simplify());
Assert.Equal("(-1; 0]".ToEntity(), "0 - [0; 1)".ToEntity().Simplify());
Assert.Equal("(0; 2)".ToEntity(), "(0; 1) * 2".ToEntity().Simplify());
}

/// <summary>
Expand Down
Loading