I see that, in C#, rounding a decimal
, by default, uses MidpointRounding.ToEven
. This is expected, and is what the C# spec dictates. However, given the following:
- A
decimal dVal
- A format
string sFmt
that, when passed in todVal.ToString(sFmt)
, will result in a string containing a rounded version ofdVal
...it is apparent that decimal.ToString(string)
returns a value rounded using MidpointRounding.AwayFromZero
. This would appear to be a direct contradiction of the C# spec.
My question is this: is there a good reason this is the case? Or is this just an inconsistency in the language?
Below, for reference, I've included some code that writes to console an assortment of rounding operation results and decimal.ToString(string)
operation results, each on every value in an array of decimal
values. The actual outputs are embedded. After that, I've included a relevant paragraph from the C# Language Specification section on the decimal
type.
The example code:
static void Main(string[] args)
{
decimal[] dArr = new decimal[] { 12.345m, 12.355m };
OutputBaseValues(dArr);
// Base values:
// d[0] = 12.345
// d[1] = 12.355
OutputRoundedValues(dArr);
// Rounding with default MidpointRounding:
// Math.Round(12.345, 2) => 12.34
// Math.Round(12.355, 2) => 12.36
// decimal.Round(12.345, 2) => 12.34
// decimal.Round(12.355, 2) => 12.36
OutputRoundedValues(dArr, MidpointRounding.ToEven);
// Rounding with mr = MidpointRounding.ToEven:
// Math.Round(12.345, 2, mr) => 12.34
// Math.Round(12.355, 2, mr) => 12.36
// decimal.Round(12.345, 2, mr) => 12.34
// decimal.Round(12.355, 2, mr) => 12.36
OutputRoundedValues(dArr, MidpointRounding.AwayFromZero);
// Rounding with mr = MidpointRounding.AwayFromZero:
// Math.Round(12.345, 2, mr) => 12.35
// Math.Round(12.355, 2, mr) => 12.36
// decimal.Round(12.345, 2, mr) => 12.35
// decimal.Round(12.355, 2, mr) => 12.36
OutputToStringFormatted(dArr, "N2");
// decimal.ToString("N2"):
// 12.345.ToString("N2") => 12.35
// 12.355.ToString("N2") => 12.36
OutputToStringFormatted(dArr, "F2");
// decimal.ToString("F2"):
// 12.345.ToString("F2") => 12.35
// 12.355.ToString("F2") => 12.36
OutputToStringFormatted(dArr, "###.##");
// decimal.ToString("###.##"):
// 12.345.ToString("###.##") => 12.35
// 12.355.ToString("###.##") => 12.36
Console.ReadKey();
}
private static void OutputBaseValues(decimal[] dArr)
{
Console.WriteLine("Base values:");
for (int i = 0; i < dArr.Length; i++) Console.WriteLine("d[{0}] = {1}", i, dArr[i]);
Console.WriteLine();
}
private static void OutputRoundedValues(decimal[] dArr)
{
Console.WriteLine("Rounding with default MidpointRounding:");
foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2) => {1}", d, Math.Round(d, 2));
foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2) => {1}", d, decimal.Round(d, 2));
Console.WriteLine();
}
private static void OutputRoundedValues(decimal[] dArr, MidpointRounding mr)
{
Console.WriteLine("Rounding with mr = MidpointRounding.{0}:", mr);
foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2, mr) => {1}", d, Math.Round(d, 2, mr));
foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2, mr) => {1}", d, decimal.Round(d, 2, mr));
Console.WriteLine();
}
private static void OutputToStringFormatted(decimal[] dArr, string format)
{
Console.WriteLine("decimal.ToString(\"{0}\"):", format);
foreach (decimal d in dArr) Console.WriteLine("{0}.ToString(\"{1}\") => {2}", d, format, d.ToString(format));
Console.WriteLine();
}
The paragraph from section 4.1.7 of the C# Language Specification ("The decimal type") (get the full spec here (.doc)):
The result of an operation on values of type decimal is that which would result from calculating an exact result (preserving scale, as defined for each operator) and then rounding to fit the representation. Results are rounded to the nearest representable value, and, when a result is equally close to two representable values, to the value that has an even number in the least significant digit position (this is known as “banker’s rounding”). A zero result always has a sign of 0 and a scale of 0.
It's easy to see that they may not have been considering ToString(string)
in this paragraph, but I'm inclined to think it fits in this description.