In this article, we are going to examine the decimal type, while focusing on controlling its precision both internally and in output.

When precision and accuracy matter, decimal numbers are essential. This is the recommended type for all financial calculations due to its design. It is designed as a base 10 floating point value that has an accuracy of between 28 and 29 decimal digits, depending on the value.

## Understanding the Decimal Data Type

Before diving into controlling the precision of decimal values, let’s review a couple of key points about the data type itself.

First, `decimal` is a 128-bit base 10 floating-point value (although only 102 bits are actually used). It consists of 96 bits of mantissa, 5 bits of exponent, and a sign bit. The main advantage of decimal when we speak about financial or certain scientific calculations is the fact that it is a base 10 floating-point value rather than binary. This helps prevent many of the rounding errors that are seen with types such as float and double. For a deep dive into rounding issues, be sure to check out our article dealing with floating-point equality.

Let’s illustrate this with a simple numeric example:

```decimal highPrecisionValue = 123456789.1234567890123456789012345M;
double regularDouble = 123456789.1234567890123456789012345;
float regularFloat = 123456789.1234567890123456789012345f;

Console.WriteLine(\$"Decimal: {highPrecisionValue}");
Console.WriteLine(\$"Double: {regularDouble}");
Console.WriteLine(\$"Float: {regularFloat}");```

Here we define three “identical” values, but assign them to three different IEEE floating-point types: `decimal`, `double`, and `float` respectively. Also, note how we use `M` to define a `decimal` constant and `f` to denote a `float` constant. This is because by default in C# a floating-point constant is considered a `double`.

Now let’s review the output:

```Decimal: 123456789.12345678901234567890
Double: 123456789.12345679
Float: 123456790```

Here we clearly see the difference in the available precision between `decimal`, `double` and `float`. This example highlights why `decimal` is the goto type for financial calculations and other computations where we need a great deal of precision.

Having that in mind, let’s explore how we can control the precision of these values.

## Controlling Output Precision of Decimal

In many cases, we are not so concerned with the internal precision of our values and want to preserve that, but don’t necessarily need to display all 28 digits of accuracy to our user. In these situations, we can use formatting strings to control the “output precision” of our values. For a deep dive into the available format strings, be sure to check out our article: ‘Standard and Custom Numeric Format Strings in .NET

### Controlling Decimal Precision Using Custom Format Strings

First, let’s see how we can use custom format strings to control `decimal` precision. Let’s test this out by restricting the fractional part of our value to two significant digits using the custom format string `"0.00"`:

```const decimal myDecimal = 123.456789M;

Console.WriteLine(\$"Value (\"0.00\"): {myDecimal.ToString("0.00")}");
Console.WriteLine(\$"Value (default format): {myDecimal}");```

Here we define a decimal value `myDecimal` having more than 2 significant digits. We then call `ToString()` with our custom format string `"0.00"` and print the resultant value to the console. Following that we print the original value without any custom formatting.

Upon examining the output we see that the internal value has remained unchanged (as observed in the second `WriteLine()` call), but when printed with our custom formatting string, the number displayed significant digits is restricted:

```Value ("0.00"): 123.46
Value (default format): 123.456789```

To display more digits, we simply need to adjust the format string. For instance, to display 4 significant digits:

`Console.WriteLine(\$"Value (\"0.0000\"): {myDecimal.ToString("0.0000")}");`

Which produces:

`Value ("0.0000"): 123.4568`

### Using NumberFormatInfo

Another option for formatting the output of `decimal` values is to make use of the NumberFormatInfo class. While this class has a plethora of options that we can use to control the output of our value, including even changing the character set for digits, for our purposes, we will focus solely on controlling the number of digits output. We do this by setting the `NumberDecimalDigits` property:

```public static string ToStringXDecimalPlaces(this decimal val, int decimalPlaces)
{
var format = new NumberFormatInfo
{
NumberDecimalDigits = decimalPlaces
};

return val.ToString("F", format);
}```

First, we initialize a new instance of `NumberFormatInfo`, setting its `NumberDecimalDigits` based on our specified precision. We then return the decimal as a string formatted using the Fixed-point standard numeric format and our `NumberFormatInfo` object.

It is important to note that the `NumberDecimalDigits` property only applies when using the standard numeric format strings “N” (Number) or “F” (Fixed-point). For more information on these and other format strings, we can consult the .NET documentation for Standard Numeric Format strings.

Let’s see our extension method in action:

`Console.WriteLine(\$"Value (NumberFormatInfo 3 digits): {myDecimal.ToStringXDecimalPlaces(3)}");`

Which produces:

`Value (NumberFormatInfo 3 digits): 123.457`

Through the use of formatting strings, we can print decimal numbers without altering their numeric value. However, we can also use rounding to control their internal precision.

## Controlling Internal Precision of Decimal

So far we have only examined how to adjust the “cosmetic appearance” of decimal values. Now let’s take a look at how we can control their internal precision.

### Controlling Decimal Precision Through Rounding

Rounding functions are essential for controlling the precision of `decimal` numbers, enabling us to how many digits of precision our value holds.

In C#, the `Math.Round()` function rounds a value to the nearest integer or a specific number of fractional digits. If our application is targeting .NET 7 or greater, we can also use the equivalent `decimal.Round()`function that was added as part of the generic math functionality. By default, the `Round()` method uses the MidpointRounding.ToEven strategy:

```public static decimal Round(this decimal value, int decimalPlaces)
=> decimal.Round(value, decimalPlaces);
```

Here we create a simple extension method that will invoke `decimal.Round()` to round our value to the specified number of places.

Now, let’s see it in action:

```Console.WriteLine(\$"Value (default format): {myDecimal}");
Console.WriteLine(\$"Value (round 2 places): {myDecimal.Round(2)}");```

Notice here that we are printing both values using the default decimal format:

```Value (default format): 123.456789
Value (round 2 places): 123.46```

### The Truncate Method

We can also use the `Truncate()` method to control the precision of our value. `Truncate()` removes the fractional part, leaving us with only the integral piece of our value. Let’s create an extension method for exercising it:

`public static decimal Truncate(this decimal value) => decimal.Truncate(value);`

And calling it with our `decimal` value (`123.456789`):

`Console.WriteLine(\$"Value (truncate): {myDecimal.Truncate()}");`

Yields:

`Value (truncate): 123`

### The Ceiling and Floor Methods

`Ceiling()` and `Floor()` are similar to `Truncate()` in that they both return a value with the fractional part removed. The difference is that while `Truncate()` simply strips the fractional part off, `Ceiling()` and `Floor()` act more like `Round()`. `Ceiling()` returns the next smallest integer value that is greater than the specified value. In contrast, `Floor()` returns the next largest integer value that is less than the specified value.

Let’s create a couple more extension methods to exercise these operations:

```public static decimal Ceiling(this decimal value) => decimal.Ceiling(value);

public static decimal Floor(this decimal value) => decimal.Floor(value);```

There isn’t much to these methods other than some syntactic sugar that allows us to call them directly on our example `decimal` value:

```Console.WriteLine(\$"Value (ceiling): {myDecimal.Ceiling()}");
Console.WriteLine(\$"Value (floor): {myDecimal.Floor()}");```

Which yields:

```Value (ceiling): 124
Value (floor): 123```

## Conclusion

In this article, we explored various techniques for controlling the precision of `decimal` values. We first examined how to control the output format without modifying the internal storage of our values. We then examined techniques for controlling the internal precision of our values. Ultimately, the option we choose is dependent upon our use case. If we wish to maintain a high amount of precision, we should probably focus on simply adjusting the display formatting of our data. On the other hand, when we have less precise data, we may wish to use one of the rounding techniques to reduce the internal precision of our values. For more information regarding the decimal type, be sure to check out Jon Skeet’s excellent article on the topic.