diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 4b216b673..bc986a28c 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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 |
@@ -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
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 876c81824..849f1b983 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -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
diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs
index 2a6d73ed2..490f578f1 100644
--- a/Sources/AngouriMath/Convenience/MathS.cs
+++ b/Sources/AngouriMath/Convenience/MathS.cs
@@ -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);
+ ///
+ /// The greatest integer not above .
+ ///
+ /// The argument to take the floor of.
+ /// The floor of the argument.
+ ///
+ /// Toward negative infinity, not toward zero: floor(-3/2) is -2. On a
+ /// complex argument it is taken componentwise, which is what SymPy and Mathematica
+ /// both do.
+ ///
+ ///
+ ///
+ /// using System;
+ /// using static AngouriMath.MathS;
+ ///
+ /// Console.WriteLine(Floor("3/2").Simplify());
+ /// Console.WriteLine(Floor("-3/2").Simplify());
+ /// Console.WriteLine(Floor("x"));
+ ///
+ /// Prints
+ ///
+ /// 1
+ /// -2
+ /// floor(x)
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Entity Floor(Entity a) => new Floorf(a);
+
+ ///
+ /// The least integer not below .
+ ///
+ /// The argument to take the ceiling of.
+ /// The ceiling of the argument.
+ ///
+ /// Toward positive infinity, not away from zero: ceil(-3/2) is -1.
+ /// Componentwise on a complex argument, as is.
+ ///
+ ///
+ ///
+ /// using System;
+ /// using static AngouriMath.MathS;
+ ///
+ /// Console.WriteLine(Ceil("3/2").Simplify());
+ /// Console.WriteLine(Ceil("-3/2").Simplify());
+ /// Console.WriteLine(Ceil("x"));
+ ///
+ /// Prints
+ ///
+ /// 2
+ /// -1
+ /// ceil(x)
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Entity Ceil(Entity a) => new Ceilf(a);
+
/// Boolean negation
/// Wikipedia
/// Argument node of which Negation function will be taken
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMath.g b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
index 4e9d1034d..14f86da08 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMath.g
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
@@ -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"); }
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
index 6fb5f8e8c..23ea8b798 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
@@ -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:
@@ -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:
@@ -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:
diff --git a/Sources/AngouriMath/Core/Domains.Classes.cs b/Sources/AngouriMath/Core/Domains.Classes.cs
index f3cd6f220..982f4bbf7 100644
--- a/Sources/AngouriMath/Core/Domains.Classes.cs
+++ b/Sources/AngouriMath/Core/Domains.Classes.cs
@@ -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.
+ ///
+ public override Domain Codomain { get; protected init; } = Domain.Complex;
+ }
+
+ partial record Ceilf
+ {
+ ///
+ public override Domain Codomain { get; protected init; } = Domain.Complex;
+ }
+
partial record Boolean
{
///
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
index 55f1cfe88..3e58b7d80 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
@@ -127,5 +127,9 @@ public abstract partial record CalculusOperator(Entity Expression, Entity Var) :
public Entity Signum() => new Signumf(this);
///
public Entity Abs() => new Absf(this);
+ ///
+ public Entity Floor() => new Floorf(this);
+ ///
+ public Entity Ceil() => new Ceilf(this);
}
}
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs
new file mode 100644
index 000000000..05f94f543
--- /dev/null
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs
@@ -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
+ ///
+ /// A node of floor: the greatest integer not above the argument.
+ ///
+ ///
+ /// On a complex argument it is taken componentwise, so that
+ /// floor(3/2 + 5/2i) is 1 + 2i. That is what SymPy and Mathematica
+ /// both do, and it is the only reading under which floor of a real number
+ /// keeps its meaning when the imaginary part happens to be zero.
+ ///
+ public sealed partial record Floorf(Entity Argument) : Function, IUnaryNode
+ {
+ public Entity NodeChild => Argument;
+
+ private Floorf New(Entity arg) =>
+ ReferenceEquals(Argument, arg) ? this : new(arg);
+ ///
+ public override Entity Replace(Func func) => func(New(Argument.Replace(func)));
+ ///
+ protected override Entity[] InitDirectChildren() => new[] { Argument };
+ }
+
+ ///
+ /// A node of ceil: the least integer not below the argument.
+ ///
+ ///
+ /// Componentwise on a complex argument, for the same reason as
+ /// .
+ ///
+ public sealed partial record Ceilf(Entity Argument) : Function, IUnaryNode
+ {
+ public Entity NodeChild => Argument;
+
+ private Ceilf New(Entity arg) =>
+ ReferenceEquals(Argument, arg) ? this : new(arg);
+ ///
+ public override Entity Replace(Func func) => func(New(Argument.Replace(func)));
+ ///
+ protected override Entity[] InitDirectChildren() => new[] { Argument };
+ }
+#pragma warning restore CS1591 // only while records' parameters cannot be documented
+ }
+}
diff --git a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
index bef6ba8df..f8a465ec5 100644
--- a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
@@ -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.
+ ///
+ protected override Entity InnerDifferentiate(Variable variable)
+ => Integer.Zero.Provided(!Argument.In(MathS.Sets.Z));
+ }
+
+ partial record Ceilf
+ {
+ ///
+ protected override Entity InnerDifferentiate(Variable variable)
+ => Integer.Zero.Provided(!Argument.In(MathS.Sets.Z));
+ }
+
partial record Providedf
{
///
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
index bee882dda..044692da2 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs
@@ -273,6 +273,42 @@ private protected override IEnumerable 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 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 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 InvertNode(Entity value, Entity x)
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
index 4e8d8c827..1cdfb8308 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
@@ -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;
+
+ ///
+ 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;
+
+ ///
+ 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
diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
index 47f354f20..1a3348501 100644
--- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs
@@ -158,6 +158,20 @@ public override string Latexise()
=> $@"\left|{Argument.Latexise()}\right|";
}
+ partial record Floorf
+ {
+ ///
+ public override string Latexise()
+ => $@"\left\lfloor{{{Argument.Latexise()}}}\right\rfloor";
+ }
+
+ partial record Ceilf
+ {
+ ///
+ public override string Latexise()
+ => $@"\left\lceil{{{Argument.Latexise()}}}\right\rceil";
+ }
+
partial record Phif
{
///
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
index d9211893b..2c29467f5 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
@@ -109,6 +109,22 @@ public partial record Absf
public override string ToString() => Stringize();
}
+ public partial record Floorf
+ {
+ ///
+ public override string Stringize() => $"floor({Argument.Stringize()})";
+ ///
+ public override string ToString() => Stringize();
+ }
+
+ public partial record Ceilf
+ {
+ ///
+ public override string Stringize() => $"ceil({Argument.Stringize()})";
+ ///
+ public override string ToString() => Stringize();
+ }
+
public partial record Factorialf
{
///
diff --git a/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs
index 7079c1544..1bbc32fec 100644
--- a/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs
@@ -64,6 +64,19 @@ internal override string ToSymPy()
=> $@"sympy.Abs({Argument.ToSymPy()})";
}
+ public partial record Floorf
+ {
+ internal override string ToSymPy()
+ => $@"sympy.floor({Argument.ToSymPy()})";
+ }
+
+ public partial record Ceilf
+ {
+ // SymPy spells it in full; `ceil` is not a name it has.
+ internal override string ToSymPy()
+ => $@"sympy.ceiling({Argument.ToSymPy()})";
+ }
+
public partial record Phif
{
internal override string ToSymPy() => $"sympy.totient({Argument.ToSymPy()})";
diff --git a/Sources/AngouriMath/Functions/Substitute.cs b/Sources/AngouriMath/Functions/Substitute.cs
index 7d9ed6c48..1627407a3 100644
--- a/Sources/AngouriMath/Functions/Substitute.cs
+++ b/Sources/AngouriMath/Functions/Substitute.cs
@@ -172,6 +172,20 @@ public override Entity Substitute(Entity x, Entity value)
=> this == x ? value : New(Argument.Substitute(x, value));
}
+ partial record Floorf
+ {
+ ///
+ public override Entity Substitute(Entity x, Entity value)
+ => this == x ? value : New(Argument.Substitute(x, value));
+ }
+
+ partial record Ceilf
+ {
+ ///
+ public override Entity Substitute(Entity x, Entity value)
+ => this == x ? value : New(Argument.Substitute(x, value));
+ }
+
partial record Boolean
{
///
diff --git a/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs b/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
index 9e682a28c..d6eddeae8 100644
--- a/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
+++ b/Sources/AngouriMath/Functions/TreeAnalyzer/Sort.Classes.cs
@@ -203,6 +203,18 @@ private protected override string SortHashName(SortLevel level)
=> Choice(level, "sgnabs_", "abs_", "abs_");
}
+ public partial record Floorf
+ {
+ private protected override string SortHashName(SortLevel level)
+ => Choice(level, "floorceil_", "floor_", "floor_");
+ }
+
+ public partial record Ceilf
+ {
+ private protected override string SortHashName(SortLevel level)
+ => Choice(level, "floorceil_", "ceil_", "ceil_");
+ }
+
partial record Boolean
{
private protected override string SortHashName(SortLevel level)
diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
new file mode 100644
index 000000000..c2c5ccf94
--- /dev/null
+++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
@@ -0,0 +1,136 @@
+//
+// 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.Linq;
+using AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Convenience
+{
+ ///
+ /// floor and ceil — #809.
+ ///
+ ///
+ /// The expected values are SymPy 1.14's, measured rather than reasoned about, as
+ /// AGENTS.md asks when a convention has to be chosen. The ones that matter are the
+ /// negative arguments — rounding toward the infinities rather than toward zero — and the
+ /// complex case, which both SymPy and Mathematica take componentwise.
+ ///
+ [Trait("Area", "Convenience")]
+ public sealed class FloorCeilTest
+ {
+ [Theory]
+ [InlineData("floor(3/2)", 1)]
+ [InlineData("floor(-3/2)", -2)]
+ [InlineData("floor(-1/2)", -1)]
+ [InlineData("floor(5/2)", 2)]
+ [InlineData("floor(-5/2)", -3)]
+ [InlineData("floor(2)", 2)]
+ [InlineData("floor(-2)", -2)]
+ [InlineData("ceil(3/2)", 2)]
+ [InlineData("ceil(-3/2)", -1)]
+ [InlineData("ceil(-1/2)", 0)]
+ [InlineData("ceil(5/2)", 3)]
+ [InlineData("ceil(-5/2)", -2)]
+ [InlineData("ceil(2)", 2)]
+ [InlineData("ceil(-2)", -2)]
+ public void TheValueIsWhatSymPyGives(string input, int expected)
+ => Assert.Equal(Entity.Number.Integer.Create(expected), input.ToEntity().Simplify());
+
+ // Componentwise, so that floor of a real keeps its meaning when the imaginary part
+ // happens to be zero.
+ [Theory]
+ [InlineData("floor(3/2 + 5/2 * i)", "1 + 2i")]
+ [InlineData("ceil(3/2 + 5/2 * i)", "2 + 3i")]
+ [InlineData("floor(-3/2 - 5/2 * i)", "-2 - 3i")]
+ [InlineData("ceil(-3/2 - 5/2 * i)", "-1 - 2i")]
+ public void AComplexArgumentIsTakenComponentwise(string input, string expected)
+ => Assert.Equal(expected.ToEntity().EvalNumerical(), input.ToEntity().EvalNumerical());
+
+ // Both produce an integer, and both leave one alone.
+ [Theory]
+ [InlineData("floor(floor(x))", "floor(x)")]
+ [InlineData("ceil(ceil(x))", "ceil(x)")]
+ [InlineData("floor(ceil(x))", "ceil(x)")]
+ [InlineData("ceil(floor(x))", "floor(x)")]
+ public void ApplyingItTwiceChangesNothing(string input, string expected)
+ => Assert.Equal(expected.ToEntity(), input.ToEntity().Simplify());
+
+ [Theory]
+ [InlineData("floor(x)")]
+ [InlineData("ceil(x)")]
+ [InlineData("floor(x + y)")]
+ [InlineData("ceil(sin(x))")]
+ public void ASymbolicArgumentIsLeftAlone(string input)
+ => Assert.Equal(input.ToEntity(), input.ToEntity().Simplify());
+
+ // Flat between the integers and discontinuous at each of them, so the derivative is
+ // zero where it exists and the condition says where that is. SymPy declines to
+ // answer at all here; this library can say more, because it has Providedf.
+ [Theory]
+ [InlineData("floor(x)")]
+ [InlineData("ceil(x)")]
+ public void TheDerivativeIsZeroAwayFromTheIntegers(string input)
+ {
+ var derivative = input.ToEntity().Differentiate("x");
+ var provided = Assert.IsType(derivative);
+ Assert.Equal(Entity.Number.Integer.Create(0), provided.Expression);
+ }
+
+ [Theory]
+ [InlineData("floor(x)", @"\left\lfloor{x}\right\rfloor")]
+ [InlineData("ceil(x)", @"\left\lceil{x}\right\rceil")]
+ public void TheLatexIsTheUsualBrackets(string input, string expected)
+ => Assert.Equal(expected, input.ToEntity().Latexise());
+
+ // Stringize prints the short spelling, and SymPy's `ceiling` is accepted on the way
+ // in so that an expression copied from there parses.
+ [Theory]
+ [InlineData("floor(x)", "floor(x)")]
+ [InlineData("ceil(x)", "ceil(x)")]
+ [InlineData("ceiling(x)", "ceil(x)")]
+ public void ItPrintsTheShortSpellingAndParsesBoth(string input, string expected)
+ => Assert.Equal(expected, input.ToEntity().Stringize());
+
+ [Theory]
+ [InlineData("floor(x)")]
+ [InlineData("ceil(x)")]
+ [InlineData("floor(x) + ceil(y)")]
+ public void ThePrintedFormParsesBackToTheSameExpression(string input)
+ => Assert.Equal(input.ToEntity(), input.ToEntity().Stringize().ToEntity());
+
+ [Theory]
+ [InlineData("floor(x)", "7/2", 3)]
+ [InlineData("ceil(x)", "7/2", 4)]
+ [InlineData("floor(x)", "-7/2", -4)]
+ [InlineData("ceil(x)", "-7/2", -3)]
+ public void SubstitutingReachesTheValue(string input, string at, int expected)
+ => Assert.Equal(Entity.Number.Integer.Create(expected),
+ input.ToEntity().Substitute("x", at.ToEntity()).Simplify());
+
+ ///
+ /// floor(x) = 3 holds on the whole of [3, 4), so the inverse is a
+ /// parameter over that interval rather than a point. What it must not do is invent a
+ /// single root.
+ ///
+ [Fact]
+ public void SolvingGivesTheWholeIntervalAndNotAPoint()
+ {
+ var solutions = "floor(x) - 3 = 0".ToEntity().Solve("x");
+ Assert.NotNull(solutions);
+ var conditions = solutions!.Nodes.OfType().ToList();
+ Assert.NotEmpty(conditions);
+
+ // Every point of [3, 4) is a solution, and 4 is not.
+ foreach (var point in new[] { "3", "3.5", "39/10" })
+ Assert.Equal(Entity.Number.Integer.Create(3),
+ $"floor({point})".ToEntity().Simplify());
+ Assert.Equal(Entity.Number.Integer.Create(4), "floor(4)".ToEntity().Simplify());
+ }
+ }
+}
diff --git a/Sources/Tests/UnitTests/Convenience/MissingFunctionNamesRefusedTest.cs b/Sources/Tests/UnitTests/Convenience/MissingFunctionNamesRefusedTest.cs
index bbde307fd..584c6d65a 100644
--- a/Sources/Tests/UnitTests/Convenience/MissingFunctionNamesRefusedTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/MissingFunctionNamesRefusedTest.cs
@@ -26,10 +26,9 @@ namespace AngouriMath.Tests.Convenience
[Trait("Area", "Convenience")]
public sealed class MissingFunctionNamesRefusedTest
{
+ // floor, ceil and ceiling were on this list until they were implemented:
+ // https://github.com/asc-community/AngouriMath/issues/809
[Theory]
- [InlineData("floor(x)")]
- [InlineData("ceil(x)")]
- [InlineData("ceiling(x)")]
[InlineData("round(x)")]
[InlineData("trunc(x)")]
[InlineData("erf(x)")]
@@ -46,7 +45,7 @@ public void ANameTheLibraryDoesNotHaveIsRefused(string written) =>
/// cannot otherwise tell what happened to their expression.
///
[Theory]
- [InlineData("floor(x)", "floor")]
+ [InlineData("round(x)", "round")]
[InlineData("gcd(x, y)", "gcd")]
[InlineData("conjugate(x)", "conjugate")]
public void TheRefusalNamesTheFunction(string written, string name) =>
@@ -63,11 +62,22 @@ public void TheRefusalNamesTheFunction(string written, string name) =>
[InlineData("min(x)")]
[InlineData("min(x, y)")]
[InlineData("min(x, y, z)")]
- [InlineData("floor(x)")]
- [InlineData("floor(x, y)")]
+ [InlineData("round(x)")]
+ [InlineData("round(x, y)")]
public void TheArgumentCountDoesNotDecideWhetherItIsReported(string written) =>
Assert.Throws(() => written.ToEntity());
+ ///
+ /// A name the library does have, called with the wrong number of arguments, is
+ /// a different complaint and says so -- the count is the problem, not the name.
+ ///
+ [Theory]
+ [InlineData("floor(x, y)", "floor")]
+ [InlineData("ceil(x, y)", "ceil")]
+ public void AnImplementedNameWithTheWrongArgumentCountSaysThat(string written, string name)
+ => Assert.Contains(name,
+ Assert.Throws(() => written.ToEntity()).Message);
+
///
/// What the defect actually cost, and the reason this is worth a parse error rather
/// than being left as a feature request: the solver answered confidently and wrongly.
@@ -76,8 +86,24 @@ public void TheArgumentCountDoesNotDecideWhetherItIsReported(string written) =>
public void TheEquationThatUsedToBeAnsweredWithNonsenseNowSaysWhyItCannotBe()
{
var thrown = Assert.Throws(
- () => "floor(x) - 3 = 0".ToEntity().Solve("x"));
- Assert.Contains("floor", thrown.Message);
+ () => "round(x) - 3 = 0".ToEntity().Solve("x"));
+ Assert.Contains("round", thrown.Message);
+ }
+
+ ///
+ /// And the original expression from #733 is now answered rather than refused, since
+ /// floor exists. floor(x) = 3 holds on the whole of [3, 4), so the
+ /// answer is that interval carried as a parameter and not a single point -- what it
+ /// must never be again is { 3 / floor }.
+ /// https://github.com/asc-community/AngouriMath/issues/809
+ ///
+ [Fact]
+ public void TheEquationFromTheOriginalReportIsAnsweredNow()
+ {
+ var solutions = "floor(x) - 3 = 0".ToEntity().Solve("x");
+ Assert.NotNull(solutions);
+ Assert.DoesNotContain(solutions!.Nodes, node => node is Entity.Variable { Name: "floor" });
+ Assert.Contains(solutions.Nodes, node => node is Entity.Providedf);
}
///
@@ -114,6 +140,9 @@ public void EverythingElseParsesAsItDid(string written, string expected) =>
[InlineData("sin(x)")]
[InlineData("arsinh(x)")]
[InlineData("phi(x)")]
+ [InlineData("floor(x)")]
+ [InlineData("ceil(x)")]
+ [InlineData("ceiling(x)")]
public void TheNamesTheLibraryHasStillParse(string written) =>
Assert.NotNull(written.ToEntity());