Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions PreciseNumber.Test/PreciseNumberParseAffixTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
58 changes: 54 additions & 4 deletions PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@
/// A fixed precision such as <c>E15</c> rounds values that need 17 digits, which turns
/// <see cref="double.MaxValue"/> into a number that converts back to infinity.
/// </remarks>
internal static string GetStringFormatForFloatType<TFloat>()

Check warning on line 759 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 759 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove this method and declare a constant for this value.
where TFloat : INumber<TFloat>
=> "R";

Expand Down Expand Up @@ -1217,7 +1217,7 @@
public static PreciseNumber MinMagnitudeNumber(PreciseNumber x, PreciseNumber y) => MinMagnitude(x, y);

/// <inheritdoc/>
public static PreciseNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider)

Check warning on line 1220 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 30 to the 15 allowed.

Check warning on line 1220 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.

Check warning on line 1220 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 30 to the 15 allowed.

Check warning on line 1220 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

All 'Parse' method overloads should be adjacent.
{
// Read the separator and signs from the same culture TryFormat writes them with, so that
// Parse(x.ToString(p), p) round-trips for every provider.
Expand Down Expand Up @@ -1277,7 +1277,7 @@
}

hasDecimal = true;
i += decimalLength - 1;

Check warning on line 1280 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 1280 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

Expand All @@ -1285,7 +1285,7 @@
int groupLength = allowThousands && !hasDecimal && digitCount > 0 ? MatchSeparator(s[i..], groupSeparator, numberGroupSeparator) : 0;
if (groupLength > 0)
{
i += groupLength - 1;

Check warning on line 1288 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 1288 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
continue;
}

Expand Down Expand Up @@ -1500,7 +1500,17 @@
/// <exception cref="FormatException">Thrown when the parentheses enclose a sign.</exception>
private static ReadOnlySpan<char> StripAffixes(ReadOnlySpan<char> 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<char> 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];
Expand All @@ -1517,13 +1527,12 @@
}
}

ReadOnlySpan<char> 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)
Expand All @@ -1539,6 +1548,47 @@
return s;
}

/// <summary>
/// Removes a currency symbol written outside the parentheses, as in <c>$(45)</c>, <c>$ (45)</c> and
/// <c>(45)$</c>, for <see cref="StripAffixes"/>, along with the spaces it may have beside it.
/// </summary>
/// <remarks>
/// 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
/// <c>$($45)</c> and <c>$(45)$</c> still fail.
/// </remarks>
private static void StripCurrencySymbolOutsideParentheses(ref ReadOnlySpan<char> s, NumberStyles style, ReadOnlySpan<char> currencySymbol, ref bool hasCurrencySymbol, ref bool isLeading)
{
if (currencySymbol.IsEmpty)
{
return;
}

if (s.StartsWith(currencySymbol, StringComparison.Ordinal))
{
ReadOnlySpan<char> 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<char> rest = s[..^currencySymbol.Length].TrimEnd(SpacesBesideCurrencySymbol(style.HasFlag(NumberStyles.AllowTrailingWhite)));
if (IsEnclosedInParentheses(rest))
{
hasCurrencySymbol = true;
s = rest;
}
}
}

private static bool IsEnclosedInParentheses(ReadOnlySpan<char> s) => s.Length >= 2 && s[0] == '(' && s[^1] == ')';

/// <summary>
/// Removes a leading sign and a leading currency symbol, in either order, for <see cref="StripAffixes"/>.
/// </summary>
Expand Down
Loading