Reputation: 110221
These lines in C#
decimal a = 2m;
decimal b = 2.0m;
decimal c = 2.00000000m;
decimal d = 2.000000000000000000000000000m;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(d);
Generates this output:
2
2.0
2.00000000
2.000000000000000000000000000
So I can see that creating a decimal variable from a literal allows me to control the precision.
Upvotes: 71
Views: 71571
Reputation: 1825
Based on the answers of: @zacuke, @Andrew Hare, and @Joe; I created the following extension class:
using System.Runtime.CompilerServices;
/// <summary>
/// Adds WithScale extension method to decimal
/// </summary>
static public class DecimalExtenstions
{
/// <summary>
/// Tries changing the scale of a decimal
/// and returns the new value.
/// </summary>
/// <remarks>
/// A lower scale can only bet set, if the value ends
/// on 0`s. If the value ends on a different digit,
/// the scale can not be decreased.
/// </remarks>
static public decimal WithScale(this decimal m, byte scale)
{
var diffScale = scale - m.GetScale();
switch(diffScale)
{
case 0:
return m;
case > 0:
return m * GetScaleMultiplier((byte)diffScale);
case < 0:
return m / GetScaleMultiplier((byte)-diffScale);
}
}
/// <summary>
/// Gets the scale of a decimal
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public byte GetScale(this decimal m) =>
#if NET7_0_OR_GREATER
m.Scale;
#else
(byte)((unchecked((uint)decimal.GetBits(m)[3]) >> 16) & 0xff);
#endif
/// <summary>
/// Creates the value 1m, 1.0m, 1.00m, …,
/// 1.0000000000000000000000000000m, depending
/// on the value of <paramref name="diffScale"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static private decimal GetScaleMultiplier(byte diffScale) =>
1m + new decimal(0, 0, 0, false, diffScale);
}
The difference here is, we create a fitting multiplier in the first place, without using 1.0m in a for loop.
I use the constructor of decimal that takes the the scale as the last argument. I create 0.000… this way. Then I add 1m.
I also created an extension method to retrieve the scale of the decimal. If an older framework is used, we use decimal.GetBits()
to retrieve the information. The scale is encoded in the 4th int. The bits 16 to 23 of that bit contain the scale (which must be between 0 and 28) [as described here: https://learn.microsoft.com/en-us/dotnet/api/system.decimal.getbits?view=netframework-4.7.2\].
You could use the NuGet-Package System.Runtime.CompilerServices.Unsafe optimize the GetScale method further like this:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public byte GetScale(this decimal m) =>
Unsafe.AddByteOffset(
ref Unsafe.As<decimal, byte>(ref m),
2); // <- might be 14, when system is big endian.
Here we create a ref to the first byte of the decimal and move it 2 bytes further, to directly access the scale byte.
I’m not sure though, if this works, if the system is big endian. Maybe the scale byte offset then is 14 instead of 2.
This code is allocation free and of course much faster than the code that allocates an array. On the other hand, this kind of optimization is probably only necessary in extreme edge cases.
Upvotes: 0
Reputation: 3
With .NET 7 there is a decimal.Scale property now. So we can write an extension method like this:
/// <summary>
/// Fix decimal to show trailing zeros
/// </summary>
/// <param name="input">Input decimal</param>
/// <param name="scale">Number of decimal places from 0 to 28</param>
/// <returns>Fixed precision decimal</returns>
public static decimal SetScale(this decimal input, int scale)
{
if (scale < 0 || scale > 28)
throw new ArgumentOutOfRangeException(nameof(scale));
if (input.Scale == scale)
return input;
//normalize
input /= 1.000000000000000000000000000000000m;
var scaleToadd = scale - input.Scale;
for(int i = 0; i < scaleToadd; i++)
{
input *= 1.0M;
}
return input;
}
And use it on a property to force precision and trailing zeros in the decimal. html textbox showing trailing zeros:
private decimal price;
public decimal Price { get => price; set => price = value.SetScale(2); }
Upvotes: 0
Reputation: 11326
This will remove all the trailing zeros from the decimal and then you can just use ToString()
.
public static class DecimalExtensions
{
public static Decimal Normalize(this Decimal value)
{
return value / 1.000000000000000000000000000000000m;
}
}
Or alternatively, if you want an exact number of trailing zeros, say 5, first Normalize() and then multiply by 1.00000m.
Upvotes: 2
Reputation: 351708
You are just seeing different representations of the exact same data. The precision of a decimal
will be scaled to be as big as it needs to be (within reason).
From System.Decimal
:
A decimal number is a floating-point value that consists of a sign, a numeric value where each digit in the value ranges from 0 to 9, and a scaling factor that indicates the position of a floating decimal point that separates the integral and fractional parts of the numeric value.
The binary representation of a Decimal value consists of a 1-bit sign, a 96-bit integer number, and a scaling factor used to divide the 96-bit integer and specify what portion of it is a decimal fraction. The scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28. Therefore, the binary representation of a Decimal value is of the form, ((-296 to 296) / 10(0 to 28)), where -296-1 is equal to MinValue, and 296-1 is equal to MaxValue.
The scaling factor also preserves any trailing zeroes in a Decimal number. Trailing zeroes do not affect the value of a Decimal number in arithmetic or comparison operations. However, trailing zeroes can be revealed by the ToString method if an appropriate format string is applied.
Upvotes: 19
Reputation: 31071
It's tempting to confuse decimal
in SQL Server with decimal
in .NET; they are quite different.
A SQL Server decimal
is a fixed-point number whose precision and scale are fixed when the column or variable is defined.
A .NET decimal
is a floating-point number like float
and double
(the difference being that decimal
accurately preserves decimal digits whereas float
and double
accurately preserve binary digits). Attempting to control the precision of a .NET decimal
is pointless, since all calculations will yield the same results regardless of the presence or absence of padding zeros.
Upvotes: 6
Reputation: 124794
Preserving trailing zeroes like this was introduced in .NET 1.1 for more strict conformance with the ECMA CLI specification.
There is some info on this on MSDN, e.g. here.
You can adjust the precision as follows:
Math.Round (or Ceiling, Floor etc) to decrease precision (b from c)
Multiply by 1.000... (with the number of decimals you want) to increase precision - e.g. multiply by 1.0M to get b from a.
Upvotes: 59
Reputation: 110221
I found that I could "tamper" with the scale by multiplying or dividing by a fancy 1.
decimal a = 2m;
decimal c = 2.00000000m;
decimal PreciseOne = 1.000000000000000000000000000000m;
//add maximum trailing zeros to a
decimal x = a * PreciseOne;
//remove all trailing zeros from c
decimal y = c / PreciseOne;
I can fabricate a sufficiently precise 1 to change scale factors by known sizes.
decimal scaleFactorBase = 1.0m;
decimal scaleFactor = 1m;
int scaleFactorSize = 3;
for (int i = 0; i < scaleFactorSize; i++)
{
scaleFactor *= scaleFactorBase;
}
decimal z = a * scaleFactor;
Upvotes: 8
Reputation: 16077
The question is - do you really need the precision stored in the decimal, rather than just displaying the decimal to the required precision. Most applications know internally how precise they want to be and display to that level of precision. For example, even if a user enters an invoice for 100 in an accounts package, it still prints out as 100.00 using something like val.ToString("n2").
How can I create b from a? How can I create b from c?
c to b is possible.
Console.WriteLine(Math.Round(2.00000000m, 1))
produces 2.0
a to b is tricky as the concept of introducing precision is a little alien to mathematics.
I guess a horrible hack could be a round trip.
decimal b = Decimal.Parse(a.ToString("#.0"));
Console.WriteLine(b);
produces 2.0
Upvotes: 1