Zastanawiam się, czy istnieje zwięzły i dokładny sposób wyciągnięcia liczby miejsc dziesiętnych z wartości dziesiętnej (jako liczby całkowitej), której można bezpiecznie używać w różnych kulturach?
Na przykład:
19,0 powinno zwrócić 1,
27,5999 powinno zwrócić 4,
19,12 powinno zwrócić 2
itd.
Napisałem zapytanie, które podzieliło ciąg na okres, aby znaleźć miejsca dziesiętne:
int priceDecimalPlaces = price.ToString().Split('.').Count() > 1
? price.ToString().Split('.').ToList().ElementAt(1).Length
: 0;
Ale wydaje mi się, że zadziała to tylko w regionach, w których jest używany znak „.” jako separator dziesiętny i dlatego jest bardzo kruchy w różnych systemach.
c#
decimal
cultureinfo
Jesse Carter
źródło
źródło
19.0
o zwrot1
to szczegół implementacyjny dotyczący wewnętrznego przechowywania wartości19.0
. Faktem jest, że program może przechowywać to jako190×10⁻¹
lub1900×10⁻²
lub19000×10⁻³
. Wszystkie są równe. Fakt, że używa pierwszej reprezentacji, gdy ma wartość19.0M
i jest to ujawniane, gdy używa się goToString
bez specyfikatora formatu, jest tylko zbiegiem okoliczności i szczęśliwą rzeczą. Z wyjątkiem tego, że nie jest szczęśliwe, gdy ludzie polegają na wykładniku w przypadkach, gdy nie powinni.19M
od19.0M
od19.00M
, trzeba utworzyć nową klasę, która wiązek wartości podstawowej jako jednej nieruchomości i liczby miejsca dziesiętne jako kolejna właściwość.Odpowiedzi:
Skorzystałem ze sposobu Joe, aby rozwiązać ten problem :)
decimal argument = 123.456m; int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
źródło
decimal
zachowuje liczbę cyfr po przecinku, dlatego znajdujesz ten "problem", musisz rzucić dziesiętną na podwójną i ponownie na dziesiętną dla poprawki: BitConverter.GetBytes (decimal.GetBits ((decimal) (double) argument) [3]) [ 2];Ponieważ żadna z udzielonych odpowiedzi nie była wystarczająco dobra dla magicznej liczby "-0.01f" zamienionej na dziesiętną ... tj .:
GetDecimal((decimal)-0.01f);
Mogę tylko założyć, że kolosalny wirus pierdnięcia umysłu zaatakował wszystkich 3 lata temu :)
Oto, co wydaje się działać implementacja do tego złego i potwornego problemu, bardzo skomplikowany problem zliczania miejsc po przecinku po przecinku - bez napisów, bez kultur, bez potrzeby liczenia bitów i bez potrzeby czytania forów matematycznych .. tylko prosta matematyka trzeciego stopnia.
public static class MathDecimals { public static int GetDecimalPlaces(decimal n) { n = Math.Abs(n); //make sure it is positive. n -= (int)n; //remove the integer part of the number. var decimalPlaces = 0; while (n > 0) { decimalPlaces++; n *= 10; n -= (int)n; } return decimalPlaces; } }
private static void Main(string[] args) { Console.WriteLine(1/3m); //this is 0.3333333333333333333333333333 Console.WriteLine(1/3f); //this is 0.3333333 Console.WriteLine(MathDecimals.GetDecimalPlaces(0.0m)); //0 Console.WriteLine(MathDecimals.GetDecimalPlaces(1/3m)); //28 Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)(1 / 3f))); //7 Console.WriteLine(MathDecimals.GetDecimalPlaces(-1.123m)); //3 Console.WriteLine(MathDecimals.GetDecimalPlaces(43.12345m)); //5 Console.WriteLine(MathDecimals.GetDecimalPlaces(0)); //0 Console.WriteLine(MathDecimals.GetDecimalPlaces(0.01m)); //2 Console.WriteLine(MathDecimals.GetDecimalPlaces(-0.001m)); //3 Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.00000001f)); //8 Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.0001234f)); //7 Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.01f)); //2 Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.01f)); //2 }
źródło
n = n % 1; if (n < 0) n = -n;
ponieważ wartość większa niżint.MaxValue
spowodujeOverflowException
np2147483648.12345
.Prawdopodobnie użyłbym rozwiązania w odpowiedzi @ fixagon .
Jednak mimo że struktura Decimal nie ma metody uzyskiwania liczby miejsc dziesiętnych, można wywołać Decimal.GetBits, aby wyodrębnić reprezentację binarną, a następnie użyć wartości całkowitej i skali, aby obliczyć liczbę miejsc dziesiętnych.
Prawdopodobnie byłoby to szybsze niż formatowanie jako ciąg, chociaż musiałbyś przetwarzać bardzo dużo liczb dziesiętnych, aby zauważyć różnicę.
Zostawię wdrożenie jako ćwiczenie.
źródło
Jedno z najlepszych rozwiązań w celu znalezienia liczby cyfr po przecinku jest pokazane w poście burn_LEGION .
Tutaj używam części z artykułu na forum STSdb: Liczba cyfr po przecinku .
W MSDN możemy przeczytać następujące wyjaśnienie:
„Liczba dziesiętna to wartość zmiennoprzecinkowa, która składa się ze znaku, wartości liczbowej, w której każda cyfra w wartości mieści się w zakresie od 0 do 9, oraz współczynnika skalowania, który wskazuje pozycję zmiennoprzecinkowego przecinka, który oddziela całkę i ułamek części wartości liczbowej. "
I również:
„Binarna reprezentacja wartości Decimal składa się ze znaku 1-bitowego, 96-bitowej liczby całkowitej i współczynnika skalowania używanego do dzielenia 96-bitowej liczby całkowitej i określania, która jej część jest ułamkiem dziesiętnym. Współczynnik skalowania to domyślnie liczba 10, podniesiona do wykładnika z zakresu od 0 do 28. ”
Na poziomie wewnętrznym wartość dziesiętna jest reprezentowana przez cztery wartości całkowite.
Istnieje publicznie dostępna funkcja GetBits do uzyskiwania reprezentacji wewnętrznej. Funkcja zwraca tablicę int []:
[__DynamicallyInvokable] public static int[] GetBits(decimal d) { return new int[] { d.lo, d.mid, d.hi, d.flags }; }
Czwarty element zwróconej tablicy zawiera współczynnik skali i znak. Jak podaje MSDN, współczynnik skalowania jest niejawnie liczbą 10, podniesioną do wykładnika z zakresu od 0 do 28. Dokładnie tego potrzebujemy.
Zatem na podstawie wszystkich powyższych badań możemy skonstruować naszą metodę:
private const int SIGN_MASK = ~Int32.MinValue; public static int GetDigits4(decimal value) { return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16; }
Tutaj SIGN_MASK jest używany do ignorowania znaku. Po logicznym i przesunęliśmy również wynik o 16 bitów w prawo, aby otrzymać rzeczywisty współczynnik skali. Ostatecznie ta wartość wskazuje liczbę cyfr po przecinku.
Zauważ, że tutaj MSDN mówi również, że współczynnik skalowania zachowuje również wszelkie końcowe zera w liczbie dziesiętnej. Końcowe zera nie wpływają na wartość liczby Decimal w operacjach arytmetycznych lub porównawczych. Jednak końcowe zera mogą zostać ujawnione przez metodę ToString, jeśli zostanie zastosowany odpowiedni ciąg formatu.
To rozwiązanie wygląda jak najlepsze, ale czekaj, jest więcej. Uzyskując dostęp do metod prywatnych w C # , możemy użyć wyrażeń do zbudowania bezpośredniego dostępu do pola flag i uniknięcia konstruowania tablicy int:
public delegate int GetDigitsDelegate(ref Decimal value); public class DecimalHelper { public static readonly DecimalHelper Instance = new DecimalHelper(); public readonly GetDigitsDelegate GetDigits; public readonly Expression<GetDigitsDelegate> GetDigitsLambda; public DecimalHelper() { GetDigitsLambda = CreateGetDigitsMethod(); GetDigits = GetDigitsLambda.Compile(); } private Expression<GetDigitsDelegate> CreateGetDigitsMethod() { var value = Expression.Parameter(typeof(Decimal).MakeByRefType(), "value"); var digits = Expression.RightShift( Expression.And(Expression.Field(value, "flags"), Expression.Constant(~Int32.MinValue, typeof(int))), Expression.Constant(16, typeof(int))); //return (value.flags & ~Int32.MinValue) >> 16 return Expression.Lambda<GetDigitsDelegate>(digits, value); } }
Ten skompilowany kod jest przypisany do pola GetDigits. Zauważ, że funkcja otrzymuje wartość dziesiętną jako ref, więc nie jest wykonywane żadne kopiowanie - tylko odniesienie do wartości. Korzystanie z funkcji GetDigits z DecimalHelper jest łatwe:
decimal value = 3.14159m; int digits = DecimalHelper.Instance.GetDigits(ref value);
Jest to najszybsza możliwa metoda uzyskania liczby cyfr po przecinku dla wartości dziesiętnych.
źródło
0.01
i0.010
są dokładnie równe numery . Co więcej, pomysł, że numeryczny typ danych ma pewną semantyczną „liczbę użytych cyfr”, na którym można polegać, jest całkowicie błędny (nie należy go mylić z „dozwoloną liczbą cyfr”. Nie należy mylić prezentacji (wyświetlanie wartość liczby w określonej bazie, na przykład dziesiętne rozszerzenie wartości wskazanej przez rozszerzenie binarne 111) o wartość bazową! Powtórzmy, liczby nie są cyframi ani nie składają się z cyfr .Poleganie na wewnętrznej reprezentacji liczb dziesiętnych nie jest fajne.
Co powiesz na to:
int CountDecimalDigits(decimal n) { return n.ToString(System.Globalization.CultureInfo.InvariantCulture) //.TrimEnd('0') uncomment if you don't want to count trailing zeroes .SkipWhile(c => c != '.') .Skip(1) .Count(); }
źródło
możesz użyć InvariantCulture
string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);
inną możliwością byłoby zrobienie czegoś takiego:
private int GetDecimals(decimal d, int i = 0) { decimal multiplied = (decimal)((double)d * Math.Pow(10, i)); if (Math.Round(multiplied) == multiplied) return i; return GetDecimals(d, i+1); }
źródło
.
.Większość ludzi wydaje się nie zdawać sobie sprawy, że liczba dziesiętna uważa końcowe zera za istotne dla przechowywania i drukowania.
Tak więc 0,1 m, 0,10 m i 0,100 m można porównać jako równe, są one przechowywane w różny sposób (odpowiednio jako wartość / skala 1/1, 10/2 i 100/3) i zostaną wydrukowane odpowiednio jako 0,1, 0,10 i 0,100 , przez
ToString()
.W związku z tym rozwiązania, które zgłaszają „zbyt dużą dokładność”, w rzeczywistości podają właściwą dokładność, na
decimal
warunkach.Ponadto rozwiązania oparte na matematyce (takie jak mnożenie przez potęgi 10) będą prawdopodobnie bardzo powolne (liczba dziesiętna jest ~ 40 razy wolniejsza niż podwójna dla arytmetyki i nie chcesz też mieszać liczb zmiennoprzecinkowych, ponieważ prawdopodobnie wprowadzi to nieprecyzyjność ). Podobnie, rzutowanie do
int
lublong
jako środek obcinania jest podatne na błędy (decimal
ma znacznie większy zakres niż którykolwiek z nich - opiera się na 96-bitowej liczbie całkowitej).Chociaż nie są one eleganckie, prawdopodobnie jednym z najszybszych sposobów uzyskania dokładności (gdy są zdefiniowane jako „miejsca dziesiętne bez końcowych zer”) będą prawdopodobnie następujące sposoby:
public static int PrecisionOf(decimal d) { var text = d.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0'); var decpoint = text.IndexOf('.'); if (decpoint < 0) return 0; return text.Length - decpoint - 1; }
Niezmienna kultura gwarantuje znak „.” jako kropka dziesiętna, końcowe zera są obcinane, a wtedy jest tylko kwestia sprawdzenia, ile pozycji pozostało po przecinku (jeśli jest nawet jedna).
Edycja: zmieniono typ powrotu na int
źródło
A oto inny sposób, użyj typu SqlDecimal, który ma właściwość scale z liczbą cyfr na prawo od miejsca dziesiętnego. Rzutuj wartość dziesiętną na SqlDecimal, a następnie uzyskaj dostęp do skali.
((SqlDecimal)(decimal)yourValue).Scale
źródło
GetBytes
więc przydziela tablicę Byte zamiast uzyskiwania dostępu do bajtów w niebezpiecznym kontekście. W kodzie referencyjnym znajduje się nawet notatka i zakomentowany kod, stwierdzający, że zamiast tego mogą to zrobić. Dlaczego tego nie zrobili, jest dla mnie tajemnicą. Trzymałbym się z daleka i miałbym bezpośredni dostęp do bitów skali, zamiast ukrywać GC Alloc w tej obsadzie, ponieważ nie jest zbyt oczywiste, co robi pod maską.Jak dotąd prawie wszystkie wymienione rozwiązania alokują pamięć GC, co jest bardzo C # sposobem robienia rzeczy, ale dalekie od ideału w środowiskach o krytycznym znaczeniu dla wydajności. (Te, które nie przydzielają pętli użytkowania, a także nie uwzględniają zer końcowych).
Aby uniknąć alokacji GC, możesz po prostu uzyskać dostęp do bitów skalowania w niebezpiecznym kontekście. To może brzmieć krucho, ale zgodnie ze źródłem odniesienia firmy Microsoft , układ dziesiętny jest sekwencyjny, a nawet zawiera komentarz, aby nie zmieniać kolejności pól:
// NOTE: Do not change the order in which these fields are declared. The // native methods in this class rely on this particular order. private int flags; private int hi; private int lo; private int mid;
Jak widać, pierwszy int to pole flag. Z dokumentacji i jak wspomniano w innych komentarzach tutaj, wiemy, że tylko bity z przedziału 16-24 kodują skalę i że musimy unikać 31. bitu, który koduje znak. Ponieważ int ma rozmiar 4 bajtów, możemy to bezpiecznie zrobić:
internal static class DecimalExtensions { public static byte GetScale(this decimal value) { unsafe { byte* v = (byte*)&value; return v[2]; } } }
Powinno to być najbardziej wydajne rozwiązanie, ponieważ nie ma alokacji GC tablicy bajtów ani konwersji ToString. Przetestowałem to z .Net 4.x i .Net 3.5 w Unity 2019.1. Jeśli są jakieś wersje, w których to się nie powiedzie, daj mi znać.
Edytować:
Podziękowania dla @Zastai za przypomnienie mi o możliwości użycia jawnego układu struktury, aby praktycznie osiągnąć tę samą logikę wskaźnika poza niebezpiecznym kodem:
[StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { const byte k_SignBit = 1 << 7; [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public readonly uint Flags; [FieldOffset(0)] public readonly ushort Reserved; [FieldOffset(2)] byte m_Scale; public byte Scale { get { return m_Scale; } set { if(value > 28) throw new System.ArgumentOutOfRangeException("value", "Scale can't be bigger than 28!") m_Scale = value; } } [FieldOffset(3)] byte m_SignByte; public int Sign { get { return m_SignByte > 0 ? -1 : 1; } } public bool Positive { get { return (m_SignByte & k_SignBit) > 0 ; } set { m_SignByte = value ? (byte)0 : k_SignBit; } } [FieldOffset(4)] public uint Hi; [FieldOffset(8)] public uint Lo; [FieldOffset(12)] public uint Mid; public DecimalHelper(decimal value) : this() { Value = value; } public static implicit operator DecimalHelper(decimal value) { return new DecimalHelper(value); } public static implicit operator decimal(DecimalHelper value) { return value.Value; } }
Aby rozwiązać ten problem, oryginalny, można znieść wszystkie pola oprócz
Value
aScale
, ale może to może być przydatne dla kogoś, aby je wszystkie.źródło
[StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public uint Flags; [FieldOffset(0)] public ushort Reserved; [FieldOffset(2)] public byte Scale; [FieldOffset(3)] public DecimalSign Sign; [FieldOffset(4)] public uint ValuePart1; [FieldOffset(8)] public ulong ValuePart2; }
const decimal Foo = 1.0000000000000000000000000000m;
to podzielenie liczby dziesiętnej przez to przeskaluje ją do najniższej możliwej skali (tj. Nie będzie już zawierała końcowych zer dziesiętnych). Nie testowałem tego, aby sprawdzić, czy jest to szybsze niż podejście oparte na ciągach, które zasugerowałem gdzie indziej.Napisałem wczoraj zwięzłą, małą metodę, która zwraca również liczbę miejsc po przecinku bez konieczności polegania na jakichkolwiek podziałach ciągów lub kulturach, co jest idealne:
public int GetDecimalPlaces(decimal decimalNumber) { // try { // PRESERVE:BEGIN int decimalPlaces = 1; decimal powers = 10.0m; if (decimalNumber > 0.0m) { while ((decimalNumber * powers) % 1 != 0.0m) { powers *= 10.0m; ++decimalPlaces; } } return decimalPlaces;
źródło
19.0 should return 1
. To rozwiązanie zawsze zakłada minimalną ilość 1 miejsca po przecinku i ignoruje zera końcowe. dziesiętny może mieć takie, ponieważ używa współczynnika skali. Dostęp do współczynnika skalowania można uzyskać tak jak w bajtach 16-24 elementu o indeksie 3 w tablicy pobranej zDecimal.GetBytes()
lub za pomocą logiki wskaźników.Używam czegoś bardzo podobnego do odpowiedzi Clementa:
private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = true) { string stemp = Convert.ToString(number); if (trimTrailingZeros) stemp = stemp.TrimEnd('0'); return stemp.Length - 1 - stemp.IndexOf( Application.CurrentCulture.NumberFormat.NumberDecimalSeparator); }
Pamiętaj, aby użyć System.Windows.Forms, aby uzyskać dostęp do Application.CurrentCulture
źródło
Możesz spróbować:
int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length;
źródło
[1]
W moim kodzie używam następującego mechanizmu
public static int GetDecimalLength(string tempValue) { int decimalLength = 0; if (tempValue.Contains('.') || tempValue.Contains(',')) { char[] separator = new char[] { '.', ',' }; string[] tempstring = tempValue.Split(separator); decimalLength = tempstring[1].Length; } return decimalLength; }
wejście dziesiętne = 3,376; var instring = input.ToString ();
wywołanie GetDecimalLength (instring)
źródło
Korzystając z rekurencji, możesz:
private int GetDecimals(decimal n, int decimals = 0) { return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals; }
źródło
19.0 should return 1
. To rozwiązanie zignoruje końcowe zera. dziesiętny może mieć takie, ponieważ używa współczynnika skali. Dostęp do współczynnika skalowania można uzyskać jak w bajtach 16–24 elementu o indeksie 3 wDecimal.GetBytes()
tablicy lub za pomocą logiki wskaźników.string number = "123.456789"; // Convert to string int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
źródło
Proponuję skorzystać z tej metody:
public static int GetNumberOfDecimalPlaces(decimal value, int maxNumber) { if (maxNumber == 0) return 0; if (maxNumber > 28) maxNumber = 28; bool isEqual = false; int placeCount = maxNumber; while (placeCount > 0) { decimal vl = Math.Round(value, placeCount - 1); decimal vh = Math.Round(value, placeCount); isEqual = (vl == vh); if (isEqual == false) break; placeCount--; } return Math.Min(placeCount, maxNumber); }
źródło
Jako metoda rozszerzenia dziesiętnego uwzględniająca:
public static class DecimalExtensions { public static int GetNumberDecimalPlaces(this decimal source) { var parts = source.ToString(CultureInfo.InvariantCulture).Split('.'); if (parts.Length < 2) return 0; return parts[1].TrimEnd('0').Length; } }
źródło