.net 计算 System.Decimal 精度和小数位数
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/763942/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Calculate System.Decimal Precision and Scale
提问by Jason Kresowaty
Suppose that we have a System.Decimal number.
假设我们有一个 System.Decimal 数。
For illustration, let's take one whose ToString() representation is as follows:
为了说明,让我们举一个其 ToString() 表示如下:
d.ToString() = "123.4500"
The following can be said about this Decimal. For our purposes here, scale is defined as the number of digits to the right of the decimal point. Effective scale is similar but ignores any trailing zeros that occur in the fractional part. (In other words, these parameters are defined like SQL decimals plus some additional parameters to account for the System.Decimal concept of trailing zeros in the fractional part.)
关于这个十进制可以说如下。出于我们的目的,比例定义为小数点右侧的位数。有效比例类似,但忽略小数部分中出现的任何尾随零。(换句话说,这些参数的定义类似于 SQL 小数加上一些附加参数,以说明小数部分中尾随零的 System.Decimal 概念。)
- Precision: 7
- Scale: 4
- EffectivePrecision: 5
- EffectiveScale: 2
- 精度:7
- 规模:4
- 有效精度:5
- 有效等级:2
Given an arbitrary System.Decimal, how can I compute all four of these parameters efficiently and without converting to a String and examining the String? The solution probably requires Decimal.GetBits.
给定一个任意 System.Decimal,如何有效地计算所有这四个参数而不转换为字符串并检查字符串?该解决方案可能需要 Decimal.GetBits。
Some more examples:
还有一些例子:
Examples Precision Scale EffectivePrecision EffectiveScale
0 1 (?) 0 1 (?) 0
0.0 2 (?) 1 1 (?) 0
12.45 4 2 4 2
12.4500 6 4 4 2
770 3 0 3 0
(?) Alternatively interpreting these precisions as zero would be fine.
(?) 或者将这些精度解释为零也可以。
回答by Jon Skeet
Yes, you'd need to use Decimal.GetBits. Unfortunately, you then have to work with a 96-bit integer, and there are no simple integer type in .NET which copes with 96 bits. On the other hand, it's possible that you could use Decimalitself...
是的,您需要使用Decimal.GetBits. 不幸的是,您必须使用 96 位整数,而 .NET 中没有处理 96 位的简单整数类型。另一方面,您可能可以使用Decimal它自己......
Here's some code which produces the same numbers as your examples. Hope you find it useful :)
这是一些生成与您的示例相同的数字的代码。希望你觉得它有用 :)
using System;
public class Test
{
static public void Main(string[] x)
{
ShowInfo(123.4500m);
ShowInfo(0m);
ShowInfo(0.0m);
ShowInfo(12.45m);
ShowInfo(12.4500m);
ShowInfo(770m);
}
static void ShowInfo(decimal dec)
{
// We want the integer parts as uint
// C# doesn't permit int[] to uint[] conversion,
// but .NET does. This is somewhat evil...
uint[] bits = (uint[])(object)decimal.GetBits(dec);
decimal mantissa =
(bits[2] * 4294967296m * 4294967296m) +
(bits[1] * 4294967296m) +
bits[0];
uint scale = (bits[3] >> 16) & 31;
// Precision: number of times we can divide
// by 10 before we get to 0
uint precision = 0;
if (dec != 0m)
{
for (decimal tmp = mantissa; tmp >= 1; tmp /= 10)
{
precision++;
}
}
else
{
// Handle zero differently. It's odd.
precision = scale + 1;
}
uint trailingZeros = 0;
for (decimal tmp = mantissa;
tmp % 10m == 0 && trailingZeros < scale;
tmp /= 10)
{
trailingZeros++;
}
Console.WriteLine("Example: {0}", dec);
Console.WriteLine("Precision: {0}", precision);
Console.WriteLine("Scale: {0}", scale);
Console.WriteLine("EffectivePrecision: {0}",
precision - trailingZeros);
Console.WriteLine("EffectiveScale: {0}", scale - trailingZeros);
Console.WriteLine();
}
}
回答by Ben Buck
I came across this article when I needed to validate precision and scale before writing a decimal value to a database. I had actually come up with a different way to achieve this using System.Data.SqlTypes.SqlDecimal which turned out to be faster that the other two methods discussed here.
当我需要在将十进制值写入数据库之前验证精度和比例时,我遇到了这篇文章。我实际上想出了一种不同的方法来使用 System.Data.SqlTypes.SqlDecimal 来实现这一点,结果证明它比此处讨论的其他两种方法更快。
static DecimalInfo SQLInfo(decimal dec)
{
System.Data.SqlTypes.SqlDecimal x;
x = new System.Data.SqlTypes.SqlDecimal(dec);
return new DecimalInfo((int)x.Precision, (int)x.Scale, (int)0);
}
回答by Jason Kresowaty
Using ToString is about 10x faster than Jon Skeet's solution. While this is reasonably fast, the challenge here (if there are any takers!) is to beat the performance of ToString.
使用 ToString 比 Jon Skeet 的解决方案快 10 倍左右。虽然这相当快,但这里的挑战(如果有接受者的话!)是击败 ToString 的性能。
The performance results I get from the following test program are: ShowInfo 239 ms FastInfo 25 ms
我从以下测试程序中得到的性能结果是: ShowInfo 239 ms FastInfo 25 ms
using System;
using System.Diagnostics;
using System.Globalization;
public class Test
{
static public void Main(string[] x)
{
Stopwatch sw1 = new Stopwatch();
Stopwatch sw2 = new Stopwatch();
sw1.Start();
for (int i = 0; i < 10000; i++)
{
ShowInfo(123.4500m);
ShowInfo(0m);
ShowInfo(0.0m);
ShowInfo(12.45m);
ShowInfo(12.4500m);
ShowInfo(770m);
}
sw1.Stop();
sw2.Start();
for (int i = 0; i < 10000; i++)
{
FastInfo(123.4500m);
FastInfo(0m);
FastInfo(0.0m);
FastInfo(12.45m);
FastInfo(12.4500m);
FastInfo(770m);
}
sw2.Stop();
Console.WriteLine(sw1.ElapsedMilliseconds);
Console.WriteLine(sw2.ElapsedMilliseconds);
Console.ReadLine();
}
// Be aware of how this method handles edge cases.
// A few are counterintuitive, like the 0.0 case.
// Also note that the goal is to report a precision
// and scale that can be used to store the number in
// an SQL DECIMAL type, so this does not correspond to
// how precision and scale are defined for scientific
// notation. The minimal precision SQL decimal can
// be calculated by subtracting TrailingZeros as follows:
// DECIMAL(Precision - TrailingZeros, Scale - TrailingZeros).
//
// dec Precision Scale TrailingZeros
// ------- --------- ----- -------------
// 0 1 0 0
// 0.0 2 1 1
// 0.1 1 1 0
// 0.01 2 2 0 [Diff result than ShowInfo]
// 0.010 3 3 1 [Diff result than ShowInfo]
// 12.45 4 2 0
// 12.4500 6 4 2
// 770 3 0 0
static DecimalInfo FastInfo(decimal dec)
{
string s = dec.ToString(CultureInfo.InvariantCulture);
int precision = 0;
int scale = 0;
int trailingZeros = 0;
bool inFraction = false;
bool nonZeroSeen = false;
foreach (char c in s)
{
if (inFraction)
{
if (c == '0')
trailingZeros++;
else
{
nonZeroSeen = true;
trailingZeros = 0;
}
precision++;
scale++;
}
else
{
if (c == '.')
{
inFraction = true;
}
else if (c != '-')
{
if (c != '0' || nonZeroSeen)
{
nonZeroSeen = true;
precision++;
}
}
}
}
// Handles cases where all digits are zeros.
if (!nonZeroSeen)
precision += 1;
return new DecimalInfo(precision, scale, trailingZeros);
}
struct DecimalInfo
{
public int Precision { get; private set; }
public int Scale { get; private set; }
public int TrailingZeros { get; private set; }
public DecimalInfo(int precision, int scale, int trailingZeros)
: this()
{
Precision = precision;
Scale = scale;
TrailingZeros = trailingZeros;
}
}
static DecimalInfo ShowInfo(decimal dec)
{
// We want the integer parts as uint
// C# doesn't permit int[] to uint[] conversion,
// but .NET does. This is somewhat evil...
uint[] bits = (uint[])(object)decimal.GetBits(dec);
decimal mantissa =
(bits[2] * 4294967296m * 4294967296m) +
(bits[1] * 4294967296m) +
bits[0];
uint scale = (bits[3] >> 16) & 31;
// Precision: number of times we can divide
// by 10 before we get to 0
uint precision = 0;
if (dec != 0m)
{
for (decimal tmp = mantissa; tmp >= 1; tmp /= 10)
{
precision++;
}
}
else
{
// Handle zero differently. It's odd.
precision = scale + 1;
}
uint trailingZeros = 0;
for (decimal tmp = mantissa;
tmp % 10m == 0 && trailingZeros < scale;
tmp /= 10)
{
trailingZeros++;
}
return new DecimalInfo((int)precision, (int)scale, (int)trailingZeros);
}
}
回答by user1785960
public static class DecimalExtensions
{
public static int GetPrecision(this decimal value)
{
return GetLeftNumberOfDigits(value) + GetRightNumberOfDigits(value);
}
public static int GetScale(this decimal value)
{
return GetRightNumberOfDigits(value);
}
/// <summary>
/// Number of digits to the right of the decimal point without ending zeros
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static int GetRightNumberOfDigits(this decimal value)
{
var text = value.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator);
if (decpoint < 0)
return 0;
return text.Length - decpoint - 1;
}
/// <summary>
/// Number of digits to the left of the decimal point without starting zeros
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static int GetLeftNumberOfDigits(this decimal value)
{
var text = Math.Abs(value).ToString(System.Globalization.CultureInfo.InvariantCulture).TrimStart('0');
var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator);
if (decpoint == -1)
return text.Length;
return decpoint;
}
}
My solution is compatible with Oracle precision and scale definition for NUMBER(p,s) DataType:
我的解决方案与 NUMBER(p,s) 数据类型的 Oracle 精度和比例定义兼容:
https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209
https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209
Regards.
问候。
回答by Andreas
I do currently have a similar issue, but I do not only need the scale, but also need the mantisse as integer. Based on the solutions above, please find the fastest, I could come up with, below. Statistics: "ViaBits" takes 2,000ms for 7,000,000 checks on my machine. "ViaString" takes 4,000ms for the same task.
我目前确实有类似的问题,但我不仅需要比例,还需要尾数作为整数。根据上面的解决方案,请在下面找到我能想到的最快的解决方案。统计数据:“ViaBits”在我的机器上进行 7,000,000 次检查需要 2,000 毫秒。“ViaString”执行相同任务需要 4,000 毫秒。
public class DecimalInfo {
public BigInteger Mantisse { get; private set; }
public SByte Scale { get; private set; }
private DecimalInfo() {
}
public static DecimalInfo Get(decimal d) {
//ViaBits is faster than ViaString.
return ViaBits(d);
}
public static DecimalInfo ViaBits(decimal d) {
//This is the fastest, I can come up with.
//Tested against the solutions from http://stackoverflow.com/questions/763942/calculate-system-decimal-precision-and-scale
if (d == 0) {
return new DecimalInfo() {
Mantisse = 0,
Scale = 0,
};
} else {
byte scale = (byte)((Decimal.GetBits(d)[3] >> 16) & 31);
//Calculating the mantisse from the bits 0-2 is slower.
if (scale > 0) {
if ((scale & 1) == 1) {
d *= 10m;
}
if ((scale & 2) == 2) {
d *= 100m;
}
if ((scale & 4) == 4) {
d *= 10000m;
}
if ((scale & 8) == 8) {
d *= 100000000m;
}
if ((scale & 16) == 16) {
d *= 10000000000000000m;
}
}
SByte realScale = (SByte)scale;
BigInteger scaled = (BigInteger)d;
//Just for bigger steps, seems reasonable.
while (scaled % 10000 == 0) {
scaled /= 10000;
realScale -= 4;
}
while (scaled % 10 == 0) {
scaled /= 10;
realScale--;
}
return new DecimalInfo() {
Mantisse = scaled,
Scale = realScale,
};
}
}
public static DecimalInfo ViaToString(decimal dec) {
if (dec == 0) {
return new DecimalInfo() {
Mantisse = 0,
Scale = 0,
};
} else {
//Is slower than "ViaBits".
string s = dec.ToString(CultureInfo.InvariantCulture);
int scale = 0;
int trailingZeros = 0;
bool inFraction = false;
foreach (char c in s) {
if (inFraction) {
if (c == '0') {
trailingZeros++;
} else {
trailingZeros = 0;
}
scale++;
} else {
if (c == '.') {
inFraction = true;
} else if (c != '-') {
if (c == '0'){
trailingZeros ++;
} else {
trailingZeros = 0;
}
}
}
}
if (inFraction) {
return new DecimalInfo() {
Mantisse = BigInteger.Parse(s.Replace(".", "").Substring(0, s.Length - trailingZeros - 1)),
Scale = (SByte)(scale - trailingZeros),
};
} else {
return new DecimalInfo() {
Mantisse = BigInteger.Parse(s.Substring(0, s.Length - trailingZeros)),
Scale = (SByte)(scale - trailingZeros),
};
}
}
}
}

