0

A decimal value in C# has a fixed decimal point and therefore knows how many decimal places it had when it was created:

25.000m.ToString()

returns "25.000", unlike a double which has a floating point.

This question is not about how to display a number with fixed decimals, I know the various string conversion options. This is about the internal representation of the decimal data type, I'm just using .ToString() to show it.

Now I want to round a number to a fixed number of decimals. This works:

Math.Round(25.0000m, 3) -> 25.000

But when the number of decimals was less than 3, or it comes from a double, it doesn't (of course):

Math.Round(25.00m, 3) -> 25.00
Math.Round((decimal) 25.0000d, 3) -> 25
(decimal) Math.Round(25.0000d, 3) -> 25

So how can I round any double number to a decimal with 3 forced places?

Since it's hard to explain, suggestions for a better title are welcome!

maf-soft
  • 2,335
  • 3
  • 26
  • 49
  • 1
    Decimal and Double are both floating point numbers, the difference is that Decimal is a 128bit floating decimal number, where as Double is a 64bit floating binary number. If you want decimal precision use decimal, if you want large natural numbers use double. – Callum Linington Apr 04 '17 at 07:49
  • 4
    http://stackoverflow.com/questions/1132765/adjusting-decimal-precision-net – JustAndrei Apr 04 '17 at 07:51
  • 3
    It seems to me that you should be choosing the precision when you convert it to a string rather than messing around with `Math.Round()`. – Matthew Watson Apr 04 '17 at 07:52
  • @MatthewWatson Decimals have inherent precision. It's reasonable to want to use that rather than ignore it. – Rawling Apr 04 '17 at 07:53
  • @Rawling I know that, but it is of questionable use. You might want to display the same number with different number of decimal places. – Matthew Watson Apr 04 '17 at 07:54
  • @MatthewWatson The questioner doesn't seem to think so. – Rawling Apr 04 '17 at 07:54
  • Putting all the comments together, a decimal is "just" a 128-bit floating point number that preserves trailinig zeroes. You can't change its precision, nor should you care about it. – Panagiotis Kanavos Apr 04 '17 at 07:54
  • @PanagiotisKanavos The second comment gives ways to change its precision. – Rawling Apr 04 '17 at 07:55
  • @Rawling I'm saying he's wrong to go about it the way he wants to. ;) – Matthew Watson Apr 04 '17 at 07:56
  • @Rawling not really. It's nothing different than doubles or floats. The same number can be represented with different combinations of base and mantissa. Multiplications and divisions can change be used on all floating point numbers to change these indirectly, with the possibility of introducing scaling errors. – Panagiotis Kanavos Apr 04 '17 at 07:59
  • @PanagiotisKanavos doubles and floats in C# are IEEE and only have one base/mantissa combination, surely. – Rawling Apr 04 '17 at 08:01
  • See http://stackoverflow.com/questions/1132765/adjusting-decimal-precision-net – Matthew Watson Apr 04 '17 at 08:01
  • @MatthewWatson see https://stackoverflow.com/questions/43201233/how-to-change-the-precision-for-a-decimal-in-c-sharp?noredirect=1#comment73474352_43201233 – Rawling Apr 04 '17 at 08:02
  • @Rawling You confuse precision for trailining zero preservation. You can change precision in SQL but not in C#. Multiplications and divisions change the scale of the operands, nothing more. In fact, the entire discussion is pointless. Decimals are *immutable*. You can't change anything. You create new values. Might as well add `0.000m` – Panagiotis Kanavos Apr 04 '17 at 08:03
  • @PanagiotisKanavos Someone who needs a solution to this question *might* confuse precision with trailing zero preservation. Or they *might* have a valid reason. – Rawling Apr 04 '17 at 08:05
  • @Rawling check [the documentation](https://msdn.microsoft.com/en-us/library/364x0z75.aspx) first, then remember that decimals are immutable. You can't change them, only create new ones. It's easier to do so with addition than multiplication – Panagiotis Kanavos Apr 04 '17 at 08:06
  • There is one place I know where this matters - ADO.NET and all based on it (EF etc.). The trailing zeroes are included in precision calculation, which is causing false positive max precision failures. – Ivan Stoev Apr 04 '17 at 08:06
  • @IvanStoev that's a completely different thing. That's because *SQL* and all databases does have numeric types with different precisions. The precision you store though is governed by the database and parameter definitions, not `decimal` and the possibility of trailing zeros – Panagiotis Kanavos Apr 04 '17 at 08:08
  • @IvanStoev the only case where the trailiing zeroes are used is if you *don't* specify a parameter's type but use the bad practice of using `AddWithValue`. This method has to guess string sizes, numeric scales and precisions and can lead to errors if any of them isn't appropriate – Panagiotis Kanavos Apr 04 '17 at 08:09
  • Related: [Is there a BigFloat class in C#](http://stackoverflow.com/questions/10359372/is-there-a-bigfloat-class-in-c) – poke Apr 04 '17 at 08:10
  • @PanagiotisKanavos Why are you talking about immutability. Of *course* decimals are immutable. Of *course* when we talk about "changing" them we mean "creating a new value based on an old value". This question is asking *how to do that*. – Rawling Apr 04 '17 at 08:12
  • I aready mentioned that my title is not good. Suggestions welcome. Should I add "trailing zero preservation"? – maf-soft Apr 04 '17 at 08:12
  • @Rawling add `0.000m`. Or `0.0000m`. Addition doesn't change the scale of a floating point number, only multiplication/division do. The only thing that changes is the number of trailing zeroes – Panagiotis Kanavos Apr 04 '17 at 08:13
  • @PanagiotisKanavos It really doesn't matter how do you initialize parameters (and you can't say that EF internally is not initializing them correctly). If you pass a decimal **value** `1.00000000` it will fail if the target max precision is let say 6. We understand that the internal format of `decimal` usually does not matter, but it would be nice if there is method to "normalize` the internal format. – Ivan Stoev Apr 04 '17 at 08:19

1 Answers1

2

You could always add 0.000m onto the result:

        var x = Math.Round(25.00m, 3);
        var y = 0.000m;
        var z = x + y;

Printing z shows it now has 3 decimal places. I agree with comments that this is of questionable value though.

(NB - this still won't create 3 decimal places if the current value of x is sufficiently large that it cannot accommodate the additional places without changing the integral portion)


EDIT by the questioner: To make this answer acceptable, adding this from the comments (the question asked for a double as source and this also solves an issue with VS2008):

var z = Math.Round(((decimal) 25d) + 0.000m, 3);

Interestingly in VS2008 it only works when the casting to decimal and adding 0.000m is done inside the Math.Round(). In VS2015 it can also be done outside. If anyone knows an explanation for this, please comment.

maf-soft
  • 2,335
  • 3
  • 26
  • 49
Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Thanks, but... Hmm, this works: `((decimal) 25d) + 0.000m`, but why not this `((decimal) Math.Round(25d, 3)) + 0.000m`? I also don't understand your note about "integral portion". To accept this answer, please change it to work with a `double` as source. – maf-soft Apr 04 '17 at 08:09
  • @maf-soft decimals preserve trailing zeroes. Doubles don't. There is no precision change. Why do you care about the number of trailing zeroes? BTW `((decimal) Math.Round(25d, 3)) + 0.000m` returns `25.000` on LinqPad. Both snippets return a decimal with 3 trailing zeroes – Panagiotis Kanavos Apr 04 '17 at 08:11
  • 2
    @maf-soft - `Decimal.MaxValue`, for instance, has no digits after the decimal. It cannot be changed to have 3 decimal places because there's not enough *precision* available in the `decimal` type. Any other value within 3 orders of magnitude of `MaxValue` (or `MinValue`) cannot be changed to have 3 d.p. – Damien_The_Unbeliever Apr 04 '17 at 08:14
  • @maf-soft are you using these values with `Parameters.AddWIthValue` ? That's the only case where trailing zeroes could matter, only because ADO.NET has to guess what precision and scale to use for the numeric parameter – Panagiotis Kanavos Apr 04 '17 at 08:15
  • @PanagiotisKanavos, I entered `( ((decimal) Math.Round(25d, 3)) + 0.000m ).ToString()` in Visual Studio 2008 QuickWatch and it returned "25". Then I did the same in VS 2015 C# interactive window and it returned "25.000". Fascinating! – maf-soft Apr 04 '17 at 08:23
  • This works in VS 2008: `( Math.Round(((decimal) 25d) + 0.000m, 3) ).ToString()` - I really wonder where this difference comes from. Makes no sense to me... – maf-soft Apr 04 '17 at 08:45
  • @PanagiotisKanavos, no it is not ADO related. The decimal goes to a web service (auto-generated XML) and the specification says I should send the data with 3 decimal places. It fails when I send more and it doesn't fail when I send less. Call me a perfectionist, I just like to have it nice. – maf-soft Apr 04 '17 at 08:52
  • @maf-soft this has nothing to do with precision then, This has to do with how you generate and format the XML string. You should use the appropriate attributes or format strings. In fact, DTO classes generated from WSDL or XSDs *already* have the appropriate attributes. You shouldn't be bothering with decimal's zeroes at all - it won't affect how the XML serializer works – Panagiotis Kanavos Apr 04 '17 at 08:57
  • You are right, but somehow the other side wasn't able to make the number of decimal places appear in the WSDL. They say, their SAP version doesn't support it. But it fails when I send more, so I needed at least the rounding. But apart from this all, this is just the explanation why I had the idea to ask this question here, it's not a real problem. I just like to learn. – maf-soft Apr 04 '17 at 09:26