Esta é uma questão antiga, mas muitos anwsers não funcionam bem ou transbordam para números grandes. Acho que a resposta de D. Nesterov é a melhor: robusta, simples e rápida. Eu só quero adicionar meus dois centavos. Eu brinquei com decimais e também verifiquei o código-fonte . Da public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)
documentação do construtor .
A representação binária de um número decimal consiste em um sinal de 1 bit, um número inteiro de 96 bits e um fator de escala usado para dividir o número inteiro e especificar qual parte dele é uma fração decimal. O fator de escala é implicitamente o número 10 elevado a um expoente que varia de 0 a 28.
Sabendo disso, minha primeira abordagem foi criar outro decimal
cuja escala corresponda aos decimais que eu queria descartar, truncar e finalmente criar um decimal com a escala desejada.
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);
}
Este método não é mais rápido que o de D. Nesterov e é mais complexo, então brinquei um pouco mais. Meu palpite é que ter que criar um auxiliar decimal
e recuperar os bits duas vezes o torna mais lento. Em minha segunda tentativa, eu mesmo manipulei os componentes retornados pelo método Decimal.GetBits (Decimal d) . A ideia é dividir os componentes em 10 vezes quantas forem necessárias e reduzir a escala. O código é baseado (fortemente) no método 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;
}
Não realizei testes de desempenho rigorosos, mas em um MacOS Sierra 10.12.6, processador Intel Core i3 de 3,06 GHz e direcionado para .NetCore 2.1 este método parece ser muito mais rápido do que o de D. Nesterov (não vou dar números desde , como já mencionei, meus testes não são rigorosos). Cabe a quem implementa isso avaliar se os ganhos de desempenho compensam ou não pela complexidade de código adicionada.