Problemy z metodą Decimal.Parse()

0

Witam,

Aktualnie pracuję w projekcie Windows Presentation Foundation .Net Framework 4.7.2
Problem z jakim się mierzę to zamiana ciągu znaków "123.456,789zł" na liczbę dziesiętną "123456,789".
Do tego celu wykorzystuję metodę Decimal.Parse(). Oto fragment mojego kodu:

 string strNumber = "123.456,78zł";
 NumberFormatInfo formatInfo = (NumberFormatInfo)NumberFormatInfo.CurrentInfo.Clone();
 formatInfo.CurrencyGroupSeparator = ".";
 formatInfo.CurrencySymbol = "zł";
 formatInfo.CurrencyDecimalSeparator = ",";
 formatInfo.CurrencyGroupSeparator = ".";
 formatInfo.CurrencyGroupSizes = new int[1] { 3 };
 decimal out_number3 = Decimal.Parse(strNumber, NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands, formatInfo);

W trakcie wykonywania programu otrzymuję wyjątek typu System.FormatException.
Dlaczego powyższy kod nie działa tak jak tego oczekuję? Czy nie wziąłem jeszcze czegoś pod uwagę? Co mogę zmienić, aby to naprawić?
Pozdrawiam

3

Żeby działało, to oprócz CurrencyDecimalSeparator i CurrencyGroupSeparator musisz w analogiczny sposób ustawić NumberDecimalSeperator i NumberGroupSeparator.

CurrencyGroupSizes nie musisz ustawiać w ogóle, 3 to wartość domyślna.

Poza tym, zamiast NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands możesz użyć po prostu NumberStyles.Currency. To zawiera w sobie wszystkie te trzy wartości, które obecnie podajesz ręcznie oraz całą resztę potrzebnych.

0

Zgadzam się, jednak w dokumentacji NumberStyles Enum jest napisane między innymi:

If the NumberStyles value includes the AllowCurrencySymbol flag and the parsed string includes a currency symbol, the decimal separator character is determined by the CurrencyDecimalSeparator property.
If the NumberStyles value includes the AllowCurrencySymbol flag and the string to be parsed includes a currency symbol, the valid group separator character is determined by the CurrencyGroupSeparator property, and the number of digits in each group is determined by the CurrencyGroupSizes property.

Więc czy jest to błąd w dokumentacji?

4

To jest bardzo filozoficzne pytanie. :)
Jak dla mnie to, co dokumuentacja opisuje jest intuicyjne oraz zgodne z rozumem i godnością człowieka, więc jak dla mnie to dokumentacja jest prawidłowa (jeśli traktować ją jako specyfikację).
Aczkolwiek dokumentacja nie zgadza się z tym, co jest w kodzie, więc w tym sensie jest błędna.

W tej linijce: https://referencesource.microsoft.com/#mscorlib/system/number.cs,847 jest taki pojebany if sprawdzający, czy weszliśmy już do części dziesiętnej liczby (trochę go rozbiłem, żeby lepiej było widać kolejność operacji):

if (
	((options & NumberStyles.AllowDecimalPoint) != 0)
	&& ((state & StateDecimal) == 0)
	&& ((next = MatchChars(p, decSep)) != null || ((parsingCurrency) && (state & StateCurrency) == 0) && (next = MatchChars(p, altdecSep)) != null)
)
{
	state |= StateDecimal;
	p = next - 1;
}

decSep to ,, a altDecSep to . (czyli to, co nadpisujesz przez NumberDecimalSeperator ).

No i w naszym wejściowym stringu po cyfrze 3 jest właśnie kropka. Pierwszy warunek z MatchChars sprawdza czy trafiliśmy na separator dziesiętny. Nie trafiliśmy, bo mamy kropkę, a dla currency ustawiliśmy przecinek. Na tym powinien zakończyć sprawę, i iść do następnego if, który jest odpowiedzialny za sprawdzenie separatora grupy cyfr.
Ale nie kończy, bo jest sprytny, upewnia się, że parsujemy walutę, i próbuje znowu sprawdzić czy kropka jest separatorem dziesiętnym, tym razem porównując z alternatywnym separatorem, czyli kropką.
Ponieważ kropka to kropka i kropka, to wszystko się zgadza, state jest ustawiany na StateDecimal i od tego momentu algorytm uważa, że jesteśmy w części dziesiętnej. Gdy spotka następny znak, który nie jest cyfrą, czyli przecinek przerywa parsowanie sekcji cyfr, potem robi kilka rzeczy (np. sprawdza, czy jest symbol waluty, ale ,78zł to nie jest symbol waluty), w efekcie nie parsuje wszystkiego prawidłowo i stąd FormatException.

1

To co masz działa
zamiast NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands po prostu wpisz NumberStyles.Currency. Poza tym zamiast klonować NumberFormatInfo.CurrentInfo to sklonuj polski standard i tylko dodaj CurrencyGroupSeparator

var strNumber = "123.456,78zł";
var formatInfo = new CultureInfo("pl").NumberFormat;
formatInfo.CurrencyGroupSeparator = ".";
var out_number3 = decimal.Parse(strNumber, NumberStyles.Currency, formatInfo);

Jak sklonujesz CurrentInfo to narażasz się na jakieś UB gdzieś na świecie - chyba że przetestujesz dla wszystkich możliwych kultur. Może dlatego u mnie Twój kod działa a u Ciebie już nie

1 użytkowników online, w tym zalogowanych: 0, gości: 1