Saya ingin tahu apakah ada cara yang ringkas dan akurat untuk menarik jumlah tempat desimal dalam nilai desimal (sebagai int) yang akan aman digunakan di berbagai info budaya?
Misalnya:
19.0 harus
menghasilkan 1, 27.5999 harus menghasilkan 4,
19.12 harus mengembalikan 2,
dll.
Saya menulis kueri yang memisahkan string pada suatu titik untuk menemukan tempat desimal:
int priceDecimalPlaces = price.ToString().Split('.').Count() > 1
? price.ToString().Split('.').ToList().ElementAt(1).Length
: 0;
Tetapi menurut saya ini hanya akan berfungsi di wilayah yang menggunakan '.' sebagai pemisah desimal dan karenanya sangat rapuh di berbagai sistem.
c#
decimal
cultureinfo
Jesse Carter
sumber
sumber
19.0
pengembalian1
adalah detail implementasi terkait penyimpanan internal nilai19.0
. Faktanya adalah bahwa program menyimpan ini sebagai190×10⁻¹
atau1900×10⁻²
atau19000×10⁻³
. Semuanya sama. Fakta bahwa ia menggunakan representasi pertama saat diberi nilai19.0M
dan ini terpapar saat menggunakanToString
tanpa penentu format hanyalah kebetulan, dan hal yang menyenangkan. Kecuali itu tidak senang ketika orang mengandalkan eksponen dalam kasus di mana mereka seharusnya tidak melakukannya.19M
dari19.0M
dari19.00M
, Anda harus membuat kelas baru yang bundel nilai yang mendasari sebagai salah satu properti dan jumlah tempat desimal sebagai properti lain.Jawaban:
Saya menggunakan cara Joe untuk mengatasi masalah ini :)
decimal argument = 123.456m; int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
sumber
decimal
terus menghitung digit setelah koma, itulah mengapa Anda menemukan "masalah" ini, Anda harus mengubah desimal menjadi dua kali lipat dan menjadi desimal lagi untuk memperbaiki: BitConverter.GetBytes (decimal.GetBits ((desimal) (double) argumen) [3]) [ 2];Karena tidak ada jawaban yang diberikan cukup baik untuk angka ajaib "-0.01f" diubah menjadi desimal .. yaitu:
GetDecimal((decimal)-0.01f);
Saya hanya dapat berasumsi bahwa virus kentut pikiran kolosal menyerang semua orang 3 tahun yang lalu :)
Inilah yang tampaknya berhasil implementasi untuk masalah jahat dan mengerikan ini, masalah yang sangat rumit menghitung tempat desimal setelah titik - tanpa string, tidak ada budaya, tidak perlu menghitung bit dan tidak perlu membaca forum matematika .. hanya matematika kelas 3 sederhana.
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 }
sumber
n = n % 1; if (n < 0) n = -n;
karena nilai yang lebih besar dariint.MaxValue
akan menyebabkanOverflowException
, misalnya2147483648.12345
.Saya mungkin akan menggunakan solusi dalam jawaban @ fixagon .
Namun, meskipun struct Desimal tidak memiliki metode untuk mendapatkan jumlah desimal, Anda dapat memanggil Decimal.GetBits untuk mengekstrak representasi biner, lalu menggunakan nilai dan skala integer untuk menghitung jumlah desimal.
Ini mungkin akan lebih cepat daripada memformat sebagai string, meskipun Anda harus memproses banyak sekali desimal untuk melihat perbedaannya.
Saya akan meninggalkan implementasinya sebagai latihan.
sumber
Salah satu solusi terbaik untuk menemukan jumlah digit setelah koma desimal ditunjukkan dalam posting burning_LEGION .
Di sini saya menggunakan bagian-bagian dari artikel forum STSdb: Jumlah digit setelah titik desimal .
Di MSDN kita bisa membaca penjelasan berikut:
"Angka desimal adalah nilai floating-point yang terdiri dari tanda, nilai numerik di mana setiap digit dalam nilai berkisar dari 0 hingga 9, dan faktor skala yang menunjukkan posisi titik desimal mengambang yang memisahkan integral dan pecahan bagian dari nilai numerik. "
Dan juga:
"Representasi biner dari nilai Desimal terdiri dari tanda 1-bit, bilangan bulat 96-bit, dan faktor skala yang digunakan untuk membagi bilangan bulat 96-bit dan menentukan bagian mana yang merupakan pecahan desimal. Faktor penskalaannya adalah secara implisit angka 10, dipangkatkan ke eksponen mulai dari 0 hingga 28. "
Pada tingkat internal, nilai desimal diwakili oleh empat nilai integer.
Ada fungsi GetBits yang tersedia untuk umum untuk mendapatkan representasi internal. Fungsi mengembalikan larik int []:
[__DynamicallyInvokable] public static int[] GetBits(decimal d) { return new int[] { d.lo, d.mid, d.hi, d.flags }; }
Elemen keempat dari array yang dikembalikan berisi faktor skala dan tanda. Dan seperti yang dikatakan MSDN, faktor penskalaan secara implisit adalah angka 10, dipangkatkan ke eksponen yang berkisar dari 0 hingga 28. Inilah yang kita butuhkan.
Jadi, berdasarkan semua investigasi di atas, kami dapat membangun metode kami:
private const int SIGN_MASK = ~Int32.MinValue; public static int GetDigits4(decimal value) { return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16; }
Di sini SIGN_MASK digunakan untuk mengabaikan tanda. Setelah logis dan kami juga menggeser hasilnya dengan 16 bit ke kanan untuk menerima faktor skala yang sebenarnya. Nilai ini, akhirnya, menunjukkan jumlah digit setelah koma desimal.
Perhatikan bahwa di sini MSDN juga mengatakan faktor penskalaan juga mempertahankan angka nol di belakangnya dalam bilangan Desimal. Nol tertinggal tidak mempengaruhi nilai bilangan desimal dalam operasi aritmatika atau perbandingan. Namun, nol di belakang mungkin akan ditampilkan oleh metode ToString jika string format yang sesuai diterapkan.
Solusi ini sepertinya yang terbaik, tapi tunggu, masih ada lagi. Dengan mengakses metode privat di C # kita bisa menggunakan ekspresi untuk membangun akses langsung ke bidang bendera dan menghindari membangun larik 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); } }
Kode yang dikompilasi ini ditugaskan ke bidang GetDigits. Perhatikan bahwa fungsi tersebut menerima nilai desimal sebagai ref, jadi tidak ada penyalinan aktual yang dilakukan - hanya referensi ke nilai tersebut. Menggunakan fungsi GetDigits dari DecimalHelper itu mudah:
decimal value = 3.14159m; int digits = DecimalHelper.Instance.GetDigits(ref value);
Ini adalah metode tercepat yang mungkin untuk mendapatkan jumlah digit setelah koma desimal untuk nilai desimal.
sumber
0.01
dan0.010
adalah angka yang sama persis . Selain itu, gagasan bahwa tipe data numerik memiliki semantik "jumlah digit yang digunakan" yang dapat diandalkan sepenuhnya salah (jangan disamakan dengan "jumlah digit yang diperbolehkan". Jangan membingungkan penyajian (tampilan nilai angka dalam basis tertentu, misalnya, perluasan desimal dari nilai yang ditunjukkan oleh ekspansi biner 111) dengan nilai yang mendasarinya! Untuk mengulangi, angka bukanlah digit, juga bukan terdiri dari digit .Mengandalkan representasi internal desimal tidaklah keren.
Bagaimana dengan ini:
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(); }
sumber
Anda dapat menggunakan InvariantCulture
string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);
kemungkinan lain adalah melakukan sesuatu seperti itu:
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); }
sumber
.
.Kebanyakan orang di sini tampaknya tidak menyadari bahwa desimal menganggap nol di belakang sebagai hal yang penting untuk penyimpanan dan pencetakan.
Jadi 0,1m, 0,10m dan 0,100m dapat dibandingkan sebagai sama, mereka disimpan secara berbeda (sebagai nilai / skala 1/1, 10/2 dan 100/3, masing-masing), dan akan dicetak sebagai 0,1, 0,10 dan 0,100, masing-masing , oleh
ToString()
.Dengan demikian, solusi yang melaporkan "presisi terlalu tinggi" sebenarnya melaporkan presisi yang benar , sesuai
decimal
ketentuan.Selain itu, solusi berbasis matematika (seperti mengalikan dengan pangkat 10) kemungkinan akan sangat lambat (desimal ~ 40x lebih lambat dari dua kali lipat untuk aritmatika, dan Anda juga tidak ingin mencampurkan dalam floating-point karena hal itu kemungkinan akan menyebabkan ketidaktepatan ). Demikian pula, mentransmisikan ke
int
ataulong
sebagai alat pemotongan rawan kesalahan (decimal
memiliki rentang yang jauh lebih besar daripada keduanya - ini didasarkan pada bilangan bulat 96-bit).Meskipun tidak elegan seperti itu, berikut ini kemungkinan akan menjadi salah satu cara tercepat untuk mendapatkan ketepatan (bila didefinisikan sebagai "tempat desimal tidak termasuk nol di belakangnya"):
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; }
Budaya invarian menjamin '.' sebagai titik desimal, nol di belakangnya dipangkas, dan kemudian ini hanya masalah melihat berapa banyak posisi yang tersisa setelah titik desimal (jika ada satu).
Edit: mengubah tipe pengembalian menjadi int
sumber
Dan inilah cara lain, gunakan tipe SqlDecimal yang memiliki properti skala dengan jumlah digit di kanan desimal. Transmisikan nilai desimal Anda ke SqlDecimal dan kemudian akses Scale.
((SqlDecimal)(decimal)yourValue).Scale
sumber
GetBytes
sehingga mengalokasikan larik Byte alih-alih mengakses byte dalam konteks yang tidak aman. Bahkan ada catatan dan kode komentar di kode referensi, yang menyatakan itu dan bagaimana mereka bisa melakukannya. Mengapa mereka tidak melakukannya adalah misteri bagi saya. Saya akan menghindari ini dan mengakses bit skala secara langsung daripada menyembunyikan Aloc GC dalam pemeran ini, karena tidak terlalu jelas apa yang dilakukannya di bawah tenda.Sejauh ini, hampir semua solusi yang terdaftar mengalokasikan Memori GC, yang merupakan cara C # untuk melakukan sesuatu tetapi jauh dari ideal dalam lingkungan kinerja kritis. (Yang tidak mengalokasikan loop penggunaan dan juga tidak mempertimbangkan nol di belakangnya.)
Jadi untuk menghindari GC Allocs, Anda cukup mengakses bit skala dalam konteks yang tidak aman. Itu mungkin terdengar rapuh tetapi sesuai sumber referensi Microsoft , tata letak struct desimal adalah Berurutan dan bahkan memiliki komentar di sana, bukan untuk mengubah urutan bidang:
// 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;
Seperti yang Anda lihat, int pertama di sini adalah bidang bendera. Dari dokumentasi dan seperti yang disebutkan dalam komentar lain di sini, kita tahu bahwa hanya bit dari 16-24 yang menyandikan skala dan bahwa kita perlu menghindari bit ke-31 yang menyandikan tanda. Karena int berukuran 4 byte, kita dapat melakukan ini dengan aman:
internal static class DecimalExtensions { public static byte GetScale(this decimal value) { unsafe { byte* v = (byte*)&value; return v[2]; } } }
Ini harus menjadi solusi yang paling berkinerja karena tidak ada alokasi GC dari larik byte atau konversi ToString. Saya telah mengujinya terhadap .Net 4.x dan .Net 3.5 di Unity 2019.1. Jika ada versi yang gagal, beri tahu saya.
Edit:
Terima kasih kepada @Zastai karena telah mengingatkan saya tentang kemungkinan menggunakan tata letak struct eksplisit untuk secara praktis mencapai logika penunjuk yang sama di luar kode yang tidak aman:
[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; } }
Untuk mengatasi masalah asli, Anda dapat menghapus semua bidang selain
Value
danScale
mungkin berguna bagi seseorang untuk memiliki semuanya.sumber
[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;
maka membagi desimal dengan yang akan mengubah skala ke skala serendah mungkin (yaitu tidak lagi menyertakan nol desimal di belakangnya). Saya belum membandingkan ini untuk melihat apakah itu lebih cepat daripada pendekatan berbasis string yang saya sarankan di tempat lain.Saya menulis metode kecil singkat kemarin yang juga mengembalikan jumlah tempat desimal tanpa harus bergantung pada pemisahan string atau budaya yang ideal:
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;
sumber
19.0 should return 1
. Solusi ini akan selalu mengasumsikan jumlah minimal 1 tempat desimal dan mengabaikan nol di belakangnya. desimal dapat memiliki itu karena menggunakan faktor skala. Faktor skala dapat diakses seperti pada byte 16-24 dari elemen dengan indeks 3 dalam larik yang didapat dariDecimal.GetBytes()
atau dengan menggunakan logika penunjuk.Saya menggunakan sesuatu yang sangat mirip dengan jawaban Clement:
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); }
Ingatlah untuk menggunakan System.Windows.Forms untuk mendapatkan akses ke Application.CurrentCulture
sumber
Anda dapat mencoba:
int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length;
sumber
[1]
Saya menggunakan mekanisme berikut dalam kode saya
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; }
masukan desimal = 3,376; var instring = input.ToString ();
panggil GetDecimalLength (instring)
sumber
Menggunakan rekursi, Anda dapat melakukan:
private int GetDecimals(decimal n, int decimals = 0) { return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals; }
sumber
19.0 should return 1
. Solusi ini akan mengabaikan nol di belakangnya. desimal dapat memiliki itu karena menggunakan faktor skala. Faktor skala dapat diakses seperti pada byte 16-24 dari elemen dengan indeks 3 dalamDecimal.GetBytes()
larik atau dengan menggunakan logika penunjuk.string number = "123.456789"; // Convert to string int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
sumber
Saya sarankan menggunakan metode ini:
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); }
sumber
Sebagai metode ekstensi desimal yang memperhitungkan:
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; } }
sumber