Найдите наибольшее число в наборе, которое нужно округлить в меньшую сторону, и вместо этого округлите его - PullRequest
5 голосов
/ 31 августа 2010

Как следует из названия, у меня есть набор объектов - назовите их Распределения - которые содержат описание и номер.Все числа в наборе составляют до 100%, но для удобства я иногда округляю до целого процента.В некоторых крайних случаях, округляя числа, я получаю 99%.

Пример:

Description  | Actual | Rounded
===============================
Allocation A | 65.23% | 65% 
Allocation B | 25.40% | 25%
Allocation C | 7.95%  | 8%
Allocation D | 1.42%  | 1%
===============================
Total        | 100%   | 99% (Bad!)

Запрашиваемое решение, которое несовершенно, но подойдет, состоит в том, чтобы найти самое высокое, которое нужно округлить в меньшую сторону, и вместо этого округлить в большую сторону.В приведенном выше примере 1,42% станет 2% при округлении. Редактировать: Под "наивысшим округляется" я подразумеваю тот, который округляется самым дальним.Следовательно, 1,42% округляется до 0,42, тогда как 65,23 округляется только до 0,23

Итак, теперь код, у меня есть класс

public class Allocation
{
  public string Description {get;set;}
  public doubel Percentage {get;set;}
}

И они хранятся вIEnumerable<Allocation>.Итак, потенциально используя LINQ, как я могу определить, какой из них следует округлить.Или, более конкретно, как я могу сгенерировать новый IEnumerable<Allocation> с округленными числами.

Если у кого-то есть какие-либо другие предложения всегда делать округленный процент, всегда равняется 100%, что было бы даже лучше!

Ответы [ 5 ]

3 голосов
/ 31 августа 2010

Я бы посоветовал всегда округлять, а затем, если результат равен 100-n, округлить числа с наибольшим остатком 'n'. Это будет работать для любых данных. Подходы, которые округляют до ближайшего, а затем пытаются скорректировать результат, оказываются более сложными. Я не думаю, что тот факт, что ассигнования, округленные до 0,01%, составляют в сумме 100,00%, говорят о том, что произойдет, когда они будут округлены до ближайших 0,1% или 1%.

Другой подход заключается в том, чтобы выполнить начальное вычисление с округлением до ближайшего, а затем, если результат не дает 100%, разделить все числа на общий процент и повторить попытку. Поэтому, если конечный процент был 101%, разделите все (неокругленные) числа на 1,01 и повторите последовательность округлений. Это даст несколько иные результаты, которые могут оказаться более или менее желательными. Предположим, что цифры 1,3 1,3 1,3 96,1. При округлении их получается 99. При округлении одного из 1,3 до 2 получается 100, но округление искажает значение на 53%, а не на 23%; наоборот, округление от 96,1 до 97 будет представлять собой искажение его значения примерно на 0,95% (97 против 96,1).

3 голосов
/ 31 августа 2010

Как указал ho1, решение добавить 1 к определенной строке не решает реальную проблему.

Рассмотрим следующие сценарии:

3 items evenly divided, 100/3 = 33 ; 33 * 3 = 99 ; Error = -1
7 items evenly divided, 100/7 = 14 ; 14 * 7 = 98 ; Error = -2
66 items evenly divided, 100/66 = 2 ; 2 * 66 = 132 ; Error = 32

Вот некоторый непроверенный код, который можетприблизить вас туда, куда вам нужно идти.Вероятно, здесь есть ошибка знака, так что будьте осторожны.

public class AllocationRoundingWrapper
{
  public Allocation Original {get;set;}
  public double Rounded {get;set;}
  public double IntroducedError()
  {
    return  Rounded - Original.Percentage;
  }
}

  //project the Allocations into Wrappers for rounding efforts.

List<Allocation> source = GetAllocations();

List<AllocationRoundingWrapper> roundingWrappers = source
  .Select(a => new AllocationRoundingWrapper()
  {
    Original = a,
    Rounded = Math.Round(a.Percentage)
  }).ToList();

int error = (int) roundingWrappers.Sum(x => x.IntroducedError());

  //distribute the rounding errors across the
  // items with the absolute largest error.

List<RoundingWrapper> orderedItems = error > 0 ?
  roundingWrappers.OrderByDescending(x => x.IntroducedError()).ToList() :
  roundingWrappers.OrderBy(x => x.IntroducedError()).ToList();

IEnumerator<RoundingWrapper> enumerator = orderedItems.GetEnumerator();

while(error > 0)
{
  enumerator.MoveNext();
  enumerator.Current.Rounded += 1.0;
  error -= 1;
}
while(error < 0)
{
  enumerator.MoveNext();
  enumerator.Current.Rounded -= 1.0;
  error += 1;
}

  //project back into Allocations for the result
List<Allocation> result = roundingWrappers
  .Select(x => new Allocation()
  {
    Description = x.Original.Description,
    Percentage = x.Rounded
  }).ToList();

Примечание. Упорядочение по введенной ошибке может привести к связям.Рассмотрим случай с 3 предметами, только один предмет получит +1 ... вы можете ожидать, что этот предмет будет выбран последовательно.Если последовательные результаты ожидаются от нескольких прогонов, связи должны быть разорваны.

2 голосов
/ 31 августа 2010

Что касается получения 100%, почему бы сначала не выполнить исходный расчет и посмотреть, какой процент вы получите, а затем узнать, сколько вам нужно округлить и уменьшить, увидев, сколько процентных пунктов отличается от 100%.

Так что, если у вас получится 97%, округлите 3 числа вверх, а не вниз. Или, если вы получите 102%, округлите два числа с наименьшими десятичными числами (более 0,5) вместо увеличения.

1 голос
/ 31 августа 2010
 var HighestDown = 
       allocation.Where(a=>Math.Round(a.Percentage) == Math.Floor(a.Percentage)
                 .Max(a=>a.Percentage - Math.Floor(a.Percentage));

  HighestDown.Percentage = Math.Ceiling(HighestDown.Percentage);

  var roundedAllocations = for a in allocation
                           select new Allocation
                                  {
                                       Description = a.Description,
                                       Percentage = Math.Round(a.Percentage)
                                  };
0 голосов
/ 31 августа 2010

Я думаю, это то, что вы ищете.Вероятно, его можно было бы очистить и оптимизировать, но он принимает наибольшее расстояние при округлении при решении округлить число другим способом.

    static List<double> Round2(List<double> actualAllocations)
    {
        List<double> actual = new List<double>();
        actual.AddRange(actualAllocations);

        List<double> rounded = new List<double>();
        foreach (var a in actual)
            rounded.Add(Math.Round(a));

        if (rounded.Sum() == 100)
        {
            return rounded;
        }
        else
        {
            bool roundUp = rounded.Sum() < 100;

            for (int i = 0; i < Math.Abs(100 - rounded.Sum()); i++)
            {
                var index = actual.IndexOf(
                    (from a in actual
                    orderby Math.Abs(Math.Round(a) - a) descending
                    select a).First());

                if (roundUp)
                    actual[index]++;
                else
                    actual[index]--;
            }
        }

        rounded.Clear();
        foreach (var a in actual)
            rounded.Add(Math.Round(a));

        return rounded;
    }
...