Finden Sie die Anzahl der Dezimalstellen im Dezimalwert, unabhängig von der Kultur


90

Ich frage mich, ob es eine präzise und genaue Möglichkeit gibt, die Anzahl der Dezimalstellen in einem Dezimalwert (als int) herauszuholen, die für verschiedene Kulturinformationen sicher verwendet werden kann.

Beispiel:
19.0 sollte 1 zurückgeben,
27.5999 sollte 4 zurückgeben,
19.12 sollte 2 zurückgeben
usw.

Ich habe eine Abfrage geschrieben, bei der eine Zeichenfolge auf einen Punkt aufgeteilt wurde, um Dezimalstellen zu finden:

int priceDecimalPlaces = price.ToString().Split('.').Count() > 1 
                  ? price.ToString().Split('.').ToList().ElementAt(1).Length 
                  : 0;

Mir fällt jedoch ein, dass dies nur in Regionen funktioniert, in denen das '.' als Dezimaltrennzeichen und ist daher systemübergreifend sehr spröde.


Eine Dezimalstelle gemäß dem Fragentitel
Jesse Carter

Wie wäre es mit einem Mustervergleich vor dem Teilen? Grundsätzlich \ d + (\ D) \ d + wobei \ D das Trennzeichen (.
Usw.

7
Dies ist keine geschlossene Frage, da sie auf den ersten Blick erröten kann. Die Aufforderung 19.0zur Rückgabe 1ist ein Implementierungsdetail in Bezug auf die interne Speicherung des Werts 19.0. Tatsache ist, dass es für das Programm absolut legitim ist, dies als 190×10⁻¹oder 1900×10⁻²oder zu speichern 19000×10⁻³. Alle diese sind gleich. Die Tatsache, dass die erste Darstellung verwendet wird, wenn ein Wert von angegeben wird, 19.0Mund dies bei Verwendung ToStringohne Formatbezeichner angezeigt wird, ist nur ein Zufall und eine glückliche Sache. Außer es ist nicht glücklich, wenn sich Menschen in Fällen, in denen sie es nicht sollten, auf den Exponenten verlassen.
ErikE

Wenn Sie eine Art wollen , die „Anzahl der Dezimalstellen verwendet“ führen kann , wenn es erstellt wird, so dass Sie zuverlässig unterscheiden 19Mvon 19.0Maus 19.00M, müssen Sie eine neue Klasse erstellen , die den zugrunde liegenden Wert als eine Eigenschaft bündelt und die Anzahl der Dezimalstellen als weitere Eigenschaft.
ErikE

1
Obwohl die Dezimalklasse 19 m "unterscheiden" kann, von 19,0 m von 19,00 m? Signifikante Ziffern sind wie einer der Hauptanwendungsfälle. Was ist 19,0 m * 1,0 m? Scheint 19.00m zu sagen, vielleicht machen die C # -Entwickler Mathe falsch: P? Auch hier sind signifikante Ziffern eine echte Sache. Wenn Sie keine signifikanten Ziffern mögen, sollten Sie wahrscheinlich nicht die Dezimalklasse verwenden.
Nicholi

Antworten:


166

Ich habe Joes Weg benutzt , um dieses Problem zu lösen :)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];

6
Nachdem ich mir das genauer angesehen und in Aktion gesehen habe, markiere ich es als Antwort, da dies meiner Meinung nach die prägnanteste und eleganteste Methode ist, Dezimalstellen zurückzugeben, die ich hier gesehen habe. Würde wieder +1, wenn ich könnte: D
Jesse Carter

9
decimalhält die Ziffer nach dem Koma, deshalb finden Sie dieses "Problem", Sie müssen die Dezimalzahl in double und erneut in decimal umwandeln, um das Problem zu beheben: BitConverter.GetBytes (decimal.GetBits ((decimal) (double) argument) [3]) [ 2];
Burning_LEGION

3
Das hat bei mir nicht funktioniert. Der von SQL zurückkommende Wert ist 21,17 und bedeutet 4 Ziffern. Der Datentyp ist als DECIMAL (12,4) definiert. Vielleicht ist es das (unter Verwendung von Entity Framework).
PeterX

10
@Nicholi - Nein, das ist außergewöhnlich schlecht, da die Methode auf der Platzierung der zugrunde liegenden Bits der Dezimalstelle beruht - etwas, das viele Möglichkeiten hat , dieselbe Zahl darzustellen . Sie würden eine Klasse nicht anhand des Status ihrer privaten Felder testen, oder?
m.edmondson

14
Ich bin mir nicht sicher, was daran elegant oder nett sein soll. Dies ist ungefähr so ​​verschleiert wie es nur geht. Wer weiß, ob es überhaupt in allen Fällen funktioniert. Unmöglich, um sicher zu gehen.
usr

23

Da keine der Antworten gut genug für die in Dezimalzahlen konvertierte magische Zahl "-0.01f" war, GetDecimal((decimal)-0.01f);
kann ich nur davon ausgehen, dass vor 3 Jahren ein kolossaler Mind-Fart-Virus alle angegriffen hat :)
Hier scheint etwas zu funktionieren Implementierung dieses bösen und monströsen Problems, des sehr komplizierten Problems des Zählens der Dezimalstellen nach dem Punkt - keine Zeichenfolgen, keine Kulturen, keine Notwendigkeit, die Bits zu zählen und keine Notwendigkeit, Mathematikforen zu lesen. Nur einfache Mathematik der 3. Klasse.

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
}

6
Ihre Lösung schlägt in einer Reihe von Fällen fehl, die nachgestellte Nullen enthalten und deren Ziffern BEDEUTEND sind. 0,01 m · 2,0 m = 0,020 m. Sollte es sich um 3 Ziffern handeln, gibt Ihre Methode 2 zurück. Sie scheinen falsch zu verstehen, was passiert, wenn Sie 0,01f in Dezimal umwandeln. Gleitkommazahlen sind von Natur aus nicht genau, daher ist der für 0,01f gespeicherte tatsächliche Binärwert nicht genau. Wenn Sie auf Dezimal setzen (eine sehr strukturierte Notation), erhalten Sie möglicherweise nicht 0,01 m (Sie erhalten tatsächlich 0,010 m). Die GetBits-Lösung ist tatsächlich korrekt, um die Anzahl der Stellen von einer Dezimalstelle zu erhalten. Wie Sie in Dezimal konvertieren, ist der Schlüssel.
Nicholi

2
@Nicholi 0,020 m entspricht 0,02 m. Nachgestellte Nullen sind nicht signifikant. OP fragt im Titel nach "unabhängig von der Kultur" und erklärt noch genauer "..das wird sicher für verschiedene Kulturinformationen verwendet werden .." - daher denke ich, dass meine Antwort noch gültiger bleibt als andere.
GY

6
OP sagte ausdrücklich: "19.0 sollte 1 zurückgeben". Dieser Code schlägt in diesem Fall fehl.
Daniloquio

9
Vielleicht ist dies nicht das, was das OP wollte, aber diese Antwort passt besser zu meinen Bedürfnissen als die beste Antwort auf diese Frage
Arsen Zahray

2
Die ersten beiden Zeilen sollten durch ersetzt werden, n = n % 1; if (n < 0) n = -n;da ein Wert, der größer als int.MaxValueist OverflowException, z 2147483648.12345.
Abscheu

22

Ich würde die Lösung wahrscheinlich in der Antwort von @ fixagon verwenden .

Während die Dezimalstruktur keine Methode zum Abrufen der Anzahl der Dezimalstellen hat, können Sie Decimal.GetBits aufrufen , um die Binärdarstellung zu extrahieren, und dann den ganzzahligen Wert und die Skalierung verwenden, um die Anzahl der Dezimalstellen zu berechnen.

Dies wäre wahrscheinlich schneller als das Formatieren als Zeichenfolge, obwohl Sie eine Menge Dezimalstellen verarbeiten müssten, um den Unterschied zu bemerken.

Ich werde die Implementierung als Übung verlassen.


1
Danke @Joe, das ist eine wirklich nette Art, sich dem anzunähern. Je nachdem, wie mein Chef die andere Lösung verwendet, werde ich Ihre Idee umsetzen. Wäre definitiv eine lustige Übung :)
Jesse Carter

17

Eine der besten Lösungen zum Ermitteln der Anzahl der Stellen nach dem Dezimalpunkt finden Sie im Beitrag von burning_LEGION .

Hier verwende ich Teile aus einem STSdb-Forumsartikel: Anzahl der Nachkommastellen .

In MSDN können wir die folgende Erklärung lesen:

"Eine Dezimalzahl ist ein Gleitkommawert, der aus einem Vorzeichen, einem numerischen Wert, bei dem jede Ziffer im Wert zwischen 0 und 9 liegt, und einem Skalierungsfaktor besteht, der die Position eines Gleitkommapunkts angibt, der das Integral und den Bruch trennt Teile des numerischen Wertes. "

Und auch:

"Die binäre Darstellung eines Dezimalwerts besteht aus einem 1-Bit-Vorzeichen, einer 96-Bit-Ganzzahl und einem Skalierungsfaktor, mit dem die 96-Bit-Ganzzahl geteilt und angegeben wird, welcher Teil davon ein Dezimalbruch ist. Der Skalierungsfaktor ist implizit die Zahl 10, angehoben auf einen Exponenten im Bereich von 0 bis 28. "

Auf interner Ebene wird der Dezimalwert durch vier ganzzahlige Werte dargestellt.

Dezimale interne Darstellung

Es gibt eine öffentlich verfügbare GetBits-Funktion zum Abrufen der internen Darstellung. Die Funktion gibt ein int [] Array zurück:

[__DynamicallyInvokable] 
public static int[] GetBits(decimal d)
{
    return new int[] { d.lo, d.mid, d.hi, d.flags };
}

Das vierte Element des zurückgegebenen Arrays enthält einen Skalierungsfaktor und ein Vorzeichen. Und wie der MSDN sagt, ist der Skalierungsfaktor implizit die Zahl 10, die auf einen Exponenten zwischen 0 und 28 angehoben wird. Genau das brauchen wir.

Basierend auf allen obigen Untersuchungen können wir also unsere Methode konstruieren:

private const int SIGN_MASK = ~Int32.MinValue;

public static int GetDigits4(decimal value)
{
    return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16;
}

Hier wird ein SIGN_MASK verwendet, um das Vorzeichen zu ignorieren. Nach logisch und wir haben auch das Ergebnis mit 16 Bit nach rechts verschoben, um den tatsächlichen Skalierungsfaktor zu erhalten. Dieser Wert gibt schließlich die Anzahl der Stellen nach dem Dezimalpunkt an.

Beachten Sie, dass MSDN hier auch sagt, dass der Skalierungsfaktor auch alle nachgestellten Nullen in einer Dezimalzahl beibehält. Nachgestellte Nullen wirken sich nicht auf den Wert einer Dezimalzahl in Arithmetik- oder Vergleichsoperationen aus. Nachfolgende Nullen werden jedoch möglicherweise von der ToString-Methode angezeigt, wenn eine geeignete Formatzeichenfolge angewendet wird.

Diese Lösung sieht aus wie die beste, aber warten Sie, es gibt noch mehr. Durch den Zugriff auf private Methoden in C # können wir Ausdrücke verwenden, um einen direkten Zugriff auf das Flag-Feld zu erstellen und die Erstellung des int-Arrays zu vermeiden:

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);
    }
}

Dieser kompilierte Code wird dem Feld GetDigits zugewiesen. Beachten Sie, dass die Funktion den Dezimalwert als ref empfängt, sodass kein tatsächliches Kopieren durchgeführt wird - nur ein Verweis auf den Wert. Die Verwendung der GetDigits-Funktion von DecimalHelper ist einfach:

decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);

Dies ist die schnellstmögliche Methode, um die Anzahl der Stellen nach dem Dezimalpunkt für Dezimalwerte zu ermitteln.


3
dezimal r = (dezimal) -0,01f; und Lösung schlägt fehl. (auf alle Antworten, die ich auf dieser Seite gesehen habe ...) :)
GY

5
HINWEIS: In Bezug auf die gesamte (Dezimal-) 0,01f-Sache setzen Sie einen Gleitkommawert, der von Natur aus NICHT PRÄZIS ist, auf etwas sehr Strukturiertes wie eine Dezimalstelle. Sehen Sie sich die Ausgabe von Console.WriteLine ((Decimal) 0.01f) an. Die Dezimalstelle, die in der Besetzung tatsächlich gebildet wird, hat 3 Ziffern. Deshalb sagen alle angebotenen Lösungen 3 statt 2. Alles funktioniert tatsächlich wie erwartet. Das "Problem" ist, dass Sie erwarten, dass Gleitkommawerte genau sind. Sie sind nicht.
Nicholi

@Nicholi Dein Punkt schlägt fehl, wenn du das erkennst 0.01und 0.010genau die gleichen Zahlen sind . Darüber hinaus ist die Idee, dass ein numerischer Datentyp eine Art Semantik "Anzahl der verwendeten Ziffern" enthält, auf die man sich verlassen kann, völlig falsch (nicht zu verwechseln mit "Anzahl der zulässigen Ziffern". Verwechseln Sie nicht die Darstellung (die Anzeige von Der Wert einer Zahl in einer bestimmten Basis, zum Beispiel die Dezimalerweiterung des durch die binäre Erweiterung 111) angegebenen Werts mit dem zugrunde liegenden Wert! Um es noch einmal zu wiederholen: Zahlen sind weder Ziffern noch bestehen sie aus Ziffern .
ErikE

6
Sie haben einen äquivalenten Wert, jedoch keine signifikanten Ziffern. Welches ist ein großer Anwendungsfall der Dezimalklasse. Wenn ich fragen würde, wie viele Ziffern in der wörtlichen 0,010 m sind, würden Sie nur 2 sagen? Auch wenn Dutzende von Mathematik- / Naturwissenschaftslehrern auf der ganzen Welt Ihnen sagen würden, dass die letzte 0 signifikant ist? Das Problem, auf das wir uns beziehen, manifestiert sich in der Umwandlung von Gleitkommawerten in Dezimalzahlen. Nicht die Verwendung von GetBits selbst, die genau so funktioniert, wie es dokumentiert ist. Wenn Sie sich nicht für signifikante Ziffern interessieren, haben Sie ein Problem und sollten die Dezimalklasse wahrscheinlich gar nicht erst verwenden.
Nicholi

1
@theberserker Soweit ich mich erinnere, gab es keinen Haken - es sollte in beide Richtungen funktionieren.
Kris

13

Sich auf die interne Darstellung von Dezimalstellen zu verlassen, ist nicht cool.

Wie wäre es damit:

    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();
    }

11

Sie können die InvariantCulture verwenden

string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);

Eine andere Möglichkeit wäre, so etwas zu tun:

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);
}

Wie hilft mir das, die Anzahl der Dezimalstellen in der Dezimalstelle zu finden? Ich habe kein Problem damit, die Dezimalstelle in eine Zeichenfolge umzuwandeln, die in jeder Kultur gut ist. Gemäß der Frage versuche ich, die Anzahl der Dezimalstellen zu finden, die auf der Dezimalstelle standen
Jesse Carter

@ JesseCarter: Das heißt, Sie können sich immer weiter teilen ..
Austin Salonen

@AustinSalonen Wirklich? Mir war nicht bewusst, dass die Verwendung von InvariantCulture die Verwendung eines Punkts als Dezimaltrennzeichen erzwingen würde
Jesse Carter,

Wie Sie zuvor getan haben, wird der Preis immer auf einen String mit a gesetzt. als Dezimaltrennzeichen. aber es ist meiner Meinung nach nicht der eleganteste Weg ...
Fixagon


8

Die meisten Leute hier scheinen nicht zu wissen, dass Dezimalstellen nachgestellte Nullen als wichtig für das Speichern und Drucken betrachten.

0,1 m, 0,10 m und 0,100 m können also als gleich verglichen werden, sie werden unterschiedlich gespeichert (als Wert / Skala 1/1, 10/2 bzw. 100/3) und als 0,1, 0,10 bzw. 0,100 gedruckt von ToString().

Daher melden die Lösungen, die eine "zu hohe Genauigkeit" melden, tatsächlich die richtige Genauigkeit zu decimalden Bedingungen.

Darüber hinaus sind mathematische Lösungen (wie das Multiplizieren mit Zehnerpotenzen) wahrscheinlich sehr langsam (die Dezimalzahl ist für die Arithmetik ~ 40x langsamer als das Doppelte, und Sie möchten auch kein Gleitkomma mischen, da dies wahrscheinlich zu Ungenauigkeiten führt ). In ähnlicher Weise ist das Umwandeln in intoder longals Mittel zum Abschneiden fehleranfällig ( decimalhat eine viel größere Reichweite als beide - es basiert auf einer 96-Bit-Ganzzahl).

Obwohl dies als solches nicht elegant ist, ist Folgendes wahrscheinlich einer der schnellsten Wege, um die Genauigkeit zu ermitteln (wenn es als "Dezimalstellen ohne nachgestellte Nullen" definiert ist):

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;
}

Die invariante Kultur garantiert ein '.' Als Dezimalpunkt werden nachgestellte Nullen abgeschnitten, und dann muss nur noch geprüft werden, wie viele Positionen nach dem Dezimalpunkt verbleiben (falls es überhaupt eine gibt).

Bearbeiten: Rückgabetyp in int geändert


1
@mvmorten Sie sind sich nicht sicher, warum Sie den Rückgabetyp in int ändern mussten. Byte stellt den zurückgegebenen Wert genauer dar: vorzeichenloser und kleiner Bereich (in der Praxis 0-29).
Zastai

1
Ich bin damit einverstanden, dass iterative und rechnerbasierte Lösungen langsam sind (abgesehen davon, dass nachgestellte Nullen nicht berücksichtigt werden). Das Zuweisen einer Zeichenfolge und das Bearbeiten dieser Zeichenfolge ist jedoch auch nicht die leistungsfähigste Aufgabe, insbesondere in leistungskritischen Kontexten und mit einem langsamen GC. Der Zugriff auf die Skala über die Zeigerlogik ist viel schneller und frei von Zuordnungen.
Martin Tilo Schmitz

Ja, die Skalierung kann viel effizienter durchgeführt werden - dies würde jedoch die nachgestellten Nullen einschließen. Um sie zu entfernen, muss der ganzzahlige Teil arithmetisch ausgeführt werden.
Zastai

6

Und hier ist eine andere Möglichkeit: Verwenden Sie den Typ SqlDecimal, der eine Skalierungseigenschaft mit der Anzahl der Stellen rechts von der Dezimalstelle hat. Wandeln Sie Ihren Dezimalwert in SqlDecimal um und greifen Sie dann auf Scale zu.

((SqlDecimal)(decimal)yourValue).Scale

1
Beim Betrachten des Microsoft-Referenzcodes wird beim internen Casting in SqlDecimal das verwendet, GetBytessodass das Byte-Array zugewiesen wird, anstatt in einem unsicheren Kontext auf die Bytes zuzugreifen. Es gibt sogar einen Hinweis und einen auskommentierten Code im Referenzcode, der besagt, wie und wie sie das stattdessen tun könnten. Warum sie es nicht taten, ist mir ein Rätsel. Ich würde mich davon fernhalten und direkt auf die Skalenbits zugreifen, anstatt das GC-Allok in dieser Besetzung zu verbergen, da es einfach nicht sehr offensichtlich ist, was es unter der Haube tut.
Martin Tilo Schmitz

4

Bisher weisen fast alle aufgelisteten Lösungen GC-Speicher zu. Dies ist zwar die C # -Methode, aber in leistungskritischen Umgebungen alles andere als ideal. (Diejenigen, die keine Use-Schleifen zuweisen und auch keine nachgestellten Nullen berücksichtigen.)

Um GC-Zuordnungen zu vermeiden, können Sie einfach in einem unsicheren Kontext auf die Skalierungsbits zugreifen. Das mag fragil klingen, aber laut Microsoft-Referenzquelle ist das Strukturlayout der Dezimalzahl sequentiell und enthält sogar einen Kommentar, um die Reihenfolge der Felder nicht zu ändern:

    // 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;

Wie Sie sehen können, ist das erste int hier das Flag-Feld. Aus der Dokumentation und wie in anderen Kommentaren hier erwähnt, wissen wir, dass nur die Bits von 16-24 die Skala codieren und dass wir das 31. Bit vermeiden müssen, das das Vorzeichen codiert. Da int die Größe von 4 Bytes hat, können wir dies sicher tun:

internal static class DecimalExtensions
{
  public static byte GetScale(this decimal value)
  {
    unsafe
    {
      byte* v = (byte*)&value;
      return v[2];
    }
  }
}

Dies sollte die leistungsstärkste Lösung sein, da keine GC-Zuordnung des Byte-Arrays oder der ToString-Konvertierungen vorhanden ist. Ich habe es gegen .Net 4.x und .Net 3.5 in Unity 2019.1 getestet. Wenn es Versionen gibt, bei denen dies fehlschlägt, lassen Sie es mich bitte wissen.

Bearbeiten:

Vielen Dank an @Zastai, der mich an die Möglichkeit erinnert hat, ein explizites Strukturlayout zu verwenden, um praktisch dieselbe Zeigerlogik außerhalb von unsicherem Code zu erreichen:

[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;
    }
}

Um das ursprüngliche Problem zu lösen, könnten Sie alle Felder abstreifen neben Valueund Scaleaber vielleicht könnte es nützlich sein , für jemanden zu haben , sie alle.


1
Sie können unsicheren Code auch vermeiden, indem Sie Ihre eigene Struktur mit explizitem Layout codieren. Setzen Sie eine Dezimalstelle an Position 0 und dann Bytes / Ints an den entsprechenden Stellen. So etwas wie:[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

Danke @Zastai, guter Punkt. Ich habe diesen Ansatz auch übernommen. :)
Martin Tilo Schmitz

1
Eines ist zu beachten: Wenn Sie die Skala außerhalb des Bereichs von 0 bis 28 einstellen, kommt es zu einem Bruch. ToString () funktioniert normalerweise, aber die Arithmetik schlägt fehl.
Zastai

Nochmals vielen Dank @Zastai, ich habe einen Scheck dafür hinzugefügt :)
Martin Tilo Schmitz

Eine andere Sache: Einige Leute hier wollten nachgestellte Dezimalstellen nicht berücksichtigen. Wenn Sie a definieren und const decimal Foo = 1.0000000000000000000000000000m;dann eine Dezimalstelle durch diese dividieren, wird sie auf die niedrigstmögliche Skala skaliert (dh keine nachgestellten Dezimalstellen mehr enthalten). Ich habe dies nicht verglichen, um festzustellen, ob es schneller ist als der stringbasierte Ansatz, den ich an anderer Stelle vorgeschlagen habe.
Zastai

2

Ich habe gestern eine prägnante kleine Methode geschrieben, die auch die Anzahl der Dezimalstellen zurückgibt, ohne sich auf String-Splits oder Kulturen verlassen zu müssen, was ideal ist:

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;

@ fix-like-codings ähnlich Ihrer zweiten Antwort, obwohl ich für so etwas den iterativen Ansatz bevorzuge, anstatt Rekursion zu verwenden
Jesse Carter

Der ursprüngliche Beitrag besagt : 19.0 should return 1. Diese Lösung nimmt immer eine minimale Menge von 1 Dezimalstelle an und ignoriert nachfolgende Nullen. Dezimal kann solche haben, da es einen Skalierungsfaktor verwendet. Auf den Skalierungsfaktor kann wie in den Bytes 16-24 des Elements mit dem Index 3 im Array zugegriffen werden, das von Decimal.GetBytes()oder unter Verwendung der Zeigerlogik erhalten wurde.
Martin Tilo Schmitz

2

Ich verwende etwas, das Clements Antwort sehr ähnlich ist:

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);
}

Denken Sie daran, System.Windows.Forms zu verwenden, um Zugriff auf Application.CurrentCulture zu erhalten


1

Du kannst es versuchen:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;

7
Würde dies nicht fehlschlagen, wenn die Dezimalstelle eine ganze Zahl ist? [1]
Silvermind

1

Ich verwende den folgenden Mechanismus in meinem Code

  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;
    }

Dezimaleingabe = 3,376; var instring = input.ToString ();

Rufen Sie GetDecimalLength (instring) auf.


1
Dies funktioniert bei mir nicht, da die ToString () - Darstellung des Dezimalwerts "00" am Ende meiner Daten hinzufügt - ich verwende einen Dezimal (12,4) -Datentyp von SQL Server.
PeterX

Können Sie Ihre Daten in eine Dezimalzahl vom Typ c # umwandeln und die Lösung ausprobieren? Wenn ich Tostring () für einen c # -Dezimalwert verwende, sehe ich nie eine "00".
Srikanth

1

Mit der Rekursion können Sie Folgendes tun:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}

Der ursprüngliche Beitrag besagt : 19.0 should return 1. Diese Lösung ignoriert nachgestellte Nullen. Dezimal kann solche haben, da es einen Skalierungsfaktor verwendet. Auf den Skalierungsfaktor kann wie in den Bytes 16-24 des Elements mit Index 3 im Decimal.GetBytes()Array oder mithilfe der Zeigerlogik zugegriffen werden.
Martin Tilo Schmitz

1
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6

0

Ich schlage vor, diese Methode zu verwenden:

    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); 
    }

0

Als Dezimalerweiterungsmethode, die Folgendes berücksichtigt:

  • Unterschiedliche Kulturen
  • Ganze Zahlen
  • Negative Zahlen
  • Nachgestellte Nullen auf der Dezimalstelle (z. B. 1,2300M geben 2 statt 4 zurück)
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;
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.