Problem
ToString and TryFormat always write fixed-point notation, including every zero the exponent implies. Parse accepts any exponent that fits in an int, so a value can parse successfully and then fail to format, or format as an enormous string. Observed on 2.0.3:
| Value |
ToString() |
TryFormat into 64 chars |
1E-2147483648 |
throws OverflowException |
throws ArgumentOutOfRangeException |
1E2147483647 |
throws ArgumentOutOfRangeException |
throws ArgumentOutOfRangeException |
1E-1000000 |
1,000,002 characters |
returns false |
1E1000000 |
1,000,001 characters |
returns false |
TryFormat should return false when the destination is too small rather than throw, as it does for the last two rows.
Cause
The formatting paths do their length arithmetic in int and negate or take the absolute value of the exponent:
ToString sizes its buffer as SignificantDigits + int.Abs(Exponent) + ... (PreciseNumber.cs lines 334 to 338). int.Abs throws for int.MinValue, and the sum overflows for exponents near int.MaxValue.
CountDecimalDigits returns int.Abs(Exponent) (PreciseNumber.cs line 657).
TryFormat computes fractionalDigits = -Exponent (PreciseNumber.cs line 1133), which wraps for int.MinValue, and derives slice lengths from the exponent.
#72 fixed this pattern in the conversion paths with range checks and by widening to long, but left formatting alone.
Suggested fix
- Do the length arithmetic in
long. TryFormat then returns false for any length that doesn't fit, and ToString fails with a clear exception when the text can't exist as a string.
- Consider scientific notation for extreme exponents. Supporting the
"E" and "G" standard format strings, or switching the default past some exponent the way double does, would make these values printable at all. Check how the format argument is handled today before choosing.
- Add tests for
int.MinValue and int.MaxValue exponents across ToString, TryFormat, and CountDecimalDigits.
Repro
#:package ktsu.PreciseNumber@2.0.3
using System.Globalization;
using ktsu.PreciseNumber;
PreciseNumber tiny = PreciseNumber.Parse("1E-2147483648", NumberStyles.Float, CultureInfo.InvariantCulture);
Console.WriteLine(tiny.ToString(CultureInfo.InvariantCulture)); // OverflowException
Problem
ToStringandTryFormatalways write fixed-point notation, including every zero the exponent implies.Parseaccepts any exponent that fits in anint, so a value can parse successfully and then fail to format, or format as an enormous string. Observed on 2.0.3:ToString()TryFormatinto 64 chars1E-2147483648OverflowExceptionArgumentOutOfRangeException1E2147483647ArgumentOutOfRangeExceptionArgumentOutOfRangeException1E-1000000false1E1000000falseTryFormatshould returnfalsewhen the destination is too small rather than throw, as it does for the last two rows.Cause
The formatting paths do their length arithmetic in
intand negate or take the absolute value of the exponent:ToStringsizes its buffer asSignificantDigits + int.Abs(Exponent) + ...(PreciseNumber.cslines 334 to 338).int.Absthrows forint.MinValue, and the sum overflows for exponents nearint.MaxValue.CountDecimalDigitsreturnsint.Abs(Exponent)(PreciseNumber.csline 657).TryFormatcomputesfractionalDigits = -Exponent(PreciseNumber.csline 1133), which wraps forint.MinValue, and derives slice lengths from the exponent.#72 fixed this pattern in the conversion paths with range checks and by widening to
long, but left formatting alone.Suggested fix
long.TryFormatthen returnsfalsefor any length that doesn't fit, andToStringfails with a clear exception when the text can't exist as a string."E"and"G"standard format strings, or switching the default past some exponent the waydoubledoes, would make these values printable at all. Check how theformatargument is handled today before choosing.int.MinValueandint.MaxValueexponents acrossToString,TryFormat, andCountDecimalDigits.Repro