From 18a1454e922b1f1b5443bdc6c63f5d4b14075617 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 25 Oct 2026 21:35:44 +0000 Subject: [PATCH] A catalogue of the library's operations, by name Part of https://github.com/asc-community/AngouriMath/issues/1684 The terminal's commands and the MCP server's tools each call the library themselves, and nothing keeps the three in step. Core/Operations/Catalogue.cs lists the operations the library offers by name, thirty-three of them, each with a one-line description and the arguments it takes besides its input: - eight transformations, simplify to substitute, each with the relation it claims; - queries, whose answer is not an expression standing for the input: solve, evaluate, latex, the truth table and its satisfying assignments, the series, base conversion, the matrix operations and the number theory. Each entry answers exactly what the method it names answers, so a surface that switches to it changes no answer. The names are #1019 item 16's v3 column, the library's own verbs. Internal until v3, as decided on the issue, in a namespace of its own. Transformation and its factories stay where they are. CatalogueTest checks that the list holds every entry once, that each entry's parameters are its delegate's arguments after the input, and that each transformation entry claims what its Transformation claims. It also places every factory of Transformation as an entry or as a building block with its reason, so a new factory fails until it is placed. Removing two factories from those lists fails it and names them both. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- AGENTS.md | 5 +- Sources/.editorconfig | 6 + .../AngouriMath/Core/Operations/Catalogue.cs | 255 ++++++++++++++++++ .../AngouriMath/Core/Operations/Operation.cs | 97 +++++++ .../Core/Operations/CatalogueTest.cs | 231 ++++++++++++++++ 5 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 Sources/AngouriMath/Core/Operations/Catalogue.cs create mode 100644 Sources/AngouriMath/Core/Operations/Operation.cs create mode 100644 Sources/Tests/UnitTests/Core/Operations/CatalogueTest.cs diff --git a/AGENTS.md b/AGENTS.md index a148385fd..e3f99929b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -875,7 +875,10 @@ landed — and **the MCP server and two other repositories brought to the releas answer back out of the nodes. That documentation is as much a deliverable as the operation (the maintainer's words on [#1409](https://github.com/asc-community/AngouriMath/issues/1409)); write it there, or in this repository's `Docs/Usage` and the website, and check at every release that the - MCP still says what the library does. + MCP still says what the library does. An operation the library offers by name is an entry of + `Core/Operations/Catalogue.cs` ([#1684](https://github.com/asc-community/AngouriMath/issues/1684)), + and a new factory of `Transformation` fails `CatalogueTest` until it is an entry or a building + block with its reason. - [CSharpMath](https://github.com/verybadcat/CSharpMath), for the round trip of `Latexize` — see *The standard command wins* above. If a PR there from an earlier release is still unmerged, add to it rather than opening a second. diff --git a/Sources/.editorconfig b/Sources/.editorconfig index be901a7f0..45b05543c 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -24,6 +24,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/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 +[AngouriMath/Core/Operations/*.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 + [AngouriMath/Functions/Continuous/Integration/RothsteinTrager.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 @@ -87,6 +90,9 @@ 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 +[Tests/UnitTests/Core/Operations/*.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 + [Tests/UnitTests/Algebra/Polynomials/EvaluationIrreducibilityTest.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/Core/Operations/Catalogue.cs b/Sources/AngouriMath/Core/Operations/Catalogue.cs new file mode 100644 index 000000000..6dfb2e3a3 --- /dev/null +++ b/Sources/AngouriMath/Core/Operations/Catalogue.cs @@ -0,0 +1,255 @@ +// +// 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; +using System.Collections.Generic; +using System.Linq; +using AngouriMath.Core.Transformations; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Core.Operations +{ + /// + /// Every operation the library offers by name, which the terminal and the MCP server take + /// theirs from. + /// + /// + /// + /// The names are the ones + /// #1019 item 16 + /// proposes for amcli's commands in v3: the library's own verbs, so simplify + /// rather than simp and differentiate rather than diff. + /// + /// + /// An operation is in this list when a surface offers it, or could offer it with nothing + /// but text in and text out. The building blocks also names, + /// such as or + /// , are not: a caller composes them, and + /// none answers a question by itself. A test lists every factory of + /// as one or the other, so that a new one fails the build + /// until it is placed. + /// + /// https://github.com/asc-community/AngouriMath/issues/1684 + /// + internal static class Catalogue + { + [ConstantField] + private static readonly Parameter TheVariable = new("variable", "The variable it is over."); + + // ---------------------------------------------------------------- transformations + + /// . + public static TransformationOperation> Simplify { get; } = new( + "simplify", "Simplifies, searching the expression's rewritings for the simplest.", + TransformationRelation.Equivalence, static e => e.Simplify()); + + /// . + public static TransformationOperation> Normalize { get; } = new( + "normalize", "Tidies the expression in one structural pass, without the search simplify makes.", + TransformationRelation.Equivalence, static e => e.InnerSimplified); + + /// . + public static TransformationOperation> Expand { get; } = new( + "expand", "Multiplies products over sums out.", + TransformationRelation.Equivalence, static e => e.Expand()); + + /// . + public static TransformationOperation> Factorize { get; } = new( + "factorize", "Gathers common factors out and factors polynomials.", + TransformationRelation.Equivalence, static e => e.Factorize()); + + /// . + public static TransformationOperation> Differentiate { get; } = new( + "differentiate", "Differentiates over a variable, as many times as the order says.", + TransformationRelation.Derivation, static (e, variable, order) => e.Differentiate(variable, order), + TheVariable, new Parameter("order", "How many times; a negative order integrates instead.")); + + /// . + public static TransformationOperation> Integrate { get; } = new( + "integrate", "Finds an antiderivative over a variable, with a constant of integration, or leaves the integral unevaluated.", + TransformationRelation.Derivation, static (e, variable) => e.Integrate(variable), + TheVariable); + + /// . + public static TransformationOperation> Limit { get; } = new( + "limit", "Takes the limit as a variable approaches a value, or leaves it unevaluated.", + TransformationRelation.Derivation, static (e, variable, destination, side) => e.Limit(variable, destination, side), + TheVariable, new Parameter("destination", "The value it approaches."), new Parameter("side", "From the left, from the right, or from both sides.")); + + /// . + public static TransformationOperation> Substitute { get; } = new( + "substitute", "Replaces every occurrence of a subexpression with another expression.", + TransformationRelation.Derivation, static (e, what, with) => e.Substitute(what, with), + new Parameter("what", "The subexpression to replace."), new Parameter("with", "What to put in its place.")); + + // ---------------------------------------------------------------- queries on an expression + + /// . + public static Query> Solve { get; } = new( + "solve", "Solves an equation, an inequality or a statement for a variable.", + static (e, variable) => e.Solve(variable), + TheVariable); + + /// . + public static Query, IReadOnlyList, Matrix?>> SolveSystem { get; } = new( + "solve_system", "Solves equations jointly for several variables, one row of the answer for each solution.", + static (equations, variables) => MathS.Equations(equations).Solve([.. variables]), + new Parameter("variables", "The variables to solve for, in the order of the answer's columns.")); + + /// . + public static Query> Evaluate { get; } = new( + "evaluate", "Evaluates as far as it goes: to a number, a boolean or a set where it can, and as an expression where it cannot.", + static e => e.Evaled); + + /// . + public static Query> EvaluateNumerically { get; } = new( + "evaluate_numerically", "Evaluates to a complex number, at the working precision.", + static e => e.EvalNumerical()); + + /// . + public static Query>> Variables { get; } = new( + "variables", "Lists the free variables.", + static e => e.Vars); + + /// . + public static Query>> Alternatives { get; } = new( + "alternatives", "Lists forms of the expression that simplify finds, simplest first.", + static (e, level) => e.Alternate(level), + new Parameter("level", "How hard to look, as simplify's level.")); + + /// . + public static Query> Latex { get; } = new( + "latex", "Writes the expression as LaTeX.", + static e => e.Latexize()); + + /// . + public static Query> Sympy { get; } = new( + "sympy", "Writes the expression as Python code for SymPy.", + static e => MathS.ToSympyCode(e)); + + /// . + public static Query, Matrix?>> TruthTable { get; } = new( + "truth_table", "Tabulates a statement over every assignment of true and false to its variables.", + static (e, variables) => MathS.Boolean.BuildTruthTable(e, [.. variables]), + new Parameter("variables", "The variables, in the order of the table's columns.")); + + /// . + public static Query, Matrix?>> SatisfyingAssignments { get; } = new( + "satisfying_assignments", "The assignments of true and false to a statement's variables under which it holds, one row each.", + static (e, variables) => MathS.SolveBooleanTable(e, [.. variables]), + new Parameter("variables", "The variables, in the order of the rows' columns.")); + + /// + /// , or + /// where there is no point. + /// + public static Query> Series { get; } = new( + "series", "Expands as a Taylor series in a variable, up to a degree, around a point or around zero.", + static (e, variable, degree, around) => around is null + ? MathS.Series.Maclaurin(e, degree, variable) + : MathS.Series.Taylor(e, degree, (variable, around)), + TheVariable, new Parameter("degree", "The highest power kept."), new Parameter("around", "The point, or none for zero.")); + + // ---------------------------------------------------------------- queries on a number + + /// . + public static Query> ToBase { get; } = new( + "to_base", "Writes a real number in a base from 2 to 36.", + static (number, numberBase) => MathS.ToBaseN(number, numberBase), + new Parameter("base", "The base, from 2 to 36.")); + + /// . + public static Query> FromBase { get; } = new( + "from_base", "Reads a real number written in a base from 2 to 36.", + static (digits, numberBase) => MathS.FromBaseN(digits, numberBase), + new Parameter("base", "The base, from 2 to 36.")); + + // ---------------------------------------------------------------- queries on a matrix + + /// . + public static Query> Determinant { get; } = new( + "determinant", "The determinant of a square matrix, or none for a matrix that is not square.", + static m => m.Determinant); + + /// . + public static Query> Inverse { get; } = new( + "inverse", "The inverse of a square matrix, or none where it has none.", + static m => m.Inverse); + + /// . + public static Query> Transpose { get; } = new( + "transpose", "The transpose of a matrix.", + static m => m.T); + + /// . + public static Query> Rank { get; } = new( + "rank", "The rank of a matrix.", + static m => m.Rank); + + /// . + public static Query> ReducedRowEchelonForm { get; } = new( + "reduced_row_echelon_form", "A matrix in reduced row echelon form.", + static m => m.ReducedRowEchelonForm); + + /// . + public static Query> Trace { get; } = new( + "trace", "The trace of a square matrix.", + static m => m.Trace); + + /// . + public static Query> TensorProduct { get; } = new( + "tensor_product", "The tensor product of two matrices.", + static (a, b) => Matrix.TensorProduct(a, b), + new Parameter("other", "The matrix on the right.")); + + // ---------------------------------------------------------------- queries on an integer + + /// . + public static Query>> PrimeFactorization { get; } = new( + "prime_factorization", "The primes that divide an integer, each with its power.", + static n => MathS.NumberTheory.Factorize(n)); + + /// , evaluated. + public static Query> Totient { get; } = new( + "totient", "Euler's totient of an integer: how many positive integers up to it are coprime to it.", + static n => MathS.NumberTheory.Phi(n).Evaled); + + /// . + public static Query> GreatestCommonDivisor { get; } = new( + "greatest_common_divisor", "The greatest common divisor of two integers.", + static (a, b) => MathS.NumberTheory.GreatestCommonDivisor(a, b), + new Parameter("other", "The second integer.")); + + /// . + public static Query> DivisorCount { get; } = new( + "divisor_count", "How many positive divisors an integer has.", + static n => MathS.NumberTheory.CountDivisors(n)); + + /// . + public static Query> IsPrime { get; } = new( + "is_prime", "Whether an integer is prime.", + static n => n.IsPrime); + + // ---------------------------------------------------------------- the list + + /// Every entry above, in the order written. + /// Last in the file, because static initialisers run in the order they are written. + public static IReadOnlyList All { get; } = + [ + Simplify, Normalize, Expand, Factorize, Differentiate, Integrate, Limit, Substitute, + Solve, SolveSystem, Evaluate, EvaluateNumerically, Variables, Alternatives, Latex, Sympy, TruthTable, SatisfyingAssignments, Series, + ToBase, FromBase, + Determinant, Inverse, Transpose, Rank, ReducedRowEchelonForm, Trace, TensorProduct, + PrimeFactorization, Totient, GreatestCommonDivisor, DivisorCount, IsPrime, + ]; + + /// The entry named , or where there is none. + /// Its name, such as simplify. + public static Operation? Find(string name) => All.FirstOrDefault(operation => operation.Name == name); + } +} diff --git a/Sources/AngouriMath/Core/Operations/Operation.cs b/Sources/AngouriMath/Core/Operations/Operation.cs new file mode 100644 index 000000000..356ddbcaf --- /dev/null +++ b/Sources/AngouriMath/Core/Operations/Operation.cs @@ -0,0 +1,97 @@ +// +// 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; +using System.Collections.Generic; +using AngouriMath.Core.Transformations; + +namespace AngouriMath.Core.Operations +{ + /// + /// One thing the library does, under the name a caller asks for it by. The terminal's + /// commands and the MCP server's tools are each made of these, and + /// lists every one. + /// + /// + /// Internal until v3, while the shape of an entry may still move. The stable surface is the + /// methods on and , and an entry answers exactly + /// what the method it names answers. + /// https://github.com/asc-community/AngouriMath/issues/1684 + /// + internal abstract class Operation + { + private protected Operation(string name, string description, Parameter[] parameters) + { + Name = name; + Description = description; + Parameters = parameters; + } + + /// The name a caller asks for it by, such as simplify. + public string Name { get; } + + /// What it does, in one sentence. + public string Description { get; } + + /// + /// What it takes besides its input, in the order its delegate takes them after the input. + /// + public IReadOnlyList Parameters { get; } + + /// + public override string ToString() => Name; + } + + /// One argument an takes besides its input. + /// Its name, such as variable. + /// What it is, in one sentence. + internal sealed record Parameter(string Name, string Description); + + /// + /// An operation whose answer is an expression that stands in a stated relation to its + /// input: one of the s, applied as the method on + /// applies it. + /// + /// + /// Its signature: the input expression, then each of , + /// and an expression back. + /// + internal sealed class TransformationOperation : Operation where TApply : Delegate + { + internal TransformationOperation(string name, string description, + TransformationRelation relation, TApply apply, params Parameter[] parameters) + : base(name, description, parameters) + { + Relation = relation; + Apply = apply; + } + + /// What the answer claims about the input. + public TransformationRelation Relation { get; } + + /// Answers. + public TApply Apply { get; } + } + + /// + /// An operation whose answer is not an expression standing for the input: a set of + /// solutions, a number, text, a table. + /// + /// + /// Its signature: the input, then each of , and the + /// answer back. + /// + internal sealed class Query : Operation where TAsk : Delegate + { + internal Query(string name, string description, TAsk ask, params Parameter[] parameters) + : base(name, description, parameters) + => Ask = ask; + + /// Answers. + public TAsk Ask { get; } + } +} diff --git a/Sources/Tests/UnitTests/Core/Operations/CatalogueTest.cs b/Sources/Tests/UnitTests/Core/Operations/CatalogueTest.cs new file mode 100644 index 000000000..288aa6743 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/Operations/CatalogueTest.cs @@ -0,0 +1,231 @@ +// +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AngouriMath; +using AngouriMath.Core; +using AngouriMath.Core.Operations; +using AngouriMath.Core.Transformations; +using Xunit; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Core.Operations +{ + /// + /// The catalogue of operations: that it lists every entry it declares, that each entry + /// describes the delegate it holds, and that every factory of + /// is either an entry or a building block that says why it is not. + /// https://github.com/asc-community/AngouriMath/issues/1684 + /// + [Trait("Area", "Transformations")] + public sealed class CatalogueTest + { + /// + /// Each factory of that an entry applies, by the entry's name. + /// + private static readonly Dictionary EntryOfFactory = new() + { + [nameof(Transformation.Simplification)] = "simplify", + [nameof(Transformation.SimplificationAtLevel)] = "simplify", + [nameof(Transformation.InnerSimplification)] = "normalize", + [nameof(Transformation.Expansion)] = "expand", + [nameof(Transformation.ExpansionAtLevel)] = "expand", + [nameof(Transformation.Factorization)] = "factorize", + [nameof(Transformation.FactorizationAtLevel)] = "factorize", + [nameof(Transformation.Differentiation)] = "differentiate", + [nameof(Transformation.Integration)] = "integrate", + [nameof(Transformation.LimitAt)] = "limit", + [nameof(Transformation.Substitution)] = "substitute", + }; + + /// + /// The factories that are parts for a caller to compose rather than operations to ask + /// for by name, each with the reason. + /// + private static readonly Dictionary BuildingBlocks = new() + { + [nameof(Transformation.Rewriting)] = "applies a rule set the caller passes, which no text names", + [nameof(Transformation.NumericContentExtraction)] = "the last step of factorize", + [nameof(Transformation.AsSingleFraction)] = "a step of the rational canonical form", + [nameof(Transformation.RuleBasedFactorizationAtLevel)] = "the rule-based half of factorize", + [nameof(Transformation.PolynomialFactorization)] = "the polynomial half of factorize", + [nameof(Transformation.Normalization)] = "orders commutative operands, so that expressions can be compared rather than simplified", + [nameof(Transformation.RationalCanonicalization)] = "a canonical form for comparing rational functions, which nothing runs by default", + [nameof(Transformation.Canonicalization)] = "a canonical form for comparing expressions, which nothing runs by default", + [nameof(Transformation.Rationalization)] = "a step simplify takes", + [nameof(Transformation.EqualitySaturation)] = "an experimental search, which takes a budget and a cost model", + [nameof(Transformation.CanonicalizationOverGraph)] = "an experimental search, which takes a budget", + }; + + private static readonly Variable x = MathS.Var("x"); + + private static string DelegateName(Operation operation) => operation.GetType().GetGenericArguments().Single().Name; + + [Fact] + public void TheListHoldsEveryEntryOnce() + { + var declared = typeof(Catalogue) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(property => typeof(Operation).IsAssignableFrom(property.PropertyType)) + .Select(property => (Operation)property.GetValue(null)!) + .ToList(); + Assert.Equal(declared.Count, Catalogue.All.Count); + Assert.Equal(declared.OrderBy(o => o.Name), Catalogue.All.OrderBy(o => o.Name)); + Assert.Equal(Catalogue.All.Count, Catalogue.All.Select(o => o.Name).Distinct().Count()); + } + + [Fact] + public void NamesAreLowercaseWordsJoinedByUnderscores() + { + foreach (var operation in Catalogue.All) + Assert.Matches("^[a-z]+(_[a-z]+)*$", operation.Name); + } + + [Fact] + public void FindAnswersByName() + { + foreach (var operation in Catalogue.All) + Assert.Same(operation, Catalogue.Find(operation.Name)); + Assert.Null(Catalogue.Find("simp")); + } + + [Fact] + public void EachDescriptionIsOneSentence() + { + foreach (var operation in Catalogue.All) + foreach (var description in operation.Parameters.Select(p => p.Description).Prepend(operation.Description)) + { + Assert.False(string.IsNullOrWhiteSpace(description), operation.Name); + Assert.DoesNotContain('\n', description); + Assert.EndsWith(".", description); + } + } + + /// + /// The parameters are the delegate's arguments after its input, so a parameter added to + /// one and not the other is caught here. + /// + [Fact] + public void TheParametersAreTheDelegatesArgumentsAfterTheInput() + { + foreach (var operation in Catalogue.All) + { + var invoke = operation.GetType().GetGenericArguments().Single().GetMethod("Invoke")!; + Assert.True(invoke.GetParameters().Length == operation.Parameters.Count + 1, + $"{operation.Name}: {DelegateName(operation)} takes {invoke.GetParameters().Length} arguments, and it names {operation.Parameters.Count} parameters"); + Assert.Equal(operation.Parameters.Count, operation.Parameters.Select(p => p.Name).Distinct().Count()); + } + } + + /// + /// A factory added to fails here until it is either an + /// entry or a building block, and a name that leaves it fails here too. + /// + [Fact] + public void EveryFactoryOfTransformationIsPlaced() + { + var factories = typeof(Transformation) + .GetMembers(BindingFlags.Public | BindingFlags.Static) + .Where(member => member switch + { + PropertyInfo property => property.PropertyType == typeof(Transformation), + MethodInfo method => method.ReturnType == typeof(Transformation) && !method.IsSpecialName, + _ => false, + }) + .Select(member => member.Name) + .ToHashSet(); + + var unplaced = factories.Where(name => !EntryOfFactory.ContainsKey(name) && !BuildingBlocks.ContainsKey(name)).ToList(); + Assert.True(unplaced.Count == 0, $"Not an entry and not a building block: {string.Join(", ", unplaced)}"); + var stale = EntryOfFactory.Keys.Concat(BuildingBlocks.Keys).Where(name => !factories.Contains(name)).ToList(); + Assert.True(stale.Count == 0, $"Listed here and no longer a factory: {string.Join(", ", stale)}"); + Assert.Empty(EntryOfFactory.Keys.Intersect(BuildingBlocks.Keys)); + foreach (var entry in EntryOfFactory.Values) + Assert.NotNull(Catalogue.Find(entry)); + } + + public static readonly IEnumerable TransformationsAndTheirEntries = new (Transformation, string)[] + { + (Transformation.Simplification, "simplify"), + (Transformation.InnerSimplification, "normalize"), + (Transformation.Expansion, "expand"), + (Transformation.Factorization, "factorize"), + (Transformation.Differentiation(x), "differentiate"), + (Transformation.Integration(x), "integrate"), + (Transformation.LimitAt(x, 0, ApproachFrom.BothSides), "limit"), + (Transformation.Substitution(x, 1), "substitute"), + }.Select(pair => new object[] { pair.Item1, pair.Item2 }); + + [Theory] + [MemberData(nameof(TransformationsAndTheirEntries))] + public void AnEntryClaimsWhatItsTransformationClaims(Transformation transformation, string entry) + { + var relation = Catalogue.Find(entry)!.GetType().GetProperty("Relation")!.GetValue(Catalogue.Find(entry)); + Assert.Equal(transformation.Relation, relation); + } + + /// + /// One answer from each entry, against the method the entry names, on an input where + /// that method and its neighbours answer differently -- so that an entry holding the + /// wrong method is caught. + /// + [Fact] + public void EachEntryAnswersAsTheMethodItNames() + { + Entity trigonometric = "sin(x)^2 + cos(x)^2", square = "(x + 1)^2"; + var y = MathS.Var("y"); + Assert.Equal(trigonometric.Simplify(), Catalogue.Simplify.Apply(trigonometric)); + Assert.Equal(trigonometric.InnerSimplified, Catalogue.Normalize.Apply(trigonometric)); + Assert.NotEqual(Catalogue.Simplify.Apply(trigonometric), Catalogue.Normalize.Apply(trigonometric)); + Assert.Equal(square.Expand(), Catalogue.Expand.Apply(square)); + Assert.Equal(square.Expand().Factorize(), Catalogue.Factorize.Apply(square.Expand())); + Assert.Equal(square.Differentiate(x, 2), Catalogue.Differentiate.Apply(square, x, 2)); + Assert.Equal(square.Integrate(x), Catalogue.Integrate.Apply(square, x)); + Assert.Equal(MathS.FromString("sin(x) / x").Limit(x, 0, ApproachFrom.Left), Catalogue.Limit.Apply("sin(x) / x", x, 0, ApproachFrom.Left)); + Assert.Equal(square.Substitute(x, y), Catalogue.Substitute.Apply(square, x, y)); + + Assert.Equal(MathS.FromString("x^2 = 4").Solve(x), Catalogue.Solve.Ask("x^2 = 4", x)); + Entity[] system = ["x + y - 3", "x - y - 1"]; + Assert.Equal(MathS.Equations(system).Solve(x, y), Catalogue.SolveSystem.Ask(system, [x, y])); + Assert.Equal(MathS.FromString("sqrt(2) + 1").Evaled, Catalogue.Evaluate.Ask("sqrt(2) + 1")); + Assert.Equal(MathS.FromString("sqrt(2) + 1").EvalNumerical(), Catalogue.EvaluateNumerically.Ask("sqrt(2) + 1")); + Assert.Equal(MathS.FromString("x + y").Vars, Catalogue.Variables.Ask("x + y")); + Assert.Equal(trigonometric.Alternate(2), Catalogue.Alternatives.Ask(trigonometric, 2)); + Assert.Equal(square.Latexize(), Catalogue.Latex.Ask(square)); + Assert.Equal(MathS.ToSympyCode(square), Catalogue.Sympy.Ask(square)); + Assert.Equal(MathS.Boolean.BuildTruthTable("a and not b", "a", "b"), Catalogue.TruthTable.Ask("a and not b", [MathS.Var("a"), MathS.Var("b")])); + Assert.Equal(MathS.SolveBooleanTable("a and not b", "a", "b"), Catalogue.SatisfyingAssignments.Ask("a and not b", [MathS.Var("a"), MathS.Var("b")])); + Assert.NotEqual(Catalogue.TruthTable.Ask("a and not b", [MathS.Var("a"), MathS.Var("b")]), Catalogue.SatisfyingAssignments.Ask("a and not b", [MathS.Var("a"), MathS.Var("b")])); + Assert.Equal(MathS.Series.Maclaurin("e^x", 3, x), Catalogue.Series.Ask("e^x", x, 3, null)); + Assert.Equal(MathS.Series.Taylor("e^x", 3, (x, 1)), Catalogue.Series.Ask("e^x", x, 3, 1)); + Assert.NotEqual(Catalogue.Series.Ask("e^x", x, 3, null), Catalogue.Series.Ask("e^x", x, 3, 1)); + + Assert.Equal(MathS.ToBaseN(10.5m, 2), Catalogue.ToBase.Ask(10.5m, 2)); + Assert.Equal(MathS.FromBaseN("1010.1", 2), Catalogue.FromBase.Ask("1010.1", 2)); + + var matrix = MathS.Matrix(new Entity[,] { { 1, 2 }, { 3, 4 } }); + Assert.Equal(matrix.Determinant, Catalogue.Determinant.Ask(matrix)); + Assert.Equal(matrix.Inverse, Catalogue.Inverse.Ask(matrix)); + Assert.Equal(matrix.T, Catalogue.Transpose.Ask(matrix)); + Assert.Equal(matrix.Rank, Catalogue.Rank.Ask(matrix)); + Assert.Equal(matrix.ReducedRowEchelonForm, Catalogue.ReducedRowEchelonForm.Ask(matrix)); + Assert.Equal(matrix.Trace, Catalogue.Trace.Ask(matrix)); + Assert.Equal(Matrix.TensorProduct(matrix, matrix.T), Catalogue.TensorProduct.Ask(matrix, matrix.T)); + + Number.Integer twelve = 12, eighteen = 18; + Assert.Equal(MathS.NumberTheory.Factorize(twelve), Catalogue.PrimeFactorization.Ask(twelve)); + Assert.Equal(MathS.NumberTheory.Phi(twelve).Evaled, Catalogue.Totient.Ask(twelve)); + Assert.Equal(MathS.NumberTheory.GreatestCommonDivisor(twelve, eighteen), Catalogue.GreatestCommonDivisor.Ask(twelve, eighteen)); + Assert.Equal(MathS.NumberTheory.CountDivisors(twelve), Catalogue.DivisorCount.Ask(twelve)); + Assert.Equal(twelve.IsPrime, Catalogue.IsPrime.Ask(twelve)); + Assert.True(Catalogue.IsPrime.Ask(7)); + } + } +}