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
24 changes: 24 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ read first.
| loud | `Expand` of a quotient of factorials | `AngouriBugException` | the expanded polynomial |
| loud | parsing a `provided` in a parenthesised comma list | `NullReferenceException` | `UnhandledParseException` |
| loud | `floor(x)`, `ceil(x)`, `ceiling(x)` | `UnrecognizedFunctionParseException` | the functions |
| loud | `round(x)`, `min(a, b)`, `max(a, b)`, `gcd(a, b)` | `UnrecognizedFunctionParseException` | the functions |
| loud | the target frameworks | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` |
| **silent** | `abs(x) = c` for a negative `c` | a set of non-solutions | the empty set |

Expand Down Expand Up @@ -1291,6 +1292,29 @@ throw. `Simplify` was never affected — it answered `1 + x` throughout.

Issue [#817](https://github.com/asc-community/AngouriMath/issues/817).

### `round`, `min`, `max` and `gcd` exist, so they no longer raise

```csharp
"round(5/2)".ToEntity().Simplify(); // was UnrecognizedFunctionParseException, is 2
"min(3, 5)".ToEntity().Simplify(); // was UnrecognizedFunctionParseException, is 3
"gcd(1/2, 1/3)".ToEntity().Simplify();// was UnrecognizedFunctionParseException, is 1/6
```

**`round` is half to even**, which is what Python, SymPy, Mathematica and IEEE 754 all mean by
rounding, and what .NET's `Math.Round` does by default: `round(1/2)` is `0`, `round(5/2)` is `2`.
It is deliberately **not** `floor(x + 1/2)`, which disagrees at every tie, and there is a test
pinning that they differ.

`min`, `max` and `gcd` take any number of arguments and fold, so `min(3, 5, 1)` is `1`. `min` and
`max` compare only where the arguments are ordered — an unordered pair is left as the node rather
than guessed at, as SymPy's `Min` does. `gcd` covers integers and rationals, `gcd(1/2, 1/3)` being
`1/6` exactly as SymPy gives; the polynomial case is left unevaluated for now even though this
library computes polynomial gcds elsewhere.

`trunc`, `lcm`, `erf` and `conjugate` are still refused by name.

Issue [#809](https://github.com/asc-community/AngouriMath/issues/809).

### `floor` and `ceil` exist, so they no longer raise

```csharp
Expand Down
2 changes: 1 addition & 1 deletion Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed

# A new file in a directory whose other files predate it, so the section is on the file
# rather than the folder.
[AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs]
[AngouriMath/Core/Entity/Continuous/Entity.Continuous.{Floors,Rounding}.Classes.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
66 changes: 66 additions & 0 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,72 @@ public static Integer GreatestCommonDivisor(Integer a, Integer b)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Ceil(Entity a) => new Ceilf(a);

/// <summary>
/// The nearest integer to <paramref name="a"/>, a tie going to the even one.
/// </summary>
/// <param name="a">The argument to round.</param>
/// <returns>The rounded argument.</returns>
/// <remarks>
/// Half to even, as Python, SymPy, Mathematica and IEEE 754 all round: <c>round(1/2)</c>
/// is <c>0</c> and <c>round(3/2)</c> is <c>2</c>. It is therefore <b>not</b>
/// <c>floor(x + 1/2)</c>, which differs at every tie.
/// </remarks>
/// <example>
/// <code>
/// using System;
/// using static AngouriMath.MathS;
///
/// Console.WriteLine(Round("1/2").Simplify());
/// Console.WriteLine(Round("3/2").Simplify());
/// Console.WriteLine(Round("5/2").Simplify());
/// </code>
/// Prints
/// <code>
/// 0
/// 2
/// 2
/// </code>
/// </example>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Round(Entity a) => new Roundf(a);

/// <summary>
/// The lesser of <paramref name="a"/> and <paramref name="b"/>.
/// </summary>
/// <param name="a">The first argument.</param>
/// <param name="b">The second argument.</param>
/// <returns>Whichever is smaller, or the unevaluated node where they cannot be compared.</returns>
/// <remarks>
/// Only ordered arguments compare, so a complex one is left alone rather than
/// guessed at. For more than two, nest: <c>Min(Min(a, b), c)</c>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Min(Entity a, Entity b) => new Minf(a, b);

/// <summary>
/// The greater of <paramref name="a"/> and <paramref name="b"/>.
/// </summary>
/// <param name="a">The first argument.</param>
/// <param name="b">The second argument.</param>
/// <returns>Whichever is larger, or the unevaluated node where they cannot be compared.</returns>
/// <remarks>See <see cref="Min(Entity, Entity)"/>.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Max(Entity a, Entity b) => new Maxf(a, b);

/// <summary>
/// The greatest common divisor of <paramref name="a"/> and <paramref name="b"/>.
/// </summary>
/// <param name="a">The first argument.</param>
/// <param name="b">The second argument.</param>
/// <returns>The gcd where it can be computed, the unevaluated node otherwise.</returns>
/// <remarks>
/// Integers and rationals are computed — <c>gcd(1/2, 1/3)</c> is <c>1/6</c>, as
/// SymPy gives. The polynomial case is left unevaluated for now, though this library
/// does compute polynomial gcds elsewhere.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Gcd(Entity a, Entity b) => new Gcdf(a, b);

/// <summary>Boolean negation
/// <a href="https://en.wikipedia.org/wiki/Negation">Wikipedia</a></summary>
/// <param name="a">Argument node of which Negation function will be taken</param>
Expand Down
10 changes: 6 additions & 4 deletions Sources/AngouriMath/Core/Antlr/AngouriMath.g
Original file line number Diff line number Diff line change
Expand Up @@ -412,17 +412,19 @@ atom returns[Entity value]
/* SymPy's spelling, accepted so that an expression copied from there parses. Stringize
prints the short form, which is what the round-trip test pins. */
| 'ceiling(' args = function_arguments ')' { Assert("ceiling", 1, $args.list.Count); $value = MathS.Ceil($args.list[0]); }
| 'round(' args = function_arguments ')' { Assert("round", 1, $args.list.Count); $value = MathS.Round($args.list[0]); }
/* min and max take any number of arguments, as they do everywhere else, and fold left
into the binary node. One argument is that argument. */
| 'min(' args = function_arguments ')' { AssertAtLeast("min", 1, $args.list.Count); $value = $args.list.Aggregate((a, b) => MathS.Min(a, b)); }
| 'max(' args = function_arguments ')' { AssertAtLeast("max", 1, $args.list.Count); $value = $args.list.Aggregate((a, b) => MathS.Max(a, b)); }
| 'gcd(' args = function_arguments ')' { AssertAtLeast("gcd", 1, $args.list.Count); $value = $args.list.Aggregate((a, b) => MathS.Gcd(a, b)); }

/* Names the library does not have. Each is a function every other CAS spells this way, so
a caller reaches for it, and without these rules each is silently read as a product --
see NotImplementedFunction above. Refusing is not the feature; it is the difference
between a missing function and a wrong answer. */

| 'round(' args = function_arguments ')' { $value = NotImplementedFunction("round", "rounding functions"); }
| 'trunc(' args = function_arguments ')' { $value = NotImplementedFunction("trunc", "rounding functions"); }
| 'min(' args = function_arguments ')' { $value = NotImplementedFunction("min", "minimum or maximum function"); }
| 'max(' args = function_arguments ')' { $value = NotImplementedFunction("max", "minimum or maximum function"); }
| 'gcd(' args = function_arguments ')' { $value = NotImplementedFunction("gcd", "greatest common divisor as a symbolic function"); }
| 'lcm(' args = function_arguments ')' { $value = NotImplementedFunction("lcm", "least common multiple as a symbolic function"); }
| 'erf(' args = function_arguments ')' { $value = NotImplementedFunction("erf", "error function"); }
| 'conjugate(' args = function_arguments ')' { $value = NotImplementedFunction("conjugate", "complex conjugate as a symbolic function"); }
Expand Down
2 changes: 1 addition & 1 deletion Sources/AngouriMath/Core/Antlr/AngouriMath.interp
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ null
'ceil('
'ceiling('
'round('
'trunc('
'min('
'max('
'gcd('
'trunc('
'lcm('
'erf('
'conjugate('
Expand Down
8 changes: 4 additions & 4 deletions Sources/AngouriMath/Core/Antlr/AngouriMath.tokens
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,10 @@ WS=150
'ceil('=130
'ceiling('=131
'round('=132
'trunc('=133
'min('=134
'max('=135
'gcd('=136
'min('=133
'max('=134
'gcd('=135
'trunc('=136
'lcm('=137
'erf('=138
'conjugate('=139
Expand Down
24 changes: 12 additions & 12 deletions Sources/AngouriMath/Core/Antlr/AngouriMathLexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public AngouriMathLexer(ICharStream input, TextWriter output, TextWriter errorOu
"'arcsch('", "'arccosech('", "'acsch('", "'factorial('", "'gamma('", "'derivative('",
"'integral('", "'limit('", "'limitleft('", "'limitright('", "'signum('",
"'sgn('", "'sign('", "'abs('", "'phi('", "'floor('", "'ceil('", "'ceiling('",
"'round('", "'trunc('", "'min('", "'max('", "'gcd('", "'lcm('", "'erf('",
"'round('", "'min('", "'max('", "'gcd('", "'trunc('", "'lcm('", "'erf('",
"'conjugate('", "'domain('", "'piecewise('", "'apply('", "'lambda('"
};
private static readonly string[] _SymbolicNames = {
Expand Down Expand Up @@ -252,8 +252,8 @@ static AngouriMathLexer() {
1,126,1,127,1,127,1,127,1,127,1,127,1,128,1,128,1,128,1,128,1,128,1,128,
1,128,1,129,1,129,1,129,1,129,1,129,1,129,1,130,1,130,1,130,1,130,1,130,
1,130,1,130,1,130,1,130,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,132,
1,132,1,132,1,132,1,132,1,132,1,132,1,133,1,133,1,133,1,133,1,133,1,134,
1,134,1,134,1,134,1,134,1,135,1,135,1,135,1,135,1,135,1,136,1,136,1,136,
1,132,1,132,1,132,1,132,1,133,1,133,1,133,1,133,1,133,1,134,1,134,1,134,
1,134,1,134,1,135,1,135,1,135,1,135,1,135,1,135,1,135,1,136,1,136,1,136,
1,136,1,136,1,137,1,137,1,137,1,137,1,137,1,138,1,138,1,138,1,138,1,138,
1,138,1,138,1,138,1,138,1,138,1,138,1,139,1,139,1,139,1,139,1,139,1,139,
1,139,1,139,1,140,1,140,1,140,1,140,1,140,1,140,1,140,1,140,1,140,1,140,
Expand Down Expand Up @@ -347,8 +347,8 @@ static AngouriMathLexer() {
963,1,0,0,0,237,970,1,0,0,0,239,982,1,0,0,0,241,992,1,0,0,0,243,999,1,
0,0,0,245,1010,1,0,0,0,247,1022,1,0,0,0,249,1030,1,0,0,0,251,1035,1,0,
0,0,253,1041,1,0,0,0,255,1046,1,0,0,0,257,1051,1,0,0,0,259,1058,1,0,0,
0,261,1064,1,0,0,0,263,1073,1,0,0,0,265,1080,1,0,0,0,267,1087,1,0,0,0,
269,1092,1,0,0,0,271,1097,1,0,0,0,273,1102,1,0,0,0,275,1107,1,0,0,0,277,
0,261,1064,1,0,0,0,263,1073,1,0,0,0,265,1080,1,0,0,0,267,1085,1,0,0,0,
269,1090,1,0,0,0,271,1095,1,0,0,0,273,1102,1,0,0,0,275,1107,1,0,0,0,277,
1112,1,0,0,0,279,1123,1,0,0,0,281,1131,1,0,0,0,283,1142,1,0,0,0,285,1149,
1,0,0,0,287,1161,1,0,0,0,289,1167,1,0,0,0,291,1209,1,0,0,0,293,1221,1,
0,0,0,295,1241,1,0,0,0,297,1244,1,0,0,0,299,1280,1,0,0,0,301,1285,1,0,
Expand Down Expand Up @@ -542,13 +542,13 @@ static AngouriMathLexer() {
5,105,0,0,1067,1068,5,108,0,0,1068,1069,5,105,0,0,1069,1070,5,110,0,0,
1070,1071,5,103,0,0,1071,1072,5,40,0,0,1072,262,1,0,0,0,1073,1074,5,114,
0,0,1074,1075,5,111,0,0,1075,1076,5,117,0,0,1076,1077,5,110,0,0,1077,1078,
5,100,0,0,1078,1079,5,40,0,0,1079,264,1,0,0,0,1080,1081,5,116,0,0,1081,
1082,5,114,0,0,1082,1083,5,117,0,0,1083,1084,5,110,0,0,1084,1085,5,99,
0,0,1085,1086,5,40,0,0,1086,266,1,0,0,0,1087,1088,5,109,0,0,1088,1089,
5,105,0,0,1089,1090,5,110,0,0,1090,1091,5,40,0,0,1091,268,1,0,0,0,1092,
1093,5,109,0,0,1093,1094,5,97,0,0,1094,1095,5,120,0,0,1095,1096,5,40,0,
0,1096,270,1,0,0,0,1097,1098,5,103,0,0,1098,1099,5,99,0,0,1099,1100,5,
100,0,0,1100,1101,5,40,0,0,1101,272,1,0,0,0,1102,1103,5,108,0,0,1103,1104,
5,100,0,0,1078,1079,5,40,0,0,1079,264,1,0,0,0,1080,1081,5,109,0,0,1081,
1082,5,105,0,0,1082,1083,5,110,0,0,1083,1084,5,40,0,0,1084,266,1,0,0,0,
1085,1086,5,109,0,0,1086,1087,5,97,0,0,1087,1088,5,120,0,0,1088,1089,5,
40,0,0,1089,268,1,0,0,0,1090,1091,5,103,0,0,1091,1092,5,99,0,0,1092,1093,
5,100,0,0,1093,1094,5,40,0,0,1094,270,1,0,0,0,1095,1096,5,116,0,0,1096,
1097,5,114,0,0,1097,1098,5,117,0,0,1098,1099,5,110,0,0,1099,1100,5,99,
0,0,1100,1101,5,40,0,0,1101,272,1,0,0,0,1102,1103,5,108,0,0,1103,1104,
5,99,0,0,1104,1105,5,109,0,0,1105,1106,5,40,0,0,1106,274,1,0,0,0,1107,
1108,5,101,0,0,1108,1109,5,114,0,0,1109,1110,5,102,0,0,1110,1111,5,40,
0,0,1111,276,1,0,0,0,1112,1113,5,99,0,0,1113,1114,5,111,0,0,1114,1115,
Expand Down
4 changes: 2 additions & 2 deletions Sources/AngouriMath/Core/Antlr/AngouriMathLexer.interp

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Sources/AngouriMath/Core/Antlr/AngouriMathLexer.tokens
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,10 @@ WS=150
'ceil('=130
'ceiling('=131
'round('=132
'trunc('=133
'min('=134
'max('=135
'gcd('=136
'min('=133
'max('=134
'gcd('=135
'trunc('=136
'lcm('=137
'erf('=138
'conjugate('=139
Expand Down
12 changes: 6 additions & 6 deletions Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public const int
"'arcsch('", "'arccosech('", "'acsch('", "'factorial('", "'gamma('", "'derivative('",
"'integral('", "'limit('", "'limitleft('", "'limitright('", "'signum('",
"'sgn('", "'sign('", "'abs('", "'phi('", "'floor('", "'ceil('", "'ceiling('",
"'round('", "'trunc('", "'min('", "'max('", "'gcd('", "'lcm('", "'erf('",
"'round('", "'min('", "'max('", "'gcd('", "'trunc('", "'lcm('", "'erf('",
"'conjugate('", "'domain('", "'piecewise('", "'apply('", "'lambda('"
};
private static readonly string[] _SymbolicNames = {
Expand Down Expand Up @@ -3100,7 +3100,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 821;
Match(T__40);
_localctx.value = NotImplementedFunction("round", "rounding functions");
Assert("round", 1, _localctx.args.list.Count); _localctx.value = MathS.Round(_localctx.args.list[0]);
}
break;
case 106:
Expand All @@ -3112,7 +3112,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 826;
Match(T__40);
_localctx.value = NotImplementedFunction("trunc", "rounding functions");
AssertAtLeast("min", 1, _localctx.args.list.Count); _localctx.value = _localctx.args.list.Aggregate((a, b) => MathS.Min(a, b));
}
break;
case 107:
Expand All @@ -3124,7 +3124,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 831;
Match(T__40);
_localctx.value = NotImplementedFunction("min", "minimum or maximum function");
AssertAtLeast("max", 1, _localctx.args.list.Count); _localctx.value = _localctx.args.list.Aggregate((a, b) => MathS.Max(a, b));
}
break;
case 108:
Expand All @@ -3136,7 +3136,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 836;
Match(T__40);
_localctx.value = NotImplementedFunction("max", "minimum or maximum function");
AssertAtLeast("gcd", 1, _localctx.args.list.Count); _localctx.value = _localctx.args.list.Aggregate((a, b) => MathS.Gcd(a, b));
}
break;
case 109:
Expand All @@ -3148,7 +3148,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 841;
Match(T__40);
_localctx.value = NotImplementedFunction("gcd", "greatest common divisor as a symbolic function");
_localctx.value = NotImplementedFunction("trunc", "rounding functions");
}
break;
case 110:
Expand Down
25 changes: 25 additions & 0 deletions Sources/AngouriMath/Core/Domains.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,31 @@ partial record Ceilf
public override Domain Codomain { get; protected init; } = Domain.Complex;
}

partial record Roundf
{
/// <inheritdoc/>
public override Domain Codomain { get; protected init; } = Domain.Complex;
}

partial record Minf
{
// Only ordered arguments compare, so the value is real wherever there is one.
/// <inheritdoc/>
public override Domain Codomain { get; protected init; } = Domain.Real;
}

partial record Maxf
{
/// <inheritdoc/>
public override Domain Codomain { get; protected init; } = Domain.Real;
}

partial record Gcdf
{
/// <inheritdoc/>
public override Domain Codomain { get; protected init; } = Domain.Complex;
}

partial record Boolean
{
/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,13 @@ public abstract partial record CalculusOperator(Entity Expression, Entity Var) :
public Entity Floor() => new Floorf(this);
/// <summary><see cref="MathS.Ceil(Entity)"/></summary>
public Entity Ceil() => new Ceilf(this);
/// <summary><see cref="MathS.Round(Entity)"/></summary>
public Entity Round() => new Roundf(this);
/// <summary><see cref="MathS.Min(Entity, Entity)"/></summary>
public Entity Min(Entity another) => new Minf(this, another);
/// <summary><see cref="MathS.Max(Entity, Entity)"/></summary>
public Entity Max(Entity another) => new Maxf(this, another);
/// <summary><see cref="MathS.Gcd(Entity, Entity)"/></summary>
public Entity Gcd(Entity another) => new Gcdf(this, another);
}
}
Loading
Loading