diff --git a/CLAUDE.md b/CLAUDE.md
index 0e7c0ce..45413a7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -50,7 +50,7 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s
### Test Structure
-Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, `PreciseNumberExponentialTests.cs` does the same for the exponentials and logarithms and additionally pins the cases a `double` fallback cannot reach — fifty published digits of a fractional power, and `ExpM1`/`LogP1` of `1e-30` not collapsing to zero — `PreciseNumberHyperbolicTests.cs` pins the hyperbolics against published digits and against `cosh²x - sinh²x = 1`, and separates the small-argument assertions that actually discriminate (`sinh`, `asinh`, `atanh`) from the two that read like they do and don't (`tanh`, `acosh`) — the class remark records which is which, so the distinction survives the next person to read it — `PreciseNumberLargeArgumentTests.cs` bounds the time of `ExpM1`/`LogP1`/hyperbolics on huge arguments and pins digits the exact sums produced, and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.
+Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, `PreciseNumberExponentialTests.cs` does the same for the exponentials and logarithms and additionally pins the cases a `double` fallback cannot reach — fifty published digits of a fractional power, and `ExpM1`/`LogP1` of `1e-30` not collapsing to zero — `PreciseNumberHyperbolicTests.cs` pins the hyperbolics against published digits and against `cosh²x - sinh²x = 1`, and separates the small-argument assertions that actually discriminate (`sinh`, `asinh`, `atanh`) from the two that read like they do and don't (`tanh`, `acosh`) — the class remark records which is which, so the distinction survives the next person to read it — `PreciseNumberLargeArgumentTests.cs` bounds the time of `ExpM1`/`LogP1`/hyperbolics on huge arguments and pins digits the exact sums produced, `PreciseNumberDomainErrorTests.cs` holds every domain check's exception message to a short rendering of the argument however extreme its exponent (all of them build it through `DomainError`), and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.
### Benchmarks
diff --git a/PreciseNumber.Test/PreciseNumberDomainErrorTests.cs b/PreciseNumber.Test/PreciseNumberDomainErrorTests.cs
new file mode 100644
index 0000000..7d19133
--- /dev/null
+++ b/PreciseNumber.Test/PreciseNumberDomainErrorTests.cs
@@ -0,0 +1,97 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.PreciseNumber.Test;
+
+using System.Globalization;
+
+///
+/// Covers the exception every domain check throws for an argument outside its function's domain.
+/// Its message renders the argument, and must stay short and not throw, however far the argument's
+/// exponent reaches: a fixed-point rendering of 1e-2147483648 needs more characters than a
+/// string can hold.
+///
+[TestClass]
+public class PreciseNumberDomainErrorTests
+{
+ /// The longest message a domain error may carry.
+ private const int MessageLengthLimit = 1_000;
+
+ private static PreciseNumber Parse(string text) => PreciseNumber.Parse(text, CultureInfo.InvariantCulture);
+
+ private static PreciseNumber Call(string function, PreciseNumber x) => function switch
+ {
+ "Log" => PreciseNumber.Log(x),
+ "Log2" => PreciseNumber.Log2(x),
+ "Log10" => PreciseNumber.Log10(x),
+ "LogP1" => PreciseNumber.LogP1(x),
+ "Acosh" => PreciseNumber.Acosh(x),
+ "Atanh" => PreciseNumber.Atanh(x),
+ "Sqrt" => PreciseNumber.Sqrt(x),
+ "RootN4" => PreciseNumber.RootN(x, 4),
+ "RootN-4" => PreciseNumber.RootN(x, -4),
+ "Asin" => PreciseNumber.Asin(x),
+ "Acos" => PreciseNumber.Acos(x),
+ "PowOfNegativeTwo" => Parse("-2").Pow(x),
+ _ => throw new ArgumentException($"No such function: {function}", nameof(function)),
+ };
+
+ [TestMethod]
+ [DataRow("Log", "-1e2000000000")]
+ [DataRow("Log", "-1e-2000000000")]
+ [DataRow("Log", "-1e-2147483648")]
+ [DataRow("Log", "-1e-200000000")]
+ [DataRow("Log2", "-1e2000000000")]
+ [DataRow("Log2", "-1e-2147483648")]
+ [DataRow("Log10", "-1e2000000000")]
+ [DataRow("Log10", "-1e-2147483648")]
+ [DataRow("LogP1", "-1e2000000000")]
+ [DataRow("Acosh", "1e-2000000000")]
+ [DataRow("Acosh", "1e-2147483648")]
+ [DataRow("Acosh", "-1e2000000000")]
+ [DataRow("Acosh", "-1e-2147483648")]
+ [DataRow("Atanh", "1e2000000000")]
+ [DataRow("Atanh", "-1e2000000000")]
+ [DataRow("Sqrt", "-1e2000000000")]
+ [DataRow("Sqrt", "-1e-2000000000")]
+ [DataRow("Sqrt", "-1e-2147483648")]
+ [DataRow("RootN4", "-1e2000000000")]
+ [DataRow("RootN4", "-1e-2147483648")]
+ [DataRow("RootN-4", "-1e2000000000")]
+ [DataRow("RootN-4", "-1e-2147483648")]
+ [DataRow("Asin", "1e2000000000")]
+ [DataRow("Asin", "-1e2000000000")]
+ [DataRow("Acos", "1e2000000000")]
+ [DataRow("Acos", "-1e2000000000")]
+ [DataRow("PowOfNegativeTwo", "1e-2000000000")]
+ [DataRow("PowOfNegativeTwo", "1e-2147483648")]
+ [DataRow("PowOfNegativeTwo", "-1e-2147483648")]
+ public void TestDomainErrorMessageIsShortForAnExtremeExponent(string function, string argument)
+ {
+ PreciseNumber x = Parse(argument);
+
+ ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => Call(function, x));
+
+ string message = exception.Message;
+ Assert.IsLessThan(MessageLengthLimit, message.Length, $"{function}({argument}) has a {message.Length}-character message");
+ }
+
+ [TestMethod]
+ public void TestDomainErrorCarriesTheArgument()
+ {
+ ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => PreciseNumber.Sqrt(Parse("-12.5")));
+
+ Assert.AreEqual("x", exception.ParamName);
+ Assert.AreEqual("-1.25E+001", exception.ActualValue);
+ Assert.Contains("-1.25E+001", exception.Message);
+ }
+
+ [TestMethod]
+ public void TestDomainErrorKeepsThirtyFractionalDigitsOfALongArgument()
+ {
+ PreciseNumber x = -PreciseNumber.Pi;
+
+ ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => PreciseNumber.Log(x));
+
+ Assert.AreEqual(x.ToString("E30", CultureInfo.InvariantCulture), exception.ActualValue);
+ }
+}
diff --git a/PreciseNumber/PreciseNumber.Exponentials.cs b/PreciseNumber/PreciseNumber.Exponentials.cs
index 90995f9..2bb3749 100644
--- a/PreciseNumber/PreciseNumber.Exponentials.cs
+++ b/PreciseNumber/PreciseNumber.Exponentials.cs
@@ -71,6 +71,30 @@ public readonly partial record struct PreciseNumber
/// The message carried by the exception thrown for a fractional power of a negative value.
private const string NegativeBaseMessage = "A negative value has no real power with a fractional exponent.";
+ /// The most fractional digits renders of the offending value.
+ private const int DomainErrorFractionalDigits = 30;
+
+ ///
+ /// Builds the exception a domain check throws for an argument outside the function's domain.
+ ///
+ /// The name of the offending parameter.
+ /// The offending value.
+ /// What the function's domain is.
+ /// The exception to throw.
+ ///
+ /// renders
+ /// with ToString(), which writes fixed point: hundreds of megabytes for an exponent in the hundreds of
+ /// millions, and an exception of its own past what a string can hold. So the value is carried in the
+ /// E format, every digit of it up to after the point,
+ /// and reading the message can neither throw nor stall.
+ ///
+ private static ArgumentOutOfRangeException DomainError(string paramName, PreciseNumber value, string message)
+ {
+ int fractionalDigits = Math.Clamp(value.SignificantDigits - 1, 0, DomainErrorFractionalDigits);
+ string rendered = value.ToString("E" + fractionalDigits.ToString(InvariantCulture), InvariantCulture);
+ return new ArgumentOutOfRangeException(paramName, rendered, message);
+ }
+
/// Two, as the divisor of the halving step and the multiplier of the atanh series.
private static PreciseNumber Two { get; } = new(0, 2);
@@ -143,7 +167,7 @@ public static PreciseNumber Log(PreciseNumber x, int significantDigits)
if (x.Significand.Sign <= 0)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, NonPositiveLogarithmMessage);
+ throw DomainError(nameof(x), x, NonPositiveLogarithmMessage);
}
if (x.IsUnit)
@@ -257,7 +281,7 @@ public static PreciseNumber Log10(PreciseNumber x, int significantDigits)
if (x.Significand.Sign <= 0)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, NonPositiveLogarithmMessage);
+ throw DomainError(nameof(x), x, NonPositiveLogarithmMessage);
}
(PreciseNumber mantissa, long decimalExponent) = SplitAroundRootTen(x);
@@ -310,7 +334,7 @@ public static PreciseNumber LogP1(PreciseNumber x, int significantDigits)
if (x <= NegativeOne)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, LogP1DomainMessage);
+ throw DomainError(nameof(x), x, LogP1DomainMessage);
}
// Away from zero there is nothing to cancel, so the general logarithm is both simpler and
diff --git a/PreciseNumber/PreciseNumber.Hyperbolics.cs b/PreciseNumber/PreciseNumber.Hyperbolics.cs
index c60c5b5..b1a9c85 100644
--- a/PreciseNumber/PreciseNumber.Hyperbolics.cs
+++ b/PreciseNumber/PreciseNumber.Hyperbolics.cs
@@ -311,7 +311,7 @@ public static PreciseNumber Acosh(PreciseNumber x, int significantDigits)
if (x < One)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, AcoshDomainMessage);
+ throw DomainError(nameof(x), x, AcoshDomainMessage);
}
if (x.IsUnit)
@@ -364,7 +364,7 @@ public static PreciseNumber Atanh(PreciseNumber x, int significantDigits)
if (Abs(x) >= One)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, AtanhDomainMessage);
+ throw DomainError(nameof(x), x, AtanhDomainMessage);
}
if (x.Significand.IsZero)
diff --git a/PreciseNumber/PreciseNumber.Roots.cs b/PreciseNumber/PreciseNumber.Roots.cs
index 1ad4db6..c49ef1e 100644
--- a/PreciseNumber/PreciseNumber.Roots.cs
+++ b/PreciseNumber/PreciseNumber.Roots.cs
@@ -161,7 +161,7 @@ public static PreciseNumber RootN(PreciseNumber x, int n, int significantDigits)
0 => throw new DivideByZeroException(),
> 0 => PositiveReciprocalRootN(x, -n, significantDigits),
_ => int.IsEvenInteger(n)
- ? throw new ArgumentOutOfRangeException(nameof(x), x, NegativeRootMessage)
+ ? throw DomainError(nameof(x), x, NegativeRootMessage)
: -PositiveReciprocalRootN(-x, -n, significantDigits),
};
}
@@ -178,7 +178,7 @@ public static PreciseNumber RootN(PreciseNumber x, int n, int significantDigits)
// An odd root of a negative value is real, so the sign comes out and goes back on.
return int.IsEvenInteger(n)
- ? throw new ArgumentOutOfRangeException(nameof(x), x, NegativeRootMessage)
+ ? throw DomainError(nameof(x), x, NegativeRootMessage)
: -PositiveRootN(-x, n, significantDigits);
}
diff --git a/PreciseNumber/PreciseNumber.Trigonometry.cs b/PreciseNumber/PreciseNumber.Trigonometry.cs
index 44ccef6..0f43a20 100644
--- a/PreciseNumber/PreciseNumber.Trigonometry.cs
+++ b/PreciseNumber/PreciseNumber.Trigonometry.cs
@@ -295,7 +295,7 @@ public static PreciseNumber Asin(PreciseNumber x, int significantDigits)
PreciseNumber magnitude = Abs(x);
if (magnitude > One)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, InverseDomainMessage);
+ throw DomainError(nameof(x), x, InverseDomainMessage);
}
if (x.Significand.IsZero)
@@ -345,7 +345,7 @@ public static PreciseNumber Acos(PreciseNumber x, int significantDigits)
PreciseNumber magnitude = Abs(x);
if (magnitude > One)
{
- throw new ArgumentOutOfRangeException(nameof(x), x, InverseDomainMessage);
+ throw DomainError(nameof(x), x, InverseDomainMessage);
}
int working = significantDigits + TrigonometricGuardDigits;
diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs
index 602eeaf..e767f73 100644
--- a/PreciseNumber/PreciseNumber.cs
+++ b/PreciseNumber/PreciseNumber.cs
@@ -2224,7 +2224,7 @@ public PreciseNumber Pow(PreciseNumber power)
// no complex results here, so this is rejected rather than quietly returned as a NaN.
if (Significand.Sign < 0)
{
- throw new ArgumentOutOfRangeException(nameof(power), power, NegativeBaseMessage);
+ throw DomainError(nameof(power), power, NegativeBaseMessage);
}
int significantDigits = Math.Max(