Reputation: 59
Here is an IEnumerable of a class.
IEnumerable<Card> cardList = new List<Card> {
new Card { CardNumber = 1234, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1235, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1236, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1237, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1238, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1239, Amount = 15m, DisplayText = "" },
new Card { CardNumber = 1240, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1241, Amount = 10m, DisplayText = "" },
new Card { CardNumber = 1242, Amount = 25m, DisplayText = "" },
new Card { CardNumber = 1243, Amount = 25m, DisplayText = "" },
new Card { CardNumber = 1244, Amount = 25m, DisplayText = "" },
new Card { CardNumber = 1245, Amount = 25m, DisplayText = "" }
};
What I want to accomplish is to group the list by amount and sequential card number and the groups have at least 4 cards in them or they don't get grouped. Here is an example of what I'm trying to achieve. The results would be another IEnumerable and contain this
Card { CardNumber = null, Amount = 10m, DisplayText = "1234 - 1238" },
Card { CardNumber = null, Amount = 15m, DisplayText = "1239" },
Card { CardNumber = null, Amount = 10m, DisplayText = "1240" },
Card { CardNumber = null, Amount = 10m, DisplayText = "1241" },
Card { CardNumber = null, Amount = 25m, DisplayText = "1242 - 1245" }
Hopefully this clear in what I am trying to do. Any help would be much appreciated.
Thanks,
Upvotes: 1
Views: 914
Reputation: 507
You can achieve that by grouping the items making it through your constraints into a new list, and then filling a second one based on the group size:
IList<Card> group = new List<Card>();
IList<Card> result = new List<Card>();
for (int i = 0, j = 0; i < cardList.Count - 1; i++)
{
group.Clear();
group.Add(cardList[i]);
for (j = i + 1; j < cardList.Count; j++, i++)
{
if (cardList[j].Amount == cardList[i].Amount && cardList[j].CardNumber - cardList[i].CardNumber == 1)
{
group.Add(cardList[j]);
}
else break;
}
if (4 > group.Count)
{
foreach (var item in group)
{
result.Add(new Card { Amount = item.Amount, DisplayText = item.CardNumber.ToString() });
}
}
else
{
result.Add(new Card { Amount = group[0].Amount, DisplayText = string.Format("{0} - {1}", group[0].CardNumber, group.Last().CardNumber) });
}
}
The results using LINQPad:
CardNumber Amount DisplayText
null 10 1234 - 1238
null 15 1239
null 10 1240
null 10 1241
null 25 1242 - 1245
I like this better than the over-complicated Linq code.
Upvotes: 0
Reputation: 205899
It's not possible with pure LINQ operators. But it's possible using a mixed standard and custom LINQ like extension method approach.
Let create a custom method which allows us to split a sequence based on predicate receiving previous and current elements:
public static class Extensions
{
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, Func<T, T, bool> splitOn)
{
using (var e = source.GetEnumerator())
{
for (bool more = e.MoveNext(); more; )
{
var last = e.Current;
var group = new List<T> { last };
while ((more = e.MoveNext()) && !splitOn(last, e.Current))
group.Add(last = e.Current);
yield return group;
}
}
}
}
Now you can use the following query to accomplish your goal:
var result = cardList.OrderBy(c => c.CardNumber)
.Split((prev, next) => prev.Amount != next.Amount || prev.CardNumber + 1 != next.CardNumber)
.SelectMany(g => g.Count() >= 4 ?
new [] { new Card { Amount = g.First().Amount, DisplayText = g.First().CardNumber + " - " + g.Last().CardNumber } } :
g.Select(c => new Card { Amount = c.Amount, DisplayText = c.CardNumber.ToString() }));
The OrderBy
followed by the custom Split
does the initial grouping. The remaining tricky part is how to group / ungroup elements based on the Count
criteria, which is achieved by conditional SelectMany
method producing a single item in one case (by selecting a single item array), or flattening the group in the other case by using an inner Select
.
Upvotes: 2
Reputation: 2877
I don't think something like this can be done using LINQ.
But this can be easily achieved without LINQ, like this: (I'm assuming that cardList
is ordered by CardNumber
)
List<Card> result = new List<Card>();
Func<List<Card>,List<Card>> bufferToResult = (buf) =>
{
List<Card> res = new List<Card>();
if(buf.Count >= 4) {
string text = buf[0].CardNumber + " - " + buf[buf.Count-1].CardNumber;
Card newCard = new Card { Amount = buf[0].Amount, DisplayText = text };
res.Add(newCard);
} else {
foreach(Card c in buf) {
Card newCard = new Card { Amount = c.Amount, DisplayText = c.CardNumber.ToString() };
res.Add(newCard);
}
}
return res;
};
List<Card> buffer = new List<Card>();
for(int i=0; i<cardList.Count(); ) {
Card card = cardList.ElementAt(i);
if(buffer.Count == 0) {
buffer.Add(card);
i++;
} else if(card.Amount == buffer[0].Amount) {
buffer.Add(card);
i++;
} else {
result.AddRange(bufferToResult(buffer));
buffer.Clear();
}
}
if(buffer.Count > 0)
result.AddRange(bufferToResult(buffer));
Upvotes: 0