假设我们有一个System.Decimal数字。
为了说明,让我们采用其ToString()表示如下:
d.ToString() = "123.4500"
关于这个十进制可以说如下。对于我们这里的目的,比例定义为小数点右边的位数。有效比例相似但忽略了小数部分中出现的任何尾随零。 (换句话说,这些参数定义为SQL小数加上一些额外的参数,以说明小数部分中尾随零的System.Decimal概念。)
给定一个任意的System.Decimal,如何有效地计算所有这四个参数而不转换为String并检查String?该解决方案可能需要Decimal.GetBits。
更多例子:
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
(?)或者将这些精度解释为零就可以了。
答案 0 :(得分:26)
是的,您需要使用Decimal.GetBits
。不幸的是,你必须使用96位整数,并且.NET中没有简单的整数类型可以处理96位。另一方面,您可以使用Decimal
本身......
这里有一些代码与您的示例产生相同的数字。希望你觉得它很有用:)
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();
}
}
答案 1 :(得分:23)
当我需要在将十进制值写入数据库之前验证精度和比例时,我遇到了这篇文章。我实际上想出了一种不同的方法来实现这一点,使用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);
}
答案 2 :(得分:10)
使用ToString比Jon Skeet的解决方案快约10倍。虽然速度相当快,但这里面临的挑战(如果有任何参与者!)是要击败ToString的性能。
我从以下测试程序中获得的性能结果如下: 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);
}
}
答案 3 :(得分:0)
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;
}
}
我的解决方案兼容NUMBER(p,s)DataType的Oracle精度和比例定义:
https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209
的问候。
答案 4 :(得分:0)
我目前有类似的问题,但我不仅需要比例,还需要将mantisse作为整数。 根据上述解决方案,请找到最快的,我可以在下面找到。 统计: 在我的机器上,“ViaBits”需要2,000毫秒才能进行7,000,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),
};
}
}
}
}