Files
AI-Studio/app/MindWork AI Studio/Tools/Mathematics/MathFormatting.cs
T

208 lines
8.9 KiB
C#

using System.Globalization;
using System.Text;
using AngouriMath;
using PeterO.Numbers;
namespace AIStudio.Tools.Mathematics;
/// <summary>
/// Writes the results of the Mathematics tools for the model: as text, as LaTeX, and as decimals.
/// </summary>
/// <remarks>
/// Every number is written the same way whatever the language of the user, because it goes to a
/// model and comes back in the plain syntax of the tools.
/// </remarks>
public static class MathFormatting
{
/// <summary>
/// The smallest decimal exponent written without scientific notation.
/// </summary>
private const int MIN_PLAIN_EXPONENT = -6;
/// <summary>
/// The largest decimal exponent written without scientific notation.
/// </summary>
private const int MAX_PLAIN_EXPONENT = 20;
/// <summary>
/// An expression in the plain syntax which the tools also read.
/// </summary>
/// <param name="expression">The expression.</param>
/// <param name="what">What the expression is, named when it is too long.</param>
/// <returns>The expression as text.</returns>
/// <exception cref="MathComputationException">The text is longer than MathLimits.MAX_RESULT_LENGTH.</exception>
public static string ToText(Entity expression, string what) => EnsureLength(expression.Stringize(), what);
/// <summary>
/// An expression as LaTeX, which the chat renders.
/// </summary>
/// <param name="expression">The expression.</param>
/// <param name="what">What the expression is, named when it is too long.</param>
/// <returns>The expression as LaTeX.</returns>
/// <exception cref="MathComputationException">The LaTeX is longer than MathLimits.MAX_RESULT_LENGTH.</exception>
public static string ToLatex(Entity expression, string what) => EnsureLength(expression.Latexize(), what);
/// <summary>
/// Refuses a text longer than MathLimits.MAX_RESULT_LENGTH.
/// </summary>
/// <param name="text">The text of a result.</param>
/// <param name="what">What the text is, named when it is too long.</param>
/// <returns>The text.</returns>
/// <exception cref="MathComputationException">The text is too long.</exception>
public static string EnsureLength(string text, string what)
{
if (text.Length > MathLimits.MAX_RESULT_LENGTH)
throw new MathComputationException(string.Create(CultureInfo.InvariantCulture, $"{what} has {text.Length:N0} characters, but at most {MathLimits.MAX_RESULT_LENGTH:N0} are returned."));
return text;
}
/// <summary>
/// A decimal, rounded to the given number of significant digits.
/// </summary>
/// <remarks>
/// Trailing zeros are left out, so 0.5 stays 0.5 at twenty digits; how many digits the value
/// carries is reported next to it. Between 1e-6 and 1e21 the decimal is written out, beyond that
/// in scientific notation such as 6.02214076e+23.
/// </remarks>
/// <param name="value">The value.</param>
/// <param name="digits">How many significant digits to keep.</param>
/// <returns>The decimal.</returns>
public static string FormatDecimal(EDecimal value, int digits)
{
if (value.IsNaN())
return "NaN";
if (value.IsInfinity())
return value.IsNegative ? "-oo" : "+oo";
var rounded = value.RoundToPrecision(EContext.ForPrecisionAndRounding(digits, ERounding.HalfEven)).Reduce(null);
if (rounded.IsZero)
return "0";
var mantissa = rounded.Mantissa.Abs().ToString();
var exponent = rounded.Exponent.ToInt64Checked() + mantissa.Length - 1;
if (exponent is >= MIN_PLAIN_EXPONENT and <= MAX_PLAIN_EXPONENT)
return rounded.ToPlainString();
var scientific = new StringBuilder();
if (rounded.IsNegative)
scientific.Append('-');
scientific.Append(mantissa[0]);
if (mantissa.Length > 1)
scientific.Append('.').Append(mantissa, 1, mantissa.Length - 1);
scientific.Append('e').Append(exponent < 0 ? '-' : '+').Append(Math.Abs(exponent).ToString(CultureInfo.InvariantCulture));
return scientific.ToString();
}
/// <summary>
/// A double, rounded to the given number of significant digits.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="digits">How many significant digits to keep, 17 at most.</param>
/// <returns>The decimal.</returns>
public static string FormatDecimal(double value, int digits)
{
if (double.IsNaN(value))
return "NaN";
if (double.IsInfinity(value))
return value < 0 ? "-oo" : "+oo";
return FormatDecimal(EDecimal.FromDouble(value), Math.Min(digits, 17));
}
/// <summary>
/// A real or complex number, rounded to the given number of significant digits, as in 1.5 - 2i.
/// </summary>
/// <param name="value">The number.</param>
/// <param name="digits">How many significant digits to keep in each part.</param>
/// <returns>The decimal.</returns>
public static string FormatNumber(Entity.Number.Complex value, int digits)
{
var real = value.RealPart.EDecimal;
var imaginary = value.ImaginaryPart.EDecimal;
if (imaginary.IsZero)
return FormatDecimal(real, digits);
var imaginaryText = FormatDecimal(imaginary.Abs(), digits) + "i";
if (real.IsZero)
return imaginary.IsNegative ? "-" + imaginaryText : imaginaryText;
return $"{FormatDecimal(real, digits)} {(imaginary.IsNegative ? '-' : '+')} {imaginaryText}";
}
/// <summary>
/// How far apart two numbers lie, as the sum of the distances of their parts.
/// </summary>
/// <param name="first">One number.</param>
/// <param name="second">The other number.</param>
/// <returns>The distance, as a double.</returns>
public static double Distance(Entity.Number.Complex first, Entity.Number.Complex second)
{
var context = EContext.ForPrecision(20);
var real = first.RealPart.EDecimal.Subtract(second.RealPart.EDecimal, context).Abs();
var imaginary = first.ImaginaryPart.EDecimal.Subtract(second.ImaginaryPart.EDecimal, context).Abs();
return real.Add(imaginary, context).ToDouble();
}
/// <summary>
/// How many significant digits two real or complex values of the same quantity have in common,
/// measured against the larger of them, so a tiny imaginary part does not count for more than it is.
/// </summary>
/// <param name="first">One value.</param>
/// <param name="second">The other value.</param>
/// <param name="maximum">The most digits to report, for values which are equal.</param>
/// <returns>The number of digits, between 0 and maximum.</returns>
public static int CountMatchingDigits(Entity.Number.Complex first, Entity.Number.Complex second, int maximum)
{
if (first.ImaginaryPart.EDecimal.IsZero && second.ImaginaryPart.EDecimal.IsZero)
return CountMatchingDigits(first.RealPart.EDecimal, second.RealPart.EDecimal, maximum);
var context = EContext.ForPrecision(20);
var firstSize = first.RealPart.EDecimal.Abs().Add(first.ImaginaryPart.EDecimal.Abs(), context);
var secondSize = second.RealPart.EDecimal.Abs().Add(second.ImaginaryPart.EDecimal.Abs(), context);
var scale = EDecimal.Max(firstSize, secondSize).ToDouble();
var distance = Distance(first, second);
if (!double.IsFinite(distance) || !double.IsFinite(scale))
return 0;
if (distance <= 0)
return maximum;
return (int)Math.Clamp(Math.Floor(-Math.Log10(distance / scale)), 0, maximum);
}
/// <summary>
/// How many significant digits two values of the same quantity have in common.
/// </summary>
/// <remarks>
/// Counted from their relative difference, as is usual for correct digits: 0.99999 and 1.00001
/// agree to four digits although no digit is the same. Evaluated at two working precisions, the
/// digits they share are the ones the computation carries.
/// </remarks>
/// <param name="first">One value.</param>
/// <param name="second">The other value.</param>
/// <param name="maximum">The most digits to report, for values which are equal.</param>
/// <returns>The number of digits, between 0 and maximum.</returns>
public static int CountMatchingDigits(EDecimal first, EDecimal second, int maximum)
{
if (first.IsNaN() || second.IsNaN() || first.IsInfinity() || second.IsInfinity())
return 0;
if (first.CompareTo(second) == 0)
return maximum;
var context = EContext.ForPrecision(20);
var scale = first.Abs().CompareTo(second.Abs()) >= 0 ? first.Abs() : second.Abs();
var relative = first.Subtract(second, context).Abs().Divide(scale, context).ToDouble();
if (relative <= 0)
return maximum;
return (int)Math.Clamp(Math.Floor(-Math.Log10(relative)), 0, maximum);
}
}