5

I have a list of objects that I am attempting to take sum of after multiple 3 of the fields by each other. The problem is that the final result is NOT matching when I run the calculation by the following 2 cases:

1 - purchasesDeserialized.Sum(reference => reference.Price * reference.Box * reference.Qty)

2 - purchasesDeserialized.OrderBy(r => r.Box).Sum(reference => reference.Price * reference.Box * reference.Qty);

The data is identical in two cases, difference is in case #1 I do the calculation without sorting first vs in case #2 I first sort then calculate. (I expected the outcome to be the same since the sorting should not change any underlying data but rather just reorder them).

Not sure if LINQ is affecting the calculation after the OrderBy or if the issue lands on the C# decimal rounding side of things.

Full replicating code:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    public class Purchase
    {
        public decimal Price { get; set; }
        public decimal Box { get; set; }
        public decimal Qty { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {

            string purchases = "[{\"Box\":10.0,\"Qty\":206.000000,\"Price\":8.323300970873786407766990292},{\"Box\":10.0,\"Qty\":108.000000,\"Price\":8.333333333333333333333333333},{\"Box\":10.0,\"Qty\":46.000000,\"Price\":8.695652173913043478260869565},{\"Box\":10.0,\"Qty\":18.000000,\"Price\":24.833333333333333333333333333},{\"Box\":1.0,\"Qty\":566.000000,\"Price\":80.87985865724381625441696112},{\"Box\":1.0,\"Qty\":12.000000,\"Price\":97.46666666666666666666666667},{\"Box\":1.0,\"Qty\":72.000000,\"Price\":103.06805555555555555555555556},{\"Box\":1.0,\"Qty\":246.000000,\"Price\":81.2906504065040650406504065},{\"Box\":1.0,\"Qty\":78.000000,\"Price\":80.08333333333333333333333333},{\"Box\":10.0,\"Qty\":146.000000,\"Price\":8.030821917808219178082191782},{\"Box\":10.0,\"Qty\":178.000000,\"Price\":8.326404494382022471910112359},{\"Box\":10.0,\"Qty\":364.000000,\"Price\":8.324175824175824175824175825},{\"Box\":10.0,\"Qty\":30.000000,\"Price\":8.666666666666666666666666667},{\"Box\":10.0,\"Qty\":36.000000,\"Price\":24.5000000000000000000},{\"Box\":1.0,\"Qty\":120.000000,\"Price\":83.662500000000000000},{\"Box\":1.0,\"Qty\":332.000000,\"Price\":80.74698795180722891566265061},{\"Box\":1.0,\"Qty\":36.000000,\"Price\":78.833333333333333333333333333},{\"Box\":1.0,\"Qty\":22.000000,\"Price\":96.35909090909090909090909091},{\"Box\":1.0,\"Qty\":134.000000,\"Price\":78.149253731343283582089552239},{\"Box\":10.0,\"Qty\":26.000000,\"Price\":24.346153846153846153846153846},{\"Box\":1.0,\"Qty\":298.000000,\"Price\":97.06644295302013422818791947},{\"Box\":1.0,\"Qty\":18.000000,\"Price\":95.22777777777777777777777778},{\"Box\":10.0,\"Qty\":6.000000,\"Price\":24.166666666666666666666666667},{\"Box\":1.0,\"Qty\":82.000000,\"Price\":96.42195121951219512195121951},{\"Box\":10.0,\"Qty\":154.000000,\"Price\":8.149350649350649350649350649}]";
            var purchasesDeserialized = JsonConvert.DeserializeObject<List<Purchase>>(purchases);

            var sumRes1 = purchasesDeserialized
                .Sum(reference => reference.Price * reference.Box * reference.Qty);
            Console.WriteLine("Sum:" + sumRes1); //returns 294648.40000000000000000000000M

            var sumRes2 = purchasesDeserialized
                .OrderBy(r => r.Box)
               .Sum(reference => reference.Price * reference.Box * reference.Qty);
            Console.WriteLine("Sum after sort:" + sumRes2); //returns 294648.39999999999999999999999M
        }
    }
}

And the output:

Sum:294648.40000000000000000000000
Sum after sort:294648.39999999999999999999999
scorpion5211
  • 1,020
  • 2
  • 13
  • 33
  • OrderBy does not alter the data, but it does impact the order in which the data is summed, so my assumption would be that this is an internal issue with decimals/floating point calculation. I was able to do a more trivial example without any difference in output values between these two queries to confirm. – emagers Jan 26 '22 at 19:59
  • 1
    Or as the documentation states it for decimal "The Decimal type does not eliminate the need for rounding. Rather, it minimizes errors due to rounding". – Ralf Jan 26 '22 at 20:17
  • 1
    Does this answer your question? [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Heretic Monkey Jan 26 '22 at 20:23
  • It's quite the same as https://stackoverflow.com/questions/6221392/decimal-order-of-addition-affects-results – Yuriy Gavrishov Jan 26 '22 at 20:35
  • Financial operations use `decimal` because it has higher precision, but I can't imagine anyone is ever interested in the maximum precision offered by decimal which is 28 or 29 decimal places. Decide on some amount of precision such as 6 decimal places, and round to that, and your "issue" is moot. – djv Jan 26 '22 at 21:08
  • For fun, try this: `(0.1 * 0.2 * 0.3) == (0.3 * 0.2 * 0.1)` – Jack A. Jan 26 '22 at 21:10
  • i'm curious what has a price to 27 decimal place – Moho Jan 26 '22 at 21:12
  • 1
    @JackA. The questions is explicitly about decimal. So try your code with the "m" suffix for the literals and see. – Ralf Jan 26 '22 at 22:11

2 Answers2

1

I think the issue is as you are increasing the value of the total amount (during the Sum operation) at a same time you are decreasing the decimal maximum precision.

In this case the order of elements in the collection matters because it will affect at which point you exceed the decimal precision. The remaining elements then will be added but not precisely which in the end results in different totals.

To give an example, lets say my data type can hold up to 4 places:

var x = 4.998;
var y = 0.002;
var z = 10.00;

x + y + z = 4.998 + 0.002 + 10.00 => 15.00;
z + x + y = 10.00 + 4.998 + 0.002 => 14.99; //(because 10.00 + 4.998 = 14.99 and there is no precision left for remaining decimal place, so its stripped)
Sonyck
  • 186
  • 6
0

Consider for example the terms indexed from your json 0 and 22

decimal d00 = 10M * 206.000000M * 8.323300970873786407766990292M;
decimal d22 = 10.0M * 6.000000M * 24.166666666666666666666666667M;
decimal sum = d00 + d22;

The actual results are (found using a full precision calculator)

d00 = 17146.00000000000000000000000152 // 31
d22 =  1450.00000000000000000000000002 // 30
sum = 18596.00000000000000000000000154 // 31

but you will lose precision as you see that d00 should have 31 total digits, etc., and being limited to decimal precision, the results in c# are

d00 = 17146.000000000000000000000002  // 29
d22 =  1450.0000000000000000000000000 // 29
sum = 18596.000000000000000000000002  // 29

so the error in this arbitrary sum is 0.00000000000000000000000046. With the way rounding goes, the errors will round up or down, depending on the order in which the addition operations go (50% chance of rounding one way or another at each step), so you will most likely end up with different results from different orders.

djv
  • 15,168
  • 7
  • 48
  • 72