假设我们有一个 System.Decimal 数字。
为了便于说明,我们以 ToString() 表示如下:
d.ToString() = "123.4500"
关于这个 Decimal 可以说如下。出于我们的目的,比例被定义为小数点右侧的位数。有效标度类似,但忽略小数部分中出现的任何尾随零。 (换句话说,这些参数的定义类似于 SQL 小数加上一些附加参数来解释小数部分尾随零的 System.Decimal 概念。)
给定一个任意的 System.Decimal,如何有效地计算所有这四个参数,而不需要转换为字符串并检查字符串?该解决方案可能需要 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
(?) 或者将这些精度解释为零也可以。
是的,您需要使用
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();
}
}
当我需要在将十进制值写入数据库之前验证精度和小数位数时,我看到了这篇文章。实际上,我想出了一种使用 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);
}
使用 ToString 比 Jon Skeet 的解决方案快大约 10 倍。虽然这相当快,但这里的挑战(如果有任何接受者!)是击败 ToString 的性能。
我从以下测试程序中得到的性能结果是: 显示信息 239 毫秒 FastInfo 25 毫秒
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);
}
}
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) 数据类型的 Oracle 精度和小数位数定义兼容:
https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209
问候。
我目前确实有类似的问题,但我不仅需要小数位数,还需要尾数作为整数。 根据上面的解决方案,请在下面找到我能想到的最快的解决方案。 统计数据: “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),
};
}
}
}
}
从 .NET 7 开始,
Decimal
类型公开 Scale
属性。
所以你可以这样做:
var number = 123.4500m;
Console.WriteLine(number.Scale); // returns 4