In This Post We Remember You Can Do Math With Computers

I recently ran into an interesting problem. I'll try to describe the problem with out going into specific details of the domain the problem lives in. Let's say I have a list of elements and each element have a property called weight that contains a decimal that represents that elements percentage in the entire list. If you sum the weights it should add up to 1M or 100%. This list of elements was loaded up from a text file and the text file weight had a precision of nine meaning that the number was represented with nine numbers after the decimal, 0.000000000. With this list I need to modify the precision to six and still have the sum of the weights add up to 100% exactly.

Here is what I came up with.

public class WeightRounder
{
    private const int SIGNIFIGANT_DIGITS = 6;

    public IList<element> RoundOff(IList<element> elements)
    {
        if (elements.Count > 0)
        {
            MakeRoundedElements(elements);
            RedistributeWeightError(elements, GetTotalWeightError(elements));
        }
        return model;
    }

    private static void MakeRoundedModel(IEnumerable<element> elements)
    {
        model.Each(x => x.UpstreamWeight = Math.Round(x.Weight, SIGNIFIGANT_DIGITS));
    }

    private void RedistributeWeightError(IEnumerable<element> elements, decimal totalWeightError)
    {
        int errorSign = Math.Sign(totalWeightError);
        decimal step = (decimal) Math.Pow(10, -SIGNIFIGANT_DIGITS)*errorSign;

        elements.OrderByDescending(x => x.UpstreamWeight)
                .TakeWhile(x => Math.Abs(totalWeightError) > decimal.Zero)
                .Each(x =>
                      {
                          x.UpstreamWeight += step;
                          totalWeightError -= step;
                      });

        //indicates the elements were nowhere near 100% to begin with.
        if (totalWeightError != 0)
            throw new ApplicationException("Rounding failed. Total weight error {0} was to large to handle.".FormatWith(totalWeightError));
    }

    private decimal GetTotalWeightError(IEnumerable<element> elements)
    {
        var totalWeightError = decimal.One;
        elements.Each(x => totalWeightError -= x.UpstreamWeight);
        return totalWeightError;
    }
}

Thoughts, comments or rants on my general approach and math skills appreciated.

Follow me on Mastodon!