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.
c#
decimal
cultureinfo
Jesse Carter
quelle
quelle
19.0
zur Rückgabe1
ist ein Implementierungsdetail in Bezug auf die interne Speicherung des Werts19.0
. Tatsache ist, dass es für das Programm absolut legitim ist, dies als190×10⁻¹
oder1900×10⁻²
oder zu speichern19000×10⁻³
. Alle diese sind gleich. Die Tatsache, dass die erste Darstellung verwendet wird, wenn ein Wert von angegeben wird,19.0M
und dies bei VerwendungToString
ohne 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.19M
von19.0M
aus19.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.Antworten:
Ich habe Joes Weg benutzt , um dieses Problem zu lösen :)
quelle
decimal
hä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];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.
quelle
n = n % 1; if (n < 0) n = -n;
da ein Wert, der größer alsint.MaxValue
istOverflowException
, z2147483648.12345
.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.
quelle
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.
Es gibt eine öffentlich verfügbare GetBits-Funktion zum Abrufen der internen Darstellung. Die Funktion gibt ein int [] Array zurück:
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:
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:
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:
Dies ist die schnellstmögliche Methode, um die Anzahl der Stellen nach dem Dezimalpunkt für Dezimalwerte zu ermitteln.
quelle
0.01
und0.010
genau 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 .Sich auf die interne Darstellung von Dezimalstellen zu verlassen, ist nicht cool.
Wie wäre es damit:
quelle
Sie können die InvariantCulture verwenden
Eine andere Möglichkeit wäre, so etwas zu tun:
quelle
.
.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
decimal
den 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
int
oderlong
als Mittel zum Abschneiden fehleranfällig (decimal
hat 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):
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
quelle
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.
quelle
GetBytes
sodass 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.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:
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:
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:
Um das ursprüngliche Problem zu lösen, könnten Sie alle Felder abstreifen neben
Value
undScale
aber vielleicht könnte es nützlich sein , für jemanden zu haben , sie alle.quelle
[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;
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.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:
quelle
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 vonDecimal.GetBytes()
oder unter Verwendung der Zeigerlogik erhalten wurde.Ich verwende etwas, das Clements Antwort sehr ähnlich ist:
Denken Sie daran, System.Windows.Forms zu verwenden, um Zugriff auf Application.CurrentCulture zu erhalten
quelle
Du kannst es versuchen:
quelle
[1]
Ich verwende den folgenden Mechanismus in meinem Code
Dezimaleingabe = 3,376; var instring = input.ToString ();
Rufen Sie GetDecimalLength (instring) auf.
quelle
Mit der Rekursion können Sie Folgendes tun:
quelle
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 imDecimal.GetBytes()
Array oder mithilfe der Zeigerlogik zugegriffen werden.quelle
Ich schlage vor, diese Methode zu verwenden:
quelle
Als Dezimalerweiterungsmethode, die Folgendes berücksichtigt:
quelle