Adjusting decimal precision, .net
Asked Answered
B

8

71

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.

  • Can I adjust the precision of decimal variables without using literals?
  • How can I create b from a? How can I create b from c?
Burnight answered 15/7, 2009 at 17:24 Comment(1)
Valid question - the scaling factor can get changed subtly e.g. when decimal properties are serialized to JSON and back again from the UI, and then binary serialization e.g. BinaryFormatter will produce slightly different bytes e.g. if you try to make a hash of the object.Vahe
B
59

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.

Baking answered 15/7, 2009 at 20:37 Comment(1)
Also divide by 1.000... to decrease precision.Walleyed
H
19

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.

Helgoland answered 15/7, 2009 at 17:26 Comment(8)
Good information about scaling factor... now how can I adjust it?Burnight
The scaling factor is not directly controllable, since it adjusts based on the size of the exponent. This is basically how floating point works (and where the term "floating" comes from). Since the number of bits in the significand is constant, the size of the number determines the scale of the significant bits.Kwangju
It is automatically adjusted to fit the needs of the data it contains. Is there a particular reason you need to scale it manually?Helgoland
There is a reason - however, I feel that the reason does not add clarity to this question. I will probably ask a separate question about the reason.Burnight
@AndrewHare I just need a quick confirmation - I believe the definition of precision (total digits in a number) and scale (total number of digits after the decimal point) is same in .net and SQL world for decimal data types. I think these concepts of precision and scale has to do with world of mathematics rather than .Net or SQL. Please correct me if I'm wrong.Soot
Shouldn't that be -2<sup>96</sup>**+**1?Anvil
According to the documentation, decimal is 128 bits. So one byte is for the sign, 96 bits are for the numeric value, and the exponent ranges from 0 to 28, requiring 5 bits. What happens to the remaining 26 bits? Are they just wasted space? (Why the range 0-28, anyway, rather than making it 0-31, using the full range of the 5 bits?)Anvil
One bit is for the sign, rather.Anvil
R
9

What about Math.Round(decimal d, int decimals)?

Reformism answered 15/7, 2009 at 17:50 Comment(1)
For .00 values you'll have to multiply with 1.00m first: decimal.Round(value*1.00m, 2)Baize
B
8

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;
Burnight answered 15/7, 2009 at 18:38 Comment(1)
Interesting, but at the same time puzzling. What exactly is the difference between 2.0m and 2.00000000m ? In non-computing environment, I would take this to mean that the latter number was guaranteed to that precision, whereas the former was only guaranteed to the first decimal point. However multiplication should mean that the result is only accurate to one decimal place.Inconsistent
B
6

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.

Brashear answered 25/8, 2009 at 21:9 Comment(1)
interestingly, setting the facets on a decimal in EF creates a proper SQL decimal which promptly fails when a c# decimal is handed it... in my case I used Precision: 2, Scale: (none) and I can't assign 800 to it on account of the value being out of rangeMethyl
F
2

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.

Fabricate answered 9/1, 2013 at 18:18 Comment(1)
No need for that many zeroes, can do with 28 zeroes after the 1. See comments to other answer.Walleyed
I
1

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

Inconsistent answered 15/7, 2009 at 18:3 Comment(0)
Y
0

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); }
Yoke answered 25/9, 2023 at 4:30 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.