diff --git a/PreciseNumber.Test/PreciseNumberParseCultureTests.cs b/PreciseNumber.Test/PreciseNumberParseCultureTests.cs index 6cdf76a..59108e9 100644 --- a/PreciseNumber.Test/PreciseNumberParseCultureTests.cs +++ b/PreciseNumber.Test/PreciseNumberParseCultureTests.cs @@ -100,4 +100,54 @@ public void Parse_ReadsTheExponentSignWithTheProvider() { Assert.AreEqual(P("0.015"), PreciseNumber.Parse("1,5eāˆ’2", CommaDecimal)); } + + [TestMethod] + [DataRow("1.5", NumberStyles.Integer)] + [DataRow("1e5", NumberStyles.Integer)] + [DataRow("-5", NumberStyles.None)] + [DataRow("1e 5", NumberStyles.Float)] + [DataRow("1e5", NumberStyles.Number)] + [DataRow("1.5", NumberStyles.None)] + [DataRow("1.5", NumberStyles.Float)] + [DataRow("-1.5e-3", NumberStyles.Float)] + [DataRow("-1.5e-3", NumberStyles.Any)] + [DataRow("1,234.5", NumberStyles.Any)] + [DataRow("-5", NumberStyles.Integer)] + [DataRow("1e+5", NumberStyles.Float)] + public void TryParse_AcceptsWhatDecimalAccepts(string text, NumberStyles style) + { + bool decimalParsed = decimal.TryParse(text, style, CultureInfo.InvariantCulture, out decimal expected); + bool parsed = PreciseNumber.TryParse(text, style, CultureInfo.InvariantCulture, out PreciseNumber actual); + + Assert.AreEqual(decimalParsed, parsed, $"PreciseNumber and decimal disagree on whether \"{text}\" parses under {style}."); + if (parsed) + { + Assert.AreEqual(P(expected.ToString(CultureInfo.InvariantCulture)), actual); + } + } + + [TestMethod] + public void Parse_ThrowsForWhatTheStyleDoesNotAllow() + { + Assert.ThrowsExactly(() => PreciseNumber.Parse("1.5", NumberStyles.Integer, CultureInfo.InvariantCulture)); + Assert.ThrowsExactly(() => PreciseNumber.Parse("1e5", NumberStyles.Integer, CultureInfo.InvariantCulture)); + Assert.ThrowsExactly(() => PreciseNumber.Parse("-5", NumberStyles.None, CultureInfo.InvariantCulture)); + Assert.ThrowsExactly(() => PreciseNumber.Parse("1e 5", NumberStyles.Float, CultureInfo.InvariantCulture)); + } + + [TestMethod] + public void Parse_WithoutAStyleStillReadsFractionsExponentsAndSigns() + { + Assert.AreEqual(P("1.5"), PreciseNumber.Parse("1.5", CultureInfo.InvariantCulture)); + Assert.AreEqual(P("-0.0015"), PreciseNumber.Parse("-1.5e-3", CultureInfo.InvariantCulture)); + Assert.AreEqual(P("1234.5"), PreciseNumber.Parse("1,234.5", CultureInfo.InvariantCulture)); + } + + [TestMethod] + public void Parse_ReadsACulturesNegativeSignOnlyWhenTheStyleAllowsIt() + { + Assert.AreEqual(P("-2.5"), PreciseNumber.Parse("āˆ’2,5", NumberStyles.Float, CommaDecimal)); + Assert.IsFalse(PreciseNumber.TryParse("āˆ’2", NumberStyles.None, CommaDecimal, out _)); + Assert.IsFalse(PreciseNumber.TryParse("-2", NumberStyles.None, CommaDecimal, out _)); + } } diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index a3da4fd..621bd81 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -1210,6 +1210,8 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor ReadOnlySpan decimalSeparator = numberFormat.NumberDecimalSeparator; ReadOnlySpan groupSeparator = numberFormat.NumberGroupSeparator; bool allowThousands = style.HasFlag(NumberStyles.AllowThousands) && !groupSeparator.IsEmpty; + bool allowDecimalPoint = style.HasFlag(NumberStyles.AllowDecimalPoint); + bool allowExponent = style.HasFlag(NumberStyles.AllowExponent); int startIndex = ReadLeadingSign(s, style, numberFormat, out bool isNegative); @@ -1229,7 +1231,7 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor for (int i = startIndex; i < s.Length; i++) { char c = s[i]; - if (s[i..].StartsWith(decimalSeparator, StringComparison.Ordinal)) + if (allowDecimalPoint && s[i..].StartsWith(decimalSeparator, StringComparison.Ordinal)) { if (hasDecimal) { @@ -1248,10 +1250,12 @@ public static PreciseNumber Parse(ReadOnlySpan s, NumberStyles style, IFor continue; } - if (c is 'e' or 'E') + if (allowExponent && c is 'e' or 'E') { // An exponent outside the range of int throws OverflowException, which TryParse reports as failure. - exponent = int.Parse(s[(i + 1)..], NumberStyles.Integer, numberFormat); + // Only a sign may precede its digits: NumberStyles.Integer would also let white space through, + // accepting "1e 5" where every BCL numeric type rejects it. + exponent = int.Parse(s[(i + 1)..], NumberStyles.AllowLeadingSign, numberFormat); break; } @@ -1313,10 +1317,17 @@ private static ReadOnlySpan TrimAllowedWhite(ReadOnlySpan s, NumberS /// Reads a leading sign spelled as spells it, returning the index /// of the first character after it. The ASCII hyphen stays accepted whatever the culture spells /// its negative sign with, as it always has been here and as the BCL does for cultures that use - /// U+2212. A positive sign is only read when allows a leading sign. + /// U+2212. Neither sign is read unless allows a leading sign, so under + /// a sign is left in place and rejected as a non-digit. /// private static int ReadLeadingSign(ReadOnlySpan s, NumberStyles style, NumberFormatInfo numberFormat, out bool isNegative) { + isNegative = false; + if (!style.HasFlag(NumberStyles.AllowLeadingSign)) + { + return 0; + } + isNegative = true; if (s.StartsWith(numberFormat.NegativeSign, StringComparison.Ordinal)) { @@ -1329,7 +1340,7 @@ private static int ReadLeadingSign(ReadOnlySpan s, NumberStyles style, Num } isNegative = false; - return style.HasFlag(NumberStyles.AllowLeadingSign) && s.StartsWith(numberFormat.PositiveSign, StringComparison.Ordinal) + return s.StartsWith(numberFormat.PositiveSign, StringComparison.Ordinal) ? numberFormat.PositiveSign.Length : 0; }