Это старый вопрос, но многие ответы не работают или переполняются для больших чисел. Я считаю, что ответ Д. Нестерова самый лучший: надежный, простой и быстрый. Я просто хочу добавить свои два цента. Я поигрался с десятичными знаками, а также проверил исходный код . Из public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)
документации конструктора .
Двоичное представление десятичного числа состоит из 1-битового знака, 96-битного целого числа и коэффициента масштабирования, используемого для деления целого числа и указания того, какая его часть является десятичной дробью. Коэффициент масштабирования неявно представляет собой число 10, возведенное в степень в диапазоне от 0 до 28.
Зная это, мой первый подход состоял в том, чтобы создать другой 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);
}
Этот метод не быстрее, чем у Д. Нестерова, и он сложнее, поэтому я еще немного поигрался. Я предполагаю, что необходимость создавать вспомогательный объект 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);
// Fast access for 10^n where n is 0-9
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;
// Divide the value by 10^scaleDifference
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, процессоре Intel Core i3 3,06 ГГц и таргетинге на .NetCore 2.1 этот метод кажется намного быстрее, чем метод Д. Нестерова (я не буду приводить цифры, поскольку , как я уже упоминал, мои тесты не являются строгими). Тот, кто реализует это, должен оценить, окупается ли прирост производительности добавленной сложностью кода.