这是一个老问题,但许多答案在处理大数时表现不佳或溢出。我认为D.Nesterov的回答是最好的:鲁棒、简单且快速。我只想再添一点。
我尝试了使用
decimals并检查了
源代码。来自
public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)
构造函数文档。
十进制数的二进制表示由1位符号、96位整数和一个缩放因子组成,该缩放因子用于除以整数并指定其中哪一部分是小数分数。缩放因子隐式地是10的指数,范围从0到28。
知道这一点后,我的第一种方法是创建另一个具有与我想要舍弃的小数相对应的比例的
decimal
,然后截断它,最后创建一个具有所需比例的
decimal
。
private const int ScaleMask = 0x00FF0000;
public static Decimal Truncate(decimal target, byte decimalPlaces)
{
var bits = Decimal.GetBits(target);
var scale = (byte)((bits[3] & (ScaleMask)) >> 16);
if (scale <= decimalPlaces)
return target;
var temporalDecimal = new Decimal(bits[0], bits[1], bits[2], target < 0, (byte)(scale - decimalPlaces));
temporalDecimal = Math.Truncate(temporalDecimal);
bits = Decimal.GetBits(temporalDecimal);
return new Decimal(bits[0], bits[1], bits[2], target < 0, decimalPlaces);
}
这种方法不比D. Nesterov的方法更快,而且更加复杂,因此我进行了一些尝试。我的猜测是需要创建一个辅助的decimal
并两次检索位数使其变慢。在第二次尝试中,我自己操作了由Decimal.GetBits(Decimal d)方法返回的组件。思路是将组件除以10,直到需要的次数,并减小比例。代码基于(大量)参考Decimal.InternalRoundFromZero(ref Decimal d, int decimalCount)方法。
private const Int32 MaxInt32Scale = 9;
private const int ScaleMask = 0x00FF0000;
private const int SignMask = unchecked((int)0x80000000);
private static UInt32[] Powers10 = new UInt32[] {
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000
};
public static Decimal Truncate(decimal target, byte decimalPlaces)
{
var bits = Decimal.GetBits(target);
int lo = bits[0];
int mid = bits[1];
int hi = bits[2];
int flags = bits[3];
var scale = (byte)((flags & (ScaleMask)) >> 16);
int scaleDifference = scale - decimalPlaces;
if (scaleDifference <= 0)
return target;
UInt32 lastDivisor;
do
{
Int32 diffChunk = (scaleDifference > MaxInt32Scale) ? MaxInt32Scale : scaleDifference;
lastDivisor = Powers10[diffChunk];
InternalDivRemUInt32(ref lo, ref mid, ref hi, lastDivisor);
scaleDifference -= diffChunk;
} while (scaleDifference > 0);
return new Decimal(lo, mid, hi, (flags & SignMask)!=0, decimalPlaces);
}
private static UInt32 InternalDivRemUInt32(ref int lo, ref int mid, ref int hi, UInt32 divisor)
{
UInt32 remainder = 0;
UInt64 n;
if (hi != 0)
{
n = ((UInt32)hi);
hi = (Int32)((UInt32)(n / divisor));
remainder = (UInt32)(n % divisor);
}
if (mid != 0 || remainder != 0)
{
n = ((UInt64)remainder << 32) | (UInt32)mid;
mid = (Int32)((UInt32)(n / divisor));
remainder = (UInt32)(n % divisor);
}
if (lo != 0 || remainder != 0)
{
n = ((UInt64)remainder << 32) | (UInt32)lo;
lo = (Int32)((UInt32)(n / divisor));
remainder = (UInt32)(n % divisor);
}
return remainder;
}
我没有进行严格的性能测试,但在MacOS Sierra 10.12.6、3.06 GHz英特尔Core i3处理器和针对.NetCore 2.1的情况下,这种方法似乎比D.Nesterov的方法快得多(由于我的测试不够严格,所以我不会给出具体数字)。由于增加了代码复杂度,实现此方法的人需要评估性能提升是否值得。