To stare pytanie, ale wiele odpowiedzi nie działa dobrze lub przepełnia się przy dużych liczbach. Myślę, że odpowiedź D. Niestierowa jest najlepsza: solidna, prosta i szybka. Chcę tylko dodać moje dwa centy. Bawiłem się liczbami dziesiętnymi, a także sprawdzałem kod źródłowy . Z public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)
dokumentacji konstruktora .
Binarna reprezentacja liczby dziesiętnej składa się ze znaku 1-bitowego, 96-bitowej liczby całkowitej oraz współczynnika skalowania używanego do dzielenia liczby całkowitej i określania, która z jej części jest ułamkiem dziesiętnym. Współczynnik skalowania jest niejawnie liczbą 10 podniesioną do wykładnika z zakresu od 0 do 28.
Wiedząc o tym, moje pierwsze podejście polegało na stworzeniu kolejnej, decimal
której skala odpowiada liczbom dziesiętnym, które chciałem odrzucić, a następnie skrócić ją i ostatecznie utworzyć ułamek dziesiętny z pożądaną skalą.
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);
}
Ta metoda nie jest szybsza niż metoda D. Niestierowa i jest bardziej złożona, więc bawiłem się trochę więcej. Domyślam się, że konieczność stworzenia pomocniczego decimal
i dwukrotnego pobrania bitów powoduje, że jest wolniejszy. Za drugim razem samodzielnie manipulowałem składnikami zwracanymi przez metodę Decimal.GetBits (Decimal d) . Chodzi o to, aby podzielić składniki 10 razy tyle razy, ile potrzeba i zmniejszyć skalę. Kod jest oparty (w dużym stopniu) na metodzie 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;
}
Nie przeprowadziłem rygorystycznych testów wydajności, ale na procesorze MacOS Sierra 10.12.6, 3,06 GHz Intel Core i3 i docelowym .NetCore 2.1 ta metoda wydaje się być znacznie szybsza niż D.Nesterov (nie podam liczb, ponieważ jak wspomniałem, moje testy nie są rygorystyczne). Każdy, kto to zaimplementuje, musi ocenić, czy wzrost wydajności opłaca się za dodatkową złożoność kodu.