简体   繁体   中英

C# algorithm refactor splitting an array into 3 parts?

I have an IEnumerable and I wanted to split the data across 3 columns using the following business logic. if 3 or less items, 1 item per column, anything else I wanted to divide the total items by 3 split the leftovers (either 1 or 2 items) between the first two columns. Now this is pretty ugly but it does the job. I'm looking for tips to leverage linq a little better or possibly eliminate the switch statement. Any advice or tips that improve the code are appreciated.

var numItems = items.Count;

            IEnumerable<JToken> col1Items,
                                col2Items, 
                                col3Items;


            if(numItems <=3)
            {
                col1Items = items.Take(1);
                col2Items = items.Skip(1).Take(1);
                col3Items = items.Skip(2).Take(1);

            } else {

                int remainder = numItems % 3,
                    take = numItems / 3,
                    col1Take, 
                    col2Take, 
                    col3Take;

                switch(remainder)
                {
                    case 1:
                        col1Take = take + 1;
                        col2Take = take;
                        col3Take = take;
                        break;
                    case 2:
                        col1Take = take + 1;
                        col2Take = take + 1;
                        col3Take = take;
                        break;
                    default:
                        col1Take = take;
                        col2Take = take;
                        col3Take = take;
                        break;

                }

                col1Items = items.Take(col1Take);
                col2Items = items.Skip(col1Take).Take(col2Take);
                col3Items = items.Skip(col1Take + col2Take).Take(col3Take);

Ultimately I am using these in a mvc Razor view

<div class="widgetColumn">
                @Html.DisplayFor(m => col1Items, "MenuColumn")                       
            </div> 

            <div class="widgetColumn">
                @Html.DisplayFor(m => col2Items, "MenuColumn")                       
            </div> 

            <div class="widgetColumn">
                @Html.DisplayFor(m => col3Items, "MenuColumn")                       
            </div>  

In my first attempt I want to get rid of the colNItems and colNTake variables but i can't figure out the correct algorithm to make it work the same.

for (int i = 1; i <= 3; i++ )
            {
                IEnumerable<JToken> widgets = new List<JToken>();
                var col = i;
                switch(col)
                {
                    case 1:
                       break;
                    case 2:
                        break;
                    case 3:
                        break;
                }
            }

Are the columns fixed-width? If so, then there's no need to do anything special with your collection. Just rely on the browser to do it for you. Have an outer container that has the overall width of the 3 columns, then just fill it with a div for each item (and float left). Set your inner containers to have a width exactly 1/3 of the outer container.

Here's a quick fiddle

Here's a quick hint at the style

div#outer{
    width:300px;    
}

div#outer > div{
    width:100px;
    float:left;    
}

Can't you just do something like?

int len = numItems / 3;
int rem = numItems % 3;

int col1Take = len + (rem > 0 ? 1 : 0);
int col2Take = len + (rem > 1 ? 1 : 0);
int col3Take = len;

Edit:

A more generic solution that works for any number of columns ( COLUMNS ) would be:

int len = numItems / COLUMNS;
int rem = numItems % COLUMNS;

foreach (var i in Enumerable.Range(0, COLUMNS)) {
  colTake[i] = len + (rem > i ? 1 : 0);
}

You could generalize:

int cols = 3;
IEnumerable<JToken> colItems[3]; // you can make this dynamic of course

int rem = numItems % cols;
int len = numItems / cols;

for (int col=0; col<cols; col++){
    int colTake = len;
    if (col < rem) colTake++;
    colItems[col] = items.Skip(col*len).Take(colTake);
}

Haven't tested, but this should work for any number of columns.

Also whenever you need variables col1, col2, col3 think of col[0], col[1], col[2].

So you want the first n/3 items in the first column, next n/3 items in the 2nd column, etc.

var concreteList = items.ToList();
var count = concreteList.Count;
var take1 = count/3 + (count % 3 > 0 ? 1 : 0);
var take2 = count/3 + (count % 3 > 1 ? 1 : 0);

var col1 = concreteList.Take(take1);
var col2 = concreteList.Skip(take1).Take(take2);
var col3 = concreteList.Skip(take1 + take2);

I make a concrete list in order to avoid iterating the Enumerable multiple times. For example, if you had:

items = File.ReadLines("foo.txt");

Then you wouldn't be able to iterate it multiple times.

If you want to fill the columns round-robin you can use:

int numColumns = 3;

var result = Enumerable.Range(1,numColumns).Select(c =>
      items.Where((x,ix) => ix % numColumns == c-1).ToArray()
   );

it might help

        IEnumerable<object> items = new Object[]{ "1", "2", "3", "4", "5", "6", "7","8", "9", "10", "11", "12","13", "14" };

        IEnumerable<object> col1Items = new List<object>(),
                            col2Items = new List<object>(), 
                            col3Items = new List<object>();

        Object[] list = new Object[]{col1Items, col2Items, col3Items};
        int limit = items.Count()/3;
        int len = items.Count();
        int col;            

        for (int i = 0; i < items.Count(); i++ )
        {                
            if (len == 3) col = i;
            else col = i / limit;

            if (col >= 3) col = i%limit ;

            ((IList<object>)(list[col])).Add( items.ElementAt(i));

        }

This isn't fast, but it'll do the trick:

var col1Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 0).Select(o => o.Value);
var col2Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 1).Select(o => o.Value);
var col3Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 2).Select(o => o.Value);

It uses the version of Select that includes an index parameter. You could use a GroupBy to speed this up a bit at the cost of a few lines of code.

If you want to go across then down see answer below this one, if you want to go down then across you can do it like this (use this code with the test below to see it working instead of the var result line there.:

var curCol = 0;
var iPer = items.Count() / 3;
var iLeft = items.Count() % 3;
var result = items.Aggregate(
               // object that will hold items
               new {  
                      cols = new List<ItemElement>[3] { new List<ItemElement>(), 
                                                        new List<ItemElement>(), 
                                                        new List<ItemElement>(), },
                          },
               (o, n) => {
                 o.cols[curCol].Add(n);

                 if (o.cols[curCol].Count() > iPer + (iLeft > (curCol+1) ? 1:0))
                   curCol++;

                 return new {
                   cols = o.cols
                };
             });

You can do this with aggregate. It would look like this:

void Main()
{
  List<ItemElement> items = new List<ItemElement>() { 
           new ItemElement() { aField = 1 },
           new ItemElement() { aField = 2 },
           new ItemElement() { aField = 3 },
           new ItemElement() { aField = 4 },
           new ItemElement() { aField = 5 },
           new ItemElement() { aField = 6 },
           new ItemElement() { aField = 7 },
           new ItemElement() { aField = 8 },
           new ItemElement() { aField = 9 }
  };

  var result = 
    items.Aggregate(
      // object that will hold items
      new {  
        cols = new List<ItemElement>[3] { new List<ItemElement>(), 
                                          new List<ItemElement>(), 
                                          new List<ItemElement>(), },
        next = 0 },
     // aggregate
     (o, n) => {
       o.cols[o.next].Add(n);

       return new {
         cols = o.cols,
         next = (o.next + 1) % 3
       };
    });
  result.Dump();
}

public class ItemElement 
{
   public int aField { get; set; }
}

You end up with an object with an array of 3 lists (one for each column).

This example will run as is in linqPad. I recomment linqPad for these kind of POC tests. (linqPad.com)

LinqLib (nuget: LinqExtLibrary) has an overload of ToArray() that does it:

using System.Collections.Generic;
using System.Linq;
using LinqLib.Array;

...

    public void TakeEm(IEnumerable<int> data)
    {
        var dataAry = data as int[] ?? data.ToArray();
        var rows = (dataAry.Length/3) + 1;
        //var columns = Enumerable.Empty<int>().ToArray(3, rows);
        // vvv These two lines are the ones that re-arrange your array
        var columns = dataAry.ToArray(3, rows);
        var menus = columns.Slice();
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM