using System.Globalization; using System.Text; using AngouriMath; using PeterO.Numbers; namespace AIStudio.Tools.Mathematics; /// /// Writes the results of the Mathematics tools for the model: as text, as LaTeX, and as decimals. /// /// /// 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. /// public static class MathFormatting { /// /// The smallest decimal exponent written without scientific notation. /// private const int MIN_PLAIN_EXPONENT = -6; /// /// The largest decimal exponent written without scientific notation. /// private const int MAX_PLAIN_EXPONENT = 20; /// /// An expression in the plain syntax which the tools also read. /// /// The expression. /// What the expression is, named when it is too long. /// The expression as text. /// The text is longer than MathLimits.MAX_RESULT_LENGTH. public static string ToText(Entity expression, string what) => EnsureLength(expression.Stringize(), what); /// /// An expression as LaTeX, which the chat renders. /// /// The expression. /// What the expression is, named when it is too long. /// The expression as LaTeX. /// The LaTeX is longer than MathLimits.MAX_RESULT_LENGTH. public static string ToLatex(Entity expression, string what) => EnsureLength(expression.Latexize(), what); /// /// Refuses a text longer than MathLimits.MAX_RESULT_LENGTH. /// /// The text of a result. /// What the text is, named when it is too long. /// The text. /// The text is too long. 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; } /// /// A decimal, rounded to the given number of significant digits. /// /// /// 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. /// /// The value. /// How many significant digits to keep. /// The decimal. 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(); } /// /// A double, rounded to the given number of significant digits. /// /// The value. /// How many significant digits to keep, 17 at most. /// The decimal. 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)); } /// /// A real or complex number, rounded to the given number of significant digits, as in 1.5 - 2i. /// /// The number. /// How many significant digits to keep in each part. /// The decimal. 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}"; } /// /// How far apart two numbers lie, as the sum of the distances of their parts. /// /// One number. /// The other number. /// The distance, as a double. 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(); } /// /// 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. /// /// One value. /// The other value. /// The most digits to report, for values which are equal. /// The number of digits, between 0 and maximum. 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); } /// /// How many significant digits two values of the same quantity have in common. /// /// /// 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. /// /// One value. /// The other value. /// The most digits to report, for values which are equal. /// The number of digits, between 0 and maximum. 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); } }