NaterDawg
NaterDawg

Reputation: 59

How to group in C# linq based on sequential values of a property and another property value?

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

Answers (3)

MJVC
MJVC

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

Ivan Stoev
Ivan Stoev

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

GregorMohorko
GregorMohorko

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

Related Questions