Znajdź liczbę miejsc dziesiętnych w wartości dziesiętnej niezależnie od kultury

91

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.

Jesse Carter
źródło
Ułamek dziesiętny, jak w tytule pytania
Jesse Carter,
Co powiesz na dopasowanie wzorców przed Splitem? Zasadniczo \ d + (\ D) \ d +, gdzie \ D zwraca separator (., Itd.)
Anshul,
7
To nie jest pytanie zamknięte, ponieważ na pierwszy rzut oka może się pojawić rumieniec. Prośba 19.0o zwrot 1to szczegół implementacyjny dotyczący wewnętrznego przechowywania wartości 19.0. Faktem jest, że program może przechowywać to jako 190×10⁻¹lub 1900×10⁻²lub 19000×10⁻³. Wszystkie są równe. Fakt, że używa pierwszej reprezentacji, gdy ma wartość 19.0Mi jest to ujawniane, gdy używa się go ToStringbez 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.
ErikE
Jeśli chcesz typ, który może udźwignąć „liczba miejsc po przecinku używane”, gdy jest tworzony tak, że można wiarygodnie odróżnić 19Mod 19.0Mod 19.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ść.
ErikE
1
Nawet jeśli klasa Decimal „odróżnia” 19 m od 19,0 m od 19,00 m? Znaczące cyfry są jak jeden z głównych przypadków użycia. Co to jest 19,0 m * 1,0 m? Wydaje się, że mówi 19.00 m, może jednak programiści C # źle robią matematykę: P? Znów cyfry znaczące są prawdziwe. Jeśli nie lubisz cyfr znaczących, prawdopodobnie nie powinieneś używać klasy Decimal.
Nicholi

Odpowiedzi:

168

Skorzystałem ze sposobu Joe, aby rozwiązać ten problem :)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
palenie_LEGION
źródło
6
Po dokładniejszym przyjrzeniu się temu i zobaczeniu w działaniu zaznaczam to jako odpowiedź, ponieważ jest to moim zdaniem najbardziej zwięzła i elegancka metoda zwracania miejsc dziesiętnych, jaką tu widziałem. Dałbym +1, gdybym mógł: D
Jesse Carter
9
decimalzachowuje 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];
Burning_LEGION
3
To nie zadziałało dla mnie. Wartość pochodząca z SQL to 21,17, czyli 4 cyfry. Typ danych jest zdefiniowany jako DECIMAL (12,4), więc być może to wszystko (przy użyciu Entity Framework).
PeterX
11
@Nicholi - Nie, jest to wyjątkowo złe, ponieważ metoda polega na umieszczeniu podstawowych bitów ułamka dziesiętnego - coś, co ma wiele sposobów reprezentowania tej samej liczby . Nie przetestowałbyś klasy na podstawie stanu jej pól prywatnych, prawda?
m.edmondson
14
Nie jestem pewien, co powinno być w tym eleganckie lub miłe. To jest tak zaciemnione, jak to tylko możliwe. Kto wie, czy to działa we wszystkich przypadkach. Nie można się upewnić.
usr
24

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
}
GY
źródło
6
Twoje rozwiązanie nie powiedzie się w wielu przypadkach, które zawierają końcowe zera, a cyfry są ZNACZĄCE. 0,01 m * 2,0 m = 0,020 m. Powinna mieć 3 cyfry, metoda zwraca 2. Wydaje się, że niepoprawnie rozumiesz, co się dzieje, gdy rzucasz 0,01f na Decimal. Punkty zmiennoprzecinkowe z natury nie są dokładne, więc rzeczywista wartość binarna przechowywana dla 0,01f nie jest dokładna. Kiedy rzucasz na Decimal (bardzo uporządkowany zapis liczbowy), możesz nie uzyskać 0,01 m (faktycznie dostajesz 0,010 m). Rozwiązanie GetBits jest w rzeczywistości poprawne do pobierania liczby cyfr z Decimal. Kluczem jest sposób konwersji na dziesiętny.
Nicholi
2
@ Nicholi 0,020 m jest równe 0,02 m. Zera końcowe nie są znaczące. OP pyta w tytule „niezależnie od kultury”, a jeszcze dokładniej wyjaśnia „… które będzie bezpieczne w użyciu w różnych kulturach info…” - dlatego uważam, że moja odpowiedź jest jeszcze bardziej aktualna niż inne.
GY
6
OP powiedział konkretnie: „19.0 powinno zwrócić 1”. Ten kod nie działa w tym przypadku.
daniloquio,
9
może nie tego chciał OP, ale ta odpowiedź lepiej odpowiada moim potrzebom niż najlepsza odpowiedź na to pytanie
Arsen Zahray
2
Pierwsze dwie linie należy zastąpić, n = n % 1; if (n < 0) n = -n;ponieważ wartość większa niż int.MaxValuespowoduje OverflowExceptionnp 2147483648.12345.
Nienawidzenie
23

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.

Joe
źródło
1
Dzięki @Joe, to naprawdę fajny sposób podejścia do tego. W zależności od tego, co szef myśli o korzystaniu z innego rozwiązania, przyjrzę się realizacji Twojego pomysłu. Zdecydowanie byłoby fajnym ćwiczeniem :)
Jesse Carter
17

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.

Dziesiętna reprezentacja wewnętrzna

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.

Kristiyan Dimitrov
źródło
3
dziesiętny r = (dziesiętny) -0,01f; i rozwiązanie zawodzi. (na wszystkie odpowiedzi, które widziałem na tej stronie ...) :)
GY
5
UWAGA: Jeśli chodzi o całość (Decimal) 0.01f, rzutujesz zmiennoprzecinkowy, z natury NIEPRECYZYJNY, na coś bardzo strukturalnego, jak Decimal. Spójrz na wynik działania Console.WriteLine ((Decimal) 0.01f). Dziesiętna tworzona w rzutowaniu RZECZYWISTA ma 3 cyfry, dlatego wszystkie podane rozwiązania mówią o 3 zamiast 2. Wszystko działa zgodnie z oczekiwaniami, „problem” polega na tym, że oczekujesz, że wartości zmiennoprzecinkowe będą dokładne. Oni nie są.
Nicholi
@Nicholi Twój punkt awarii, gdy zdajesz sobie sprawę, że 0.01i 0.010są 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 .
ErikE
6
Mają równoważną wartość, ale nie zawierają cyfr znaczących. Co jest dużym przypadkiem użycia klasy Decimal. Gdybym zapytał, ile cyfr ma dosłowne 0,010 m, powiedziałbyś, że tylko 2? Nawet jeśli dziesiątki nauczycieli matematyki / przedmiotów ścisłych na całym świecie powie Ci, że końcowe 0 jest znaczące? Problem, do którego się odnosimy, objawia się rzutowaniem z punktów zmiennoprzecinkowych na liczbę dziesiętną. Nie użycie samego GetBits, które robi dokładnie to, co zostało udokumentowane. Jeśli nie przejmujesz się znaczącymi cyframi, to tak, masz problem i prawdopodobnie nie powinieneś używać klasy Decimal w pierwszej kolejności.
Nicholi
1
@theberserker O ile dobrze pamiętam, nie było żadnego haczyka - powinno działać w obie strony.
Kristiyan Dimitrov
13

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();
    }
Łaskawy
źródło
11

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);
}
fixagon
źródło
W jaki sposób pomaga mi to znaleźć liczbę miejsc po przecinku? Nie mam problemu z konwersją ułamka dziesiętnego na ciąg, który jest dobry w każdej kulturze. Zgodnie z pytaniem próbuję znaleźć liczbę miejsc dziesiętnych, które były po przecinku
Jesse Carter
@JesseCarter: Oznacza to, że zawsze możesz się podzielić ..
Austin Salonen
@AustinSalonen Naprawdę? Nie wiedziałem, że użycie InvariantCulture wymusi użycie kropki jako separatora dziesiętnego
Jesse Carter
tak jak wcześniej, cena zawsze będzie rzutowana na łańcuch z rozszerzeniem. jako separator dziesiętny. ale moim zdaniem nie jest to najbardziej elegancki sposób ...
fixagon
@JesseCarter: NumberFormatInfo.NumberDecimalSeparator
Austin Salonen
8

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 decimalwarunkach.

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 intlub longjako środek obcinania jest podatne na błędy ( decimalma 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

Zastai
źródło
1
@mvmorten Nie jestem pewien, dlaczego uważasz, że konieczna jest zmiana typu zwracanego wyniku na int; bajt dokładniej reprezentuje zwracaną wartość: bez znaku i mały zakres (w praktyce 0-29).
Zastai
1
Zgadzam się, że rozwiązania iteracyjne i oparte na obliczeniach są powolne (poza tym, że nie uwzględniają zer końcowych). Jednak przydzielenie ciągu do tego i operowanie na tamtym również nie jest najbardziej wydajną rzeczą do zrobienia, szczególnie w kontekstach krytycznych dla wydajności i przy powolnym GC. Dostęp do wagi za pomocą logiki wskaźnika jest dużo szybszy i nie wymaga przydziału.
Martin Tilo Schmitz
Tak, uzyskanie skali można wykonać znacznie wydajniej - ale obejmowałoby to końcowe zera. A ich usunięcie wymaga wykonania arytmetyki na części całkowitej.
Zastai
6

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
RitchieD
źródło
1
Patrząc na kod referencyjny firmy Microsoft , rzutowanie na SqlDecimal wewnętrznie używa metody, GetByteswię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ą.
Martin Tilo Schmitz
4

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 Valuea Scale, ale może to może być przydatne dla kogoś, aby je wszystkie.

Martin Tilo Schmitz
źródło
1
Możesz również uniknąć niebezpiecznego kodu, kodując własną strukturę z jawnym układem - umieść przecinek na pozycji 0, a następnie bajty / int w odpowiednich miejscach. Coś jak:[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; }
Zastai
Dzięki @Zastai, dobra uwaga. Ja również zastosowałem to podejście. :)
Martin Tilo Schmitz
1
Jedna uwaga: ustawienie skali poza zakresem 0-28 powoduje pęknięcie. ToString () zwykle działa, ale arytmetyka zawodzi.
Zastai
Jeszcze raz dziękuję @Zastai, dodałem za to czek :)
Martin Tilo Schmitz
Inna sprawa: kilka osób tutaj nie chciało brać pod uwagę końcowych zer dziesiętnych. Jeśli zdefiniujesz a, 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.
Zastai
2

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;
Jesse Carter
źródło
@ fix-like-codings podobne do twojej drugiej odpowiedzi, chociaż w przypadku czegoś takiego wolę podejście iteracyjne, a nie rekursję
Jesse Carter
Oryginalny post stwierdza, że: 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 z Decimal.GetBytes()lub za pomocą logiki wskaźników.
Martin Tilo Schmitz
2

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

RooiWillie
źródło
1

Możesz spróbować:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
NicoRiff
źródło
7
Czy to się nie powiedzie, gdy ułamek dziesiętny jest liczbą całkowitą? [1]
Silvermind
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)

Srikanth
źródło
1
Dla mnie to nie działa, ponieważ reprezentacja ToString () wartości dziesiętnej dodaje „00” na końcu moich danych - używam typu danych Decimal (12,4) z SQL Server.
PeterX
Czy możesz rzutować swoje dane na dziesiętny typ C # i wypróbować rozwiązanie. Dla mnie, gdy używam Tostring () na wartości dziesiętnej c #, nigdy nie widzę „00”.
Srikanth
1

Korzystając z rekurencji, możesz:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
Mars
źródło
Oryginalny post stwierdza, że: 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 w Decimal.GetBytes()tablicy lub za pomocą logiki wskaźników.
Martin Tilo Schmitz
1
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6
Eva Chang
źródło
0

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); 
    }
Veysel Ozdemir
źródło
0

Jako metoda rozszerzenia dziesiętnego uwzględniająca:

  • Różne kultury
  • Wszystkie liczby
  • Liczby ujemne
  • Końcowe ustawione zera w miejscu dziesiętnym (np. 1,2300 M zwróci 2, a nie 4)
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;
    }
}
bytedev
źródło