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;
}
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].
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);
}
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()
);
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)
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));
}
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();
}
Related
I have a list of integers, which I would like to split into 2 or more lists based upon meeting a certain criteria. For example:
List<int> myList = new List<int>();
myList.Add(100);
myList.Add(200);
myList.Add(300);
myList.Add(400);
myList.Add(200);
myList.Add(500);
I would like to split the list into several lists, each of which contains all items which total <= 600. In the above, it would then result in 3 separate List objects.
List 1 would contain 100, 200 300
List 2 would contain 400, 200
List 3 would contain 500
Ideally, I'd like it to be a single LINQ statement.
Although doable, this is an excellent example of what LINQ is not for. Check yourself.
Having
var myList = new List<int> { 100, 200, 300, 400, 200, 500, };
int maxSum = 600;
"Pure" LINQ (the power of Aggregate)
var result = myList.Aggregate(
new { Sum = 0, List = new List<List<int>>() },
(data, value) =>
{
int sum = data.Sum + value;
if (data.List.Count > 0 && sum <= maxSum)
data.List[data.List.Count - 1].Add(value);
else
data.List.Add(new List<int> { (sum = value) });
return new { Sum = sum, List = data.List };
},
data => data.List)
.ToList();
A normal (non LINQ) implementation of the above
var result = new List<List<int>>();
int sum = 0;
foreach (var value in myList)
{
if (result.Count > 0 && (sum += value) <= maxSum)
result[result.Count - 1].Add(value);
else
result.Add(new List<int> { (sum = value) });
}
For completeness (and some fun), a "Hackish" LINQ (the power of closures and C# operators)
int sum = 0, key = -1;
var result = myList.GroupBy(x => key >= 0 && (sum += x) <= maxSum ? key : ++key + (sum = x) * 0, (k, e) => e.ToList()).ToList();
Here is the solution to your problem. I am not sure if that is the best case solver but it will surely do the job:
List<int> First = myList.Where(x => x <= 300).ToList();
List<int> Second = myList.Where(x => x == 400 || x == 200).ToList();
List<int> Third = myList.Where(x => x == 500).ToList();
It does query through the list and checks for values that meets the requirements then it will convert IEnumerable into the List.
This will do what you want for a list any size but maybe not as short as you are looking for. You would have to write a LINQ extension method to shorten it but then it become a bit more complicated.
List<int> myList = new List<int>();
myList.Add(100);
myList.Add(200);
myList.Add(300);
myList.Add(400);
myList.Add(200);
myList.Add(500);
var result = new List<List<int>>();
var skip = 0;
while (skip < myList.Count)
{
var sum = 0;
result.Add(myList.Skip(skip).TakeWhile(x =>
{
sum += x;
return sum <= 600;
}).ToList());
skip += result.Last().Count();
}
I want to merge 2 lists into one list, however, it have to be in special order.
For example, I have a list of type A:
{ A, A ,A ,A ,A, A .... }
And also a list of type B:
{B, B, B , B ....}
And desired result should be like this:
{A, A, B, A, A, B, A, A, B}
The merge should take the 2 items from list A, and then 1 item from List B.
One thing to note is, in case of one list gets empty, fill all the rest items with the second list's items.
I trying to find an elegant way to do it with LINQ.
Heres my code, but it's a bit long and I hope there is a better way to do it via linq:
Thanks a lot.
public IList<PersonBase> Order(IList<Person1> people1, IList<Person2> people2)
{
if (people1.IsNullOrEmpty())
return people2;
if (people2.IsNullOrEmpty())
return people1;
List<PersonBase> orderedList = new List<PersonBase>();
var people1Count = 0;
var people2Count = 0;
while (people2Count < people2.Count || people1Count < people1.Count)
{
var people1ToAdd = tags.Skip(people1Count).Take(1).ToList();
people1Count = people1.Count();
orderedList.AddRange(people1ToAdd);
if (people1Count >= people1.Count)
{
orderedList.AddRange(people2.Skip(people2Count));
break;
}
var people2ToAdd = people2.Skip(peopleCount).Take(2).ToList();
people2Count = people2.Count();
orderedList.AddRange(people2ToAdd);
if (people2Count >= people2.Count)
{
orderedList.AddRange(people1.Skip(people1Count));
break;
}
}
return orderedList;
}
This is pretty horrible code but does what you want it too. Basically we keep track of each lists index and have an int that keeps track of which list to use to populate the result array.
List<int> list1 = new List<int>() { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
List<int> list2 = new List<int>() { 2, 2, 2, 2, 2 };
int list1Counter = 0;
int list2Counter = 0;
int arraychecker = 1;
int[] resultArray = new int[list1.Count + list2.Count];
for (int i = 0; i < resultArray.Length; i++)
{
if (list1Counter < list1.Count && list2Counter < list2.Count)
{
if (arraychecker == 1 || arraychecker == 2)
{
resultArray[i] = list1[list1Counter];
list1Counter++;
arraychecker++;
}
else
{
resultArray[i] = list2[list2Counter];
list2Counter++;
arraychecker = 1;
}
}
else if (list1Counter < list1.Count)
{
resultArray[i] = list1[list1Counter];
list1Counter++;
}
else
{
resultArray[i] = list2[list2Counter];
list2Counter++;
}
}
You can calculate the index for each item and then order by that index.
var mergedList =
listA.Select((item, index) =>
new { Index = index / 2 * 3 + (i % 2), Item = item})
.Concat(listB.Select((item, index) =>
new { Index = i * 3 + 2, Item = item}))
.OrderBy(x => x.Index)
.Select(x => x.Item)
.ToList();
Or write a method. This is more efficient, since it doesn't need to sort; It just runs through each list once.
static IEnumerable<T> Alternate<T>(IEnumerable<T> sourceA, IEnumerable<T> sourceB) {
using (IEnumerator<T> eA = sourceA.GetEnumerator(), eB = sourceB.GetEnumerator()) {
bool aHasItems = true, bHasItems = true;
while (aHasItems || bHasItems) {
if (eA.MoveNext()) yield return eA.Current;
if (aHasItems = eA.MoveNext()) yield return eA.Current;
if (bHasItems = eB.MoveNext()) yield return eB.Current;
}
}
}
This is the best I could do:
string[] test1 = { "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A" };
string[] test2 = { "B", "B" };
string[] result = test2.SelectMany((value, key) => test1.Skip(key * 2).Take(2).Concat(test2.Skip(key).Take(1))).ToArray();
result = result.Concat(test1.Skip(result.Length / 3 * 2).Take(test1.Length - result.Length / 3 * 1)).ToArray();
This will take 2 from array1 then 1 from array 2 then add whats left of the longer one. Output:
AABAABAAAAAAAAA
I have a list of data which contains of random data with combination of string and number:
List<String> Data1 = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
I want this data to arrange into series like this:
1001A - 1004A, 1007A - 1009A, 1015A, 1016A
for every more than 2 counts of data series the output shall be have "-" between the first count and the last count of series, the other non series data will be just added to the last part and all together will separated by ",".
I'd already made some codes only to arrange the data series by the last char of it:
string get_REVISIONMARK = "A";
var raw_serries = arrange_REVISIONSERIES.Where(p => p[p.Length - 1].ToString() == get_REVISIONMARK) .OrderBy(p => p[p.Length - 1) .ThenBy(p => p.Substring(0, p.Length - 1)).ToList();
just ignore the last char I'd already have function for that, and my problem only about the arrangement of the numbers, the length of data is not fixed. for other example of output "1001A - 1005A, 301A, 32A"
I had another sample of my codes this works fine to me, but for me its so lazy code.
for (int c1 = 0; c1 < list_num.Count; c1++)
{
if (list_num[c1] != 0)
{
check1 = list_num[c1];
for (int c2 = 0; c2 < list_num.Count; c2++)
{
if (check1 == list_num[c2])
{
list_num[c2] = 0;
check1 += 1;
list_series.Add(arrange_REVISIONSERIES[c2]);
}
}
check1 = 0;
if (list_series.Count > 2)
{
res_series.Add(list_series[0] + " to " +list_series[list_series.Count - 1]);
list_series.Clear();
}
else
{
if (list_series.Count == 1)
{
res_series.Add(list_series[0]);
list_series.Clear();
}
else
{
res_series.Add(list_series[0] + "," + list_series[1]);
list_series.Clear();
}
}
}
}
var combine_res = String.Join(",", res_series);
MessageBox.Show(combine_res);
this codes work fine for the series number ...
A possible solution (working with current set of values), Please follow the steps below
Declare a class level string list as
public List<String> data_result = new List<string>();
Create a function to iterate through input string list (input string declared inside, named 'data')
public void ArrangeList()
{
List<String> data = new List<string>() { "1001A", "1002A", "1003A",
"1004A", "1015A", "1016A", "1007A", "1008A", "1009A", "1017A" };
List<int> data_int = data.Select(a => Convert.ToInt32(a.Substring(0,
a.Length - 1))).OrderBy(b => b).ToList();
int initializer = 0, counter = 0;
int finalizer = 0;
foreach (var item in data_int)
{
if (initializer == 0)
{ initializer = item; continue; }
else
{
counter++;
if (item == initializer + counter)
finalizer = item;
else
{
LogListing(initializer, finalizer);
initializer = item;
finalizer = item;
counter = 0;
}
}
}
LogListing(initializer, finalizer);
}
Create a function which just logs the result into data_result string list.
public void LogListing(int initializer, int finalizer)
{
if (initializer != finalizer)
{
if (finalizer == initializer + 1)
{
data_result.Add(initializer + "A");
data_result.Add(finalizer + "A");
}
else
data_result.Add(initializer + "A - " + finalizer + "A");
}
else
data_result.Add(initializer + "A");
}
It perfectly generates the result list as
Thumb-up if you like
A linqy solution:
char get_REVISIONMARK = 'A';
var res = arrange_REVISIONSERIES.Select(s => new { Rev = s[s.Length - 1], Value = int.Parse(s.Substring(0, s.Length - 1)), Org = s })
.Where(d => d.Rev == get_REVISIONMARK).OrderBy(d => d.Value)
.Select((val, ind) => new { Index = ind, Org = val.Org, Value = val.Value }).GroupBy(a => a.Value - a.Index)
.Select(gr=>gr.ToList()).OrderBy(l=>l.Count > 2 ? 0 : 1 ).Aggregate(new List<string>(), (list, sublist) =>
{
if (sublist.Count > 2)
list.Add(sublist[0].Org + " - " + sublist[sublist.Count - 1].Org);
else
list.AddRange(sublist.Select(a => a.Org));
return list;
});
The first lines are basically the same as the code you already have (filter on revision and sort), but with the difference that the subvalues are stored in an anonymous type. You could do the same on the pre ordered list, but since splitting the string would be done twice I've included it in the total.
Then a select with index (.Select((val, ind) =>) is made to get value/index pairs. This is done to be able to get the sequences based on an old t-sql row_number trick: for each 'group' the difference between value and index is the same .GroupBy(a => a.Value - a.Index)
After that, normally you'd be as good as done, but since you only want to make sequences of 2 and longer, we make sublists out of the groupby values and do the ordering beforehand to make sure the ranges come for the eventual single elements .Select(gr=>gr.ToList()).OrderBy(l=>l.Count > 2 ? 0 : 1 )
Finally, the list is created of the groups. Several options, but I like to use Aggregate for that. The seed is the resulting list, and the aggregate simply adds to that (where subranges > 2 are cummulated and for single elements and pairs, the single elements are added)
I'm making two assumptions:
The list is already ordered
The non-numeric characters can be ignored
You will get the results in the results variable:
void Main()
{
List<String> Data1 = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
var accu = new List<List<Tuple<int, string>>>();
foreach (var data in Data1)
{
if (accu.Any(t => t.Any(d => d.Item1 == (ToInt(data) - 1))))
{
var item = accu.First(t => t.Any(d => d.Item1 == (ToInt(data) - 1)));
item.Add(new Tuple<int, string>(ToInt(data), data));
}
else
{
accu.Add(new List<Tuple<int, string>>{ new Tuple <int, string>(ToInt(data), data)});
}
}
var results = new List<string>();
results.AddRange(accu.Where(g => g.Count > 2).Select(g => string.Format("{0} - {1}", g.First().Item2, g.Last().Item2)));
results.AddRange(accu.Where(g => g.Count <= 2).Aggregate(new List<string>(), (total, current) => { total.AddRange(current.Select(i => i.Item2)); return total; } ));
}
private static Regex digitsOnly = new Regex(#"[^\d]");
public static int ToInt(string literal)
{
int i;
int.TryParse(digitsOnly.Replace(literal, ""), out i);
return i;
}
So given your starting data:
List<String> arrange_REVISIONSERIES = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
I do this first:
var splits =
arrange_REVISIONSERIES
.Select(datum => new
{
value = int.Parse(datum.Substring(0, datum.Length - 1)),
suffix = datum.Substring(datum.Length - 1, 1),
})
.OrderBy(split => split.suffix)
.ThenBy(split => split.value)
.ToArray();
That's basically the same as your raw_serries, but orders the number part as a number. It seems to me that you need it as a number to make the range part work.
I then do this to compute the groupings:
var results =
splits
.Skip(1)
.Aggregate(
new[]
{
new
{
start = splits[0].value,
end = splits[0].value,
suffix = splits[0].suffix
}
}.ToList(),
(a, s) =>
{
if (a.Last().suffix == s.suffix && a.Last().end + 1 == s.value)
{
a[a.Count - 1] = new
{
start = a.Last().start,
end = s.value,
suffix = s.suffix
};
}
else
{
a.Add(new
{
start = s.value,
end = s.value,
suffix = s.suffix
});
}
return a;
})
.Select(r => r.start == r.end
? String.Format("{0}{1}", r.end, r.suffix)
: (r.start + 1 == r.end
? String.Format("{0}{2}, {1}{2}", r.start, r.end, r.suffix)
: String.Format("{0}{2} - {1}{2}", r.start, r.end, r.suffix)))
.ToArray();
And finally, this to create a single string:
var result = String.Join(", ", results);
That gives me:
1001A - 1004A, 1007A - 1009A, 1015A, 1016A
This code nicely works with data containing different suffixes.
I am trying to create a List of Anonymous types as shown below but I am making mistake somewhere
for (int i = 0; i < 10; i++)
{
var list = new[]
{
new { Number = i, Name = string.Concat("name",i) }
};
}
E.g.
var o1 = new { Id = 1, Name = "Name1" };
var o2 = new { Id = 2, Name = "Name2" };
var list = new[] { o1, o2 }.ToList();
how to do the same at runtime?
no error...but the collection is always 1
That is because your are creating a new list in each iteration
You can try it like:
var list = new[] { new { Number = 0, Name = "Name1" } }.ToList(); //just to create a
//list of anonymous type object
list.Clear();
for (int i = 0; i < 10; i++)
{
list.Add(new { Number = i, Name = string.Concat("name",i) });
}
Or one way to do that would be to use List<Object> like:
List<object> list = new List<object>();
for (int i = 0; i < 10; i++)
{
list.Add(new { Number = i, Name = string.Concat("name",i) });
}
Or you can use Enumerable.Range like
var list = Enumerable.Range(0, 10)
.Select(i => new { Number = i, Name = "SomeName" })
.ToList();
Were you thinking of something like the following (using LINQ):
var anonList = Enumerable
.Range(1, 10)
.Select(i => new {
ID = i,
Name = String.Format("Name{0}", i)
});
You could of course replace Enumerable.Range() with anything that give you a list to select from.
You need a List object to store it.
List<Object> o = new List<Object>();
for (int i = 0; i < 10; i++)
{
o.Add(new { Number = i, Name = string.Concat("name",i) });
}
o.Dump();
Define a list first then store the value in For loop
List<Object> NewList = new List<Object>();
for (int i = 0; i < 10; i++)
{
NewList.Add(
{
new { Number = i, Name = string.Concat("name",i) }
});
}
int i = 0;
while(i < 10)
{
list.Add(new { Number = i, Name = string.Concat("name",i) });
i++;
}
I have a list of lists of dynamic which is currently being filtered through this:
var CPUdataIWant = from s in rawData
where s.stat.Contains("CPU")
select s;
//CPUDataIWant is a List<List<dynamic>>.
I have 86000 values in each inner list.
And what I need to do, is group the values into groups of 3, select the max of that group, and insert that into another list of List of dynamic, or just filter it out of CPUDataIWant.
So an example of what I want would be:
Raw data = 14,5,7,123,5,1,43,87,9
And my processed value would be:
ProceData = [14,5,7], [123,5,1], [43,87,9]
ProceData = [14,123,87]
Doesn't have to be using linq but the easier the better.
EDIT: Ok I explained what a wanted a bit poorly.
here's what I have:
List<List<object>>
In this List, I'll have X amount of Lists called A.
In A I'll have 86000 values, let's say they're ints for now.
What I'd like, is to have
List<List<object>>
But instead of 86000 values in A, I want 28700, which would be made from the max of every 3 values in A.
IEnumerable<int> filtered = raw.Select((x, i) => new { Index = i, Value = x }).
GroupBy(x => x.Index / 3).
Select(x => x.Max(v => v.Value));
or, if you plan to use it more often
public static IEnumerable<int> SelectMaxOfEvery(this IEnumerable<int> source, int n)
{
int i = 0;
int currentMax = 0;
foreach (int d in source)
{
if (i++ == 0)
currentMax = d;
else
currentMax = Math.Max(d, currentMax);
if (i == n)
{
i = 0;
yield return currentMax;
}
}
if (i > 0)
yield return currentMax;
}
//...
IEnumerable<int> filtered = raw.SelectMaxOfEvery(3);
Old-school way of doing things makes it quite simple (although it's not as compact as LINQ):
// Based on this spec: "CPUDataIWant is a List<List<dynamic>>"
// and on the example, which states that the contents are numbers.
//
List<List<dynamic>> filteredList = new List<List<dynamic>>();
foreach (List<dynamic> innerList in CPUDataIWant)
{
List<dynamic> innerFiltered = new List<dynamic>();
// if elements are not in multiples of 3, the last one or two won't be checked.
for (int i = 0; i < innerList.Count; i += 3)
{
if(innerList[i+1] > innerList[i])
if(innerList[i+2] > innerList[i+1])
innerFiltered.Add(innerList[i+2]);
else
innerFiltered.Add(innerList[i+1]);
else
innerFiltered.Add(innerList[i]);
}
filteredList.Add(innerFiltered);
}
This should give the desired result:
var data = new List<dynamic> { 1, 2, 3, 3, 10, 1, 5, 2, 8 };
var firsts = data.Where((x, i) => i % 3 == 0);
var seconds = data.Where((x, i) => (i + 2) % 3 == 0);
var thirds = data.Where((x, i) => (i + 1) % 3 == 0);
var list = firsts.Zip(
seconds.Zip(
thirds, (x, y) => Math.Max(x, y)
),
(x, y) => Math.Max(x, y)
).ToList();
List now contains:
3, 10, 8
Or generalized to an extension method:
public static IEnumerable<T> ReduceN<T>(this IEnumerable<T> values, Func<T, T, T> map, int N)
{
int counter = 0;
T previous = default(T);
foreach (T item in values)
{
counter++;
if (counter == 1)
{
previous = item;
}
else if (counter == N)
{
yield return map(previous, item);
counter = 0;
}
else
{
previous = map(previous, item);
}
}
if (counter != 0)
{
yield return previous;
}
}
Used like this:
data.ReduceN(Math.Max, 3).ToList()
If you felt a need to use Aggregate you could do it like this:
(tested wiht LinqPad)
class Holder
{
public dynamic max = null;
public int count = 0;
}
void Main()
{
var data = new List<dynamic>
{new { x = 1 }, new { x = 2 }, new { x = 3 },
new { x = 3 }, new { x = 10}, new { x = 1 },
new { x = 5 }, new { x = 2 }, new { x = 1 },
new { x = 1 }, new { x = 9 }, new { x = 3 },
new { x = 11}, new { x = 10}, new { x = 1 },
new { x = 5 }, new { x = 2 }, new { x = 12 }};
var x = data.Aggregate(
new LinkedList<Holder>(),
(holdList,inItem) =>
{
if ((holdList.Last == null) || (holdList.Last.Value.count == 3))
{
holdList.AddLast(new Holder { max = inItem, count = 1});
}
else
{
if (holdList.Last.Value.max.x < inItem.x)
holdList.Last.Value.max = inItem;
holdList.Last.Value.count++;
}
return holdList;
},
(holdList) => { return holdList.Select((h) => h.max );} );
x.Dump("We expect 3,10,5,9,11,12");
}