Split value in # randomly sized parts using C# (with a max value per part)

366 views Asked by At

I'm trying to do as the title suggests. Take in a value like 100 with a part number like 5. Then split 100 into 5 parts that add up to 100. Each part being random. So a result would be like 20, 25, 5, 40, 10. It would return a list/array. This is the code I'm currently using thanks to a post here from 10+ years ago.

        List<int> a = new List<int>();
        a = Enumerable.Repeat(0, numOfStats - 1)        // Seq with (n-1) elements...
                          .Select(x => Random.Range(1, points))  // ...mapped to random values
                          .Concat(new[] { 0, points })
                          .OrderBy(x => x)
                          .ToArray()
                          .ToList();

        return a.Skip(1).Select((x, i) => x - a[i]).ToList();

numStats is the division number and points is the total value that will be split.

The only problem is that I need to make sure each part is no more than a certain number. So each part would be max 30 for example. Anyone know how I can edit this to make sure there is a clamp on the parts?

1

There are 1 answers

7
AlanK On BEST ANSWER

Give up on trying to do it in one line (and program defensively, there are quite a few edge cases)

EDIT Added SplitValue2() (an improvement over SplitValue()) and Shuffle()

static List<int> SplitValue(int value, int nParts, int maxPart)
{
  if (maxPart < value / nParts) throw new Exception("Not possible");
  var rng = new Random();
  var lst = new List<int>();
  var total = 0;
  //  Initial random allocation
  for (var i = 0; i < nParts; i++)
  {
    var part = rng.Next(Math.Min(maxPart + 1, value - total)); // upper bound is exclusive
    lst.Add(part);
    total += part;
    //  Need more room
    if (total == value && i + 1 < nParts)
      for (var j = i; j >= 0; j--)
      {
        if (lst[i] > 0)
        {
          lst[i] -= 1;
          total--;
        }
      }
  }
  //  Top-up
  for (var i = 0; i < nParts && total < value; i++)
  {
    var topup = Math.Min(maxPart - lst[i], value - total);
    lst[i] += topup;
    total += topup;
  }
  if (total != 100) throw new Exception("Failed");
  return lst;
}

static List<int> SplitValue2(int valueToSplit, int nParts, int maxPart)
{
  var result = new int[nParts];
  var prng = new Random();
  if (maxPart < valueToSplit / nParts) throw new Exception("Not possible");
  var remaining = valueToSplit;
  while (remaining > 0)
  {
    for (var i = 0; i < nParts && remaining > 0; i++)
    {
      var next = prng.Next(0, Math.Min(maxPart - result[i], remaining) + 1);
      result[i] += next;
      remaining -= next;
    }
  }
  return Shuffle(result.ToList());
}

static List<int> Shuffle(List<int> list)
{
  if (list == null) throw new Exception("nothing to do");
  var cpy = new List<int>(list);
  var prng = new Random();
  var ret = new List<int>();
  var len = cpy.Count;
  if (len == 0) return ret;
  var lenRem = len;
  while (lenRem > 1)
  {
    var select = prng.Next(lenRem);
    ret.Add(cpy[select]);
    cpy.RemoveAt(select);
    lenRem--;
  }
  ret.Add(cpy[0]);
  return ret;
}

Console.WriteLine("Split 1");
//Console.WriteLine(string.Join(',', SplitValue(100,5,10)));
Console.WriteLine(string.Join(',', SplitValue(100,5,20)));
Console.WriteLine(string.Join(',', SplitValue(100,5,30)));
Console.WriteLine(string.Join(',', SplitValue(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue(100,5,150)));
Console.WriteLine("\nSplit 2");
//Console.WriteLine(string.Join(',', SplitValue2(100,5,10)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,20)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,30)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,70)));
Console.WriteLine(string.Join(',', SplitValue2(100,5,150)));

I don't claim that this is bug-free, you will need to test (and curious to see what other ideas are offered)

Sample output

Split 1
20,20,20,20,20
30,30,15,15,10
69,27,2,2,0
44,24,22,1,9
85,9,6,0,0

Split 2
20,20,20,20,20
21,30,11,25,13
3,4,64,4,25
8,10,56,13,13
3,0,1,86,10