Search code examples
c#arrayslinqmathenumerable

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


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?


Solution

  • 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