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
28 changes: 28 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ read first.
| loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` |
| 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 | 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 @@ -1290,6 +1291,33 @@ throw. `Simplify` was never affected — it answered `1 + x` throughout.

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

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

```csharp
"floor(x)".ToEntity();
// was UnrecognizedFunctionParseException: floor is not a function this library has
// is floor(x)

"floor(x) - 3 = 0".ToEntity().Solve("x");
// was UnrecognizedFunctionParseException
// is { 3 + t_1 provided t_1 in RR and t_1 >= 0 and t_1 < 1 }
```

`ceiling(` is accepted as well, since that is SymPy's spelling; `Stringize` prints the short
`ceil(`. Both round toward the infinities rather than toward zero — `floor(-3/2)` is `-2` — and both
are taken componentwise on a complex argument, which is what SymPy and Mathematica do. Every value
in the test file was measured against SymPy 1.14 rather than reasoned about.

**If you were catching `UnrecognizedFunctionParseException` around these names, that is now dead
code.** Nothing else moves: `round`, `trunc`, `min`, `max`, `gcd` and `lcm` are still refused by
name, and `floor` on its own — without a bracket — is still an ordinary variable.

The derivative is `0 provided not x in ZZ`: flat between the integers, undefined at each of them.
SymPy leaves that derivative unevaluated; this library states the condition instead, which is the
stance `Signumf` and `Absf` already take.

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

### A parse failure no longer escapes as a `NullReferenceException`

```csharp
Expand Down
5 changes: 5 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed

[Tests/UnitTests/Core/Transformations/*.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

# 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]
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
57 changes: 57 additions & 0 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,63 @@ public static Integer GreatestCommonDivisor(Integer a, Integer b)
[MethodImpl(MethodImplOptions.AggressiveInlining), NativeExport]
public static Entity Abs(Entity a) => new Absf(a);

/// <summary>
/// The greatest integer not above <paramref name="a"/>.
/// </summary>
/// <param name="a">The argument to take the floor of.</param>
/// <returns>The floor of the argument.</returns>
/// <remarks>
/// Toward negative infinity, not toward zero: <c>floor(-3/2)</c> is <c>-2</c>. On a
/// complex argument it is taken componentwise, which is what SymPy and Mathematica
/// both do.
/// </remarks>
/// <example>
/// <code>
/// using System;
/// using static AngouriMath.MathS;
///
/// Console.WriteLine(Floor("3/2").Simplify());
/// Console.WriteLine(Floor("-3/2").Simplify());
/// Console.WriteLine(Floor("x"));
/// </code>
/// Prints
/// <code>
/// 1
/// -2
/// floor(x)
/// </code>
/// </example>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Floor(Entity a) => new Floorf(a);

/// <summary>
/// The least integer not below <paramref name="a"/>.
/// </summary>
/// <param name="a">The argument to take the ceiling of.</param>
/// <returns>The ceiling of the argument.</returns>
/// <remarks>
/// Toward positive infinity, not away from zero: <c>ceil(-3/2)</c> is <c>-1</c>.
/// Componentwise on a complex argument, as <see cref="Floor(Entity)"/> is.
/// </remarks>
/// <example>
/// <code>
/// using System;
/// using static AngouriMath.MathS;
///
/// Console.WriteLine(Ceil("3/2").Simplify());
/// Console.WriteLine(Ceil("-3/2").Simplify());
/// Console.WriteLine(Ceil("x"));
/// </code>
/// Prints
/// <code>
/// 2
/// -1
/// ceil(x)
/// </code>
/// </example>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Entity Ceil(Entity a) => new Ceilf(a);

/// <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
8 changes: 5 additions & 3 deletions Sources/AngouriMath/Core/Antlr/AngouriMath.g
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,17 @@ atom returns[Entity value]
| 'sign(' args = function_arguments ')' { Assert("sign", 1, $args.list.Count); $value = MathS.Signum($args.list[0]); }
| 'abs(' args = function_arguments ')' { Assert("abs", 1, $args.list.Count); $value = MathS.Abs($args.list[0]); }
| 'phi(' args = function_arguments ')' { Assert("phi", 1, $args.list.Count); $value = MathS.NumberTheory.Phi($args.list[0]); }
| 'floor(' args = function_arguments ')' { Assert("floor", 1, $args.list.Count); $value = MathS.Floor($args.list[0]); }
| 'ceil(' args = function_arguments ')' { Assert("ceil", 1, $args.list.Count); $value = MathS.Ceil($args.list[0]); }
/* 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]); }

/* 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. */

| 'floor(' args = function_arguments ')' { $value = NotImplementedFunction("floor", "rounding functions"); }
| 'ceil(' args = function_arguments ')' { $value = NotImplementedFunction("ceil", "rounding functions"); }
| 'ceiling(' args = function_arguments ')' { $value = NotImplementedFunction("ceiling", "rounding functions"); }
| '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"); }
Expand Down
6 changes: 3 additions & 3 deletions Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3064,7 +3064,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 806;
Match(T__40);
_localctx.value = NotImplementedFunction("floor", "rounding functions");
Assert("floor", 1, _localctx.args.list.Count); _localctx.value = MathS.Floor(_localctx.args.list[0]);
}
break;
case 103:
Expand All @@ -3076,7 +3076,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 811;
Match(T__40);
_localctx.value = NotImplementedFunction("ceil", "rounding functions");
Assert("ceil", 1, _localctx.args.list.Count); _localctx.value = MathS.Ceil(_localctx.args.list[0]);
}
break;
case 104:
Expand All @@ -3088,7 +3088,7 @@ public AtomContext atom() {
_localctx.args = function_arguments();
State = 816;
Match(T__40);
_localctx.value = NotImplementedFunction("ceiling", "rounding functions");
Assert("ceiling", 1, _localctx.args.list.Count); _localctx.value = MathS.Ceil(_localctx.args.list[0]);
}
break;
case 105:
Expand Down
15 changes: 15 additions & 0 deletions Sources/AngouriMath/Core/Domains.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ partial record Absf
public override Domain Codomain { get; protected init; } = Domain.Real;
}

partial record Floorf
{
// Complex rather than Integer: taken componentwise, floor of a complex number
// is a Gaussian integer, and there is no domain for those. The codomain of the
// real case is stated by the value, not by this.
/// <inheritdoc/>
public override Domain Codomain { get; protected init; } = Domain.Complex;
}

partial record Ceilf
{
/// <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 @@ -127,5 +127,9 @@ public abstract partial record CalculusOperator(Entity Expression, Entity Var) :
public Entity Signum() => new Signumf(this);
/// <summary><see cref="MathS.Abs(Entity)"/></summary>
public Entity Abs() => new Absf(this);
/// <summary><see cref="MathS.Floor(Entity)"/></summary>
public Entity Floor() => new Floorf(this);
/// <summary><see cref="MathS.Ceil(Entity)"/></summary>
public Entity Ceil() => new Ceilf(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// 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 System;

namespace AngouriMath
{
partial record Entity
{
#pragma warning disable CS1591 // only while records' parameters cannot be documented
/// <summary>
/// A node of floor: the greatest integer not above the argument.
/// </summary>
/// <remarks>
/// On a complex argument it is taken componentwise, so that
/// <c>floor(3/2 + 5/2i)</c> is <c>1 + 2i</c>. That is what SymPy and Mathematica
/// both do, and it is the only reading under which <c>floor</c> of a real number
/// keeps its meaning when the imaginary part happens to be zero.
/// </remarks>
public sealed partial record Floorf(Entity Argument) : Function, IUnaryNode
{
public Entity NodeChild => Argument;

private Floorf New(Entity arg) =>
ReferenceEquals(Argument, arg) ? this : new(arg);
/// <inheritdoc/>
public override Entity Replace(Func<Entity, Entity> func) => func(New(Argument.Replace(func)));
/// <inheritdoc/>
protected override Entity[] InitDirectChildren() => new[] { Argument };
}

/// <summary>
/// A node of ceil: the least integer not below the argument.
/// </summary>
/// <remarks>
/// Componentwise on a complex argument, for the same reason as
/// <see cref="Floorf"/>.
/// </remarks>
public sealed partial record Ceilf(Entity Argument) : Function, IUnaryNode
{
public Entity NodeChild => Argument;

private Ceilf New(Entity arg) =>
ReferenceEquals(Argument, arg) ? this : new(arg);
/// <inheritdoc/>
public override Entity Replace(Func<Entity, Entity> func) => func(New(Argument.Replace(func)));
/// <inheritdoc/>
protected override Entity[] InitDirectChildren() => new[] { Argument };
}
#pragma warning restore CS1591 // only while records' parameters cannot be documented
}
}
19 changes: 19 additions & 0 deletions Sources/AngouriMath/Functions/Continuous/Differentiation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,25 @@ protected override Entity InnerDifferentiate(Variable variable)
=> MathS.Signum(Argument).Provided(!Argument.EqualTo(Integer.Zero)) * Argument.InnerDifferentiate(variable);
}

partial record Floorf
{
// Flat between consecutive integers and discontinuous at each of them, so the
// derivative is 0 wherever it exists and nowhere at an integer argument. Saying
// that with a condition is the stance Signumf and Absf take above; SymPy
// instead leaves the derivative unevaluated, which says less, and this library
// has Providedf to say more with.
/// <inheritdoc/>
protected override Entity InnerDifferentiate(Variable variable)
=> Integer.Zero.Provided(!Argument.In(MathS.Sets.Z));
}

partial record Ceilf
{
/// <inheritdoc/>
protected override Entity InnerDifferentiate(Variable variable)
=> Integer.Zero.Provided(!Argument.In(MathS.Sets.Z));
}

partial record Providedf
{
/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,42 @@ private protected override IEnumerable<Entity> InvertNode(Entity value, Entity x
}
}

partial record Floorf
{
// floor(f(x)) = value
// solvable only where value is an integer, and then f(x) is anywhere in the
// half-open interval [value, value + 1) -- so the preimage is a parameter t in
// [0, 1) rather than a point, which is the same device Signumf and Absf use for
// their own many-to-one inverses.
private protected override IEnumerable<Entity> InvertNode(Entity value, Entity x)
{
var t = Variable.CreateUnique(value + Argument, "t");
return Argument.Invert(value + t, x)
.Select(c => c.Provided(
value.In(MathS.Sets.Z)
& t.In(MathS.Sets.R)
& new GreaterOrEqualf(t, 0)
& new Lessf(t, 1)));
}
}

partial record Ceilf
{
// ceil(f(x)) = value
// likewise integer-valued, with f(x) in (value - 1, value] -- so the parameter
// is subtracted rather than added.
private protected override IEnumerable<Entity> InvertNode(Entity value, Entity x)
{
var t = Variable.CreateUnique(value + Argument, "t");
return Argument.Invert(value - t, x)
.Select(c => c.Provided(
value.In(MathS.Sets.Z)
& t.In(MathS.Sets.R)
& new GreaterOrEqualf(t, 0)
& new Lessf(t, 1)));
}
}

partial record Boolean
{
private protected override IEnumerable<Entity> InvertNode(Entity value, Entity x)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,51 @@ protected override Entity InnerSimplify(bool isExact)
(@this, a) => ((Signumf)@this).New(a), isExact);
}

public partial record Floorf
{
// Defined everywhere in the complex plane, taken componentwise.
private protected override Entity IntrinsicCondition => Boolean.True;

/// <inheritdoc/>
protected override Entity InnerSimplify(bool isExact)
=> ExpandOnOneArgument(Argument,
a => a switch
{
// An integer is already its own floor, and it stays exact.
Integer n => n,
Rational n => Integer.Create(n.EDecimal.Floor().ToEInteger()),
Real n when !isExact => Integer.Create(n.EDecimal.Floor().ToEInteger()),
Complex n when !isExact => Complex.Create(
n.RealPart.EDecimal.Floor(), n.ImaginaryPart.EDecimal.Floor()),
// Idempotent: the floor of an integer is that integer, and floor
// always produces one.
Floorf or Ceilf => a,
_ => null
},
(@this, a) => ((Floorf)@this).New(a), isExact);
}

public partial record Ceilf
{
// Defined everywhere in the complex plane, taken componentwise.
private protected override Entity IntrinsicCondition => Boolean.True;

/// <inheritdoc/>
protected override Entity InnerSimplify(bool isExact)
=> ExpandOnOneArgument(Argument,
a => a switch
{
Integer n => n,
Rational n => Integer.Create(n.EDecimal.Ceiling().ToEInteger()),
Real n when !isExact => Integer.Create(n.EDecimal.Ceiling().ToEInteger()),
Complex n when !isExact => Complex.Create(
n.RealPart.EDecimal.Ceiling(), n.ImaginaryPart.EDecimal.Ceiling()),
Floorf or Ceilf => a,
_ => null
},
(@this, a) => ((Ceilf)@this).New(a), isExact);
}

public partial record Absf
{
// Absolute value is defined everywhere in the complex plane
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ public override string Latexise()
=> $@"\left|{Argument.Latexise()}\right|";
}

partial record Floorf
{
/// <inheritdoc/>
public override string Latexise()
=> $@"\left\lfloor{{{Argument.Latexise()}}}\right\rfloor";
}

partial record Ceilf
{
/// <inheritdoc/>
public override string Latexise()
=> $@"\left\lceil{{{Argument.Latexise()}}}\right\rceil";
}

partial record Phif
{
/// <inheritdoc/>
Expand Down
Loading
Loading