diff --git a/PreciseNumber.Test/PreciseNumberParseAffixTests.cs b/PreciseNumber.Test/PreciseNumberParseAffixTests.cs index 40b8aa2..3e338f8 100644 --- a/PreciseNumber.Test/PreciseNumberParseAffixTests.cs +++ b/PreciseNumber.Test/PreciseNumberParseAffixTests.cs @@ -117,6 +117,13 @@ public class PreciseNumberParseAffixTests [DataRow("1 234,50 CHF", NumberStyles.Currency, "Swiss")] [DataRow("CHF 1 234.50", NumberStyles.Currency, "Swiss")] [DataRow("€1,5", NumberStyles.Any, "Mixed")] + [DataRow("$(45)", NumberStyles.Currency, "Dollars")] + [DataRow("$(1,234.50)", NumberStyles.Currency, "Dollars")] + [DataRow("(45)$", NumberStyles.Currency, "Dollars")] + [DataRow("$ (45)", NumberStyles.Currency, "Dollars")] + [DataRow("¤(45)", NumberStyles.Any, "Invariant")] + [DataRow("(¤45)", NumberStyles.Any, "Invariant")] + [DataRow("(45¤)", NumberStyles.Any, "Invariant")] public void TestParseAcceptsWhatDecimalAccepts(string text, NumberStyles style, string format) { NumberFormatInfo numberFormat = Format(format); @@ -173,6 +180,9 @@ public void TestParseAcceptsANoBreakSpaceBeforeACurrencySymbol() [DataRow("-$ 5", NumberStyles.AllowCurrencySymbol | NumberStyles.AllowLeadingSign, "Dollars")] [DataRow("5 $", NumberStyles.AllowCurrencySymbol | NumberStyles.AllowTrailingSign, "Dollars")] [DataRow("CHF 1,5", NumberStyles.Currency, "Swiss")] + [DataRow("$($45)", NumberStyles.Currency, "Dollars")] + [DataRow("$(45)$", NumberStyles.Currency, "Dollars")] + [DataRow("$(45", NumberStyles.Currency, "Dollars")] public void TestParseRejectsWhatDecimalRejects(string text, NumberStyles style, string format) { NumberFormatInfo numberFormat = Format(format); diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index 8d5ec22..3ad24e9 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -1500,7 +1500,17 @@ private static ReadOnlySpan TrimAllowedWhite(ReadOnlySpan s, NumberS /// Thrown when the parentheses enclose a sign. private static ReadOnlySpan StripAffixes(ReadOnlySpan s, NumberStyles style, NumberFormatInfo numberFormat, out bool isNegative, out bool hasLeadingCurrencySymbol) { - bool hasParentheses = style.HasFlag(NumberStyles.AllowParentheses) && s.Length >= 2 && s[0] == '(' && s[^1] == ')'; + ReadOnlySpan currencySymbol = style.HasFlag(NumberStyles.AllowCurrencySymbol) ? numberFormat.CurrencySymbol : []; + bool hasCurrencySymbol = false; + bool hasOuterLeadingCurrencySymbol = false; + + bool allowParentheses = style.HasFlag(NumberStyles.AllowParentheses); + if (allowParentheses) + { + StripCurrencySymbolOutsideParentheses(ref s, style, currencySymbol, ref hasCurrencySymbol, ref hasOuterLeadingCurrencySymbol); + } + + bool hasParentheses = allowParentheses && IsEnclosedInParentheses(s); if (hasParentheses) { s = s[1..^1]; @@ -1517,13 +1527,12 @@ private static ReadOnlySpan StripAffixes(ReadOnlySpan s, NumberStyle } } - ReadOnlySpan currencySymbol = style.HasFlag(NumberStyles.AllowCurrencySymbol) ? numberFormat.CurrencySymbol : []; bool hasSign = false; isNegative = false; - bool hasCurrencySymbol = false; + bool hadCurrencySymbol = hasCurrencySymbol; StripLeadingAffixes(ref s, style, numberFormat, currencySymbol, ref hasSign, ref isNegative, ref hasCurrencySymbol); - hasLeadingCurrencySymbol = hasCurrencySymbol; + hasLeadingCurrencySymbol = hasOuterLeadingCurrencySymbol || (hasCurrencySymbol && !hadCurrencySymbol); StripTrailingAffixes(ref s, style, numberFormat, currencySymbol, ref hasSign, ref isNegative, ref hasCurrencySymbol); if (hasParentheses) @@ -1539,6 +1548,47 @@ private static ReadOnlySpan StripAffixes(ReadOnlySpan s, NumberStyle return s; } + /// + /// Removes a currency symbol written outside the parentheses, as in $(45), $ (45) and + /// (45)$, for , along with the spaces it may have beside it. + /// + /// + /// The BCL takes the symbol either inside or outside the parentheses. It is removed only when what + /// is left is wholly enclosed in them, and it counts as the one symbol a number may have, so + /// $($45) and $(45)$ still fail. + /// + private static void StripCurrencySymbolOutsideParentheses(ref ReadOnlySpan s, NumberStyles style, ReadOnlySpan currencySymbol, ref bool hasCurrencySymbol, ref bool isLeading) + { + if (currencySymbol.IsEmpty) + { + return; + } + + if (s.StartsWith(currencySymbol, StringComparison.Ordinal)) + { + ReadOnlySpan rest = s[currencySymbol.Length..].TrimStart(SpacesBesideCurrencySymbol(style.HasFlag(NumberStyles.AllowLeadingWhite))); + if (IsEnclosedInParentheses(rest)) + { + hasCurrencySymbol = true; + isLeading = true; + s = rest; + return; + } + } + + if (s.EndsWith(currencySymbol, StringComparison.Ordinal)) + { + ReadOnlySpan rest = s[..^currencySymbol.Length].TrimEnd(SpacesBesideCurrencySymbol(style.HasFlag(NumberStyles.AllowTrailingWhite))); + if (IsEnclosedInParentheses(rest)) + { + hasCurrencySymbol = true; + s = rest; + } + } + } + + private static bool IsEnclosedInParentheses(ReadOnlySpan s) => s.Length >= 2 && s[0] == '(' && s[^1] == ')'; + /// /// Removes a leading sign and a leading currency symbol, in either order, for . ///