0

I have a custom object Deck that has a List<Card> cards between its properties. Each Card has its own List<string> colors denoting the color of the card using one or more capital letters like such [W, U, B, R, G].

What I need to do is sort the cards list based on the colors list in a way that I get first all the cards from one color and so on for each color; with cards having more than one color I'd like them being sorted based on a custom priority list (like if it's W and U put it between W cards) but I realize this is even more complex so it's not really a necessity for me.

What I tried to do is

deck.cards = deck.cards.OrderBy(x => x.colors).ToList();

but I receive an error stating that at least an object needs to implement ICompare.

What can I do to sort my deck list? Is it possible to not only sort it like described but also based on a specific order like B before R before G and so on?

nicktheone
  • 77
  • 2
  • 10
  • I don't follow this sentence: "(like if it's W and U put it between W cards)." Some example input and output would probably help. – canton7 Feb 08 '19 at 10:47
  • @canton7 Each card can be one or more colors, with colors denoted by a capital letter. When sorting multi-colored cards if it's possible I'd like them to be inserted between those of a specific color based on a custom "priority" order so if a card is both W and U I'd like for it to be sorted between W cards. I realize this makes the whole thing even more complex so it's not really a necessity, more of a quirk. – nicktheone Feb 08 '19 at 10:50
  • 2
    You could create a `class` implementing `IComparer` and use `List.Sort` method. – Alessandro D'Andria Feb 08 '19 at 10:51
  • That just repeats what you said in your original question. So to take an example, if you have the cards [1, "W"], [2, "W"], [3, "W", "U"], then you want the ["W", "U"] card to be sorted between the "W" cards, so the final result is [1, "W"], [3, "W", "U"], [2, "W"]? – canton7 Feb 08 '19 at 10:52
  • @canton7 Exactly. So if a have like [1, "W"], [2, "W"], [3, "W", "U"], [4, "U"], [5, "U"] I'll sort them in the same order with card 3 being treated as "W" card. – nicktheone Feb 08 '19 at 10:54
  • @AlessandroD'Andria this is exactly what I tried to do before but I can't seem to find an example of what I need because almost everything I talks about comparing numbers while I have a list of strings. Any pointers as to where to look further? – nicktheone Feb 08 '19 at 10:56
  • What do you mean, "I'll sort them in the same order with card 3 being treated as "W" card"? Are you trying to say "If a card has multiple colours, then a single one of those colours shall be selected (the colour that is selected is the one that appears first in a priority list of colours), and the card shall be sorted on the basis of that single highest-priority colour, without taking any of the other colours into account"? – canton7 Feb 08 '19 at 10:57
  • @canton7 Precisely. This is what I tried to say speaking of a custom priority list. – nicktheone Feb 08 '19 at 10:58
  • 2
    I think you need to specify multi-colour behaviour a bit better. You have "If its W and U put it between W only and U only" but what if you had a third colour - eg you have "B", "BU" and "BW". I'd assume B goes first and then BW, but where does BU go? Or is it simply something like that you sort alphabetically with mono-colour first and then multi-colour cards at the end of those with them ordered by second (or third, etc.) colour if appropriate? I find that often once you have clearly and unambiguously explained how you want your sort to work that the code follows from it quite easily... – Chris Feb 08 '19 at 11:22
  • 1
    That having been said I think this is probably complciated enough that you may want to create a custom comparer (ie a `IComparer`). There are questions relating to this that might give you a good start (eg https://stackoverflow.com/questions/14336416/using-icomparer-for-sorting/14336463 ). – Chris Feb 08 '19 at 11:26
  • @Chris Your second example ("Or is it simply something like that you sort alphabetically [...]") is exactly the behavior I'd like. I agree though that a custom IComparer seems the way to do this. I'll take a look at what you linked. – nicktheone Feb 08 '19 at 11:29
  • When I get a chance I'll try to create a full answer... – Chris Feb 08 '19 at 11:33
  • @Chris thanks! I appreciate it. – nicktheone Feb 08 '19 at 12:10
  • @nicktheone: Looks like you've got an answer you are happy with already so I won't bother. :) – Chris Feb 08 '19 at 12:22

2 Answers2

4

Based on the discussion in the comments, when a card has multiple colours, you want to select a single colour (the one that appears first in a priority list), and sort it on that basis.

// Higher-priority colours come first
var coloursPriority = new List<string>() { "W", "U", "B", "R", "G" };

// Turn the card's colour into an index. If the card has multiple colours,
// pick the smallest of the corresponding indexes.
cards.OrderBy(card => card.Colours.Select(colour => coloursPriority.IndexOf(colour)).Min());

Responding to the discussion in the comments: if you wanted to sort the cards based first on their highest-priority colour, and then by their next-highest-priority colour, etc, then this is a reasonably efficient way of doing it:

public class CardColourComparer : IComparer<List<int>>
{
    public static readonly CardColourComparer Instance = new CardColourComparer();
    private CardColourComparer() { }

    public int Compare(List<int> x, List<int> y)
    {
        // Exercise for the reader: null handling

        // For each list, compare elements. The lowest element wins
        for (int i = 0; i < Math.Min(x.Count, y.Count); i++)
        {
            int result = x[i].CompareTo(y[i]);
            if (result != 0)
            {
                return result;
            }
        }

        // If we're here, then either both lists are identical, or one is shorter, but it
        // has the same elements as the longer one.
        // In this case, the shorter list wins
        return x.Count.CompareTo(y.Count);
    }
}

Then

// Higher-priority colours come first
var coloursPriority = new List<string>() { "W", "U", "B", "R", "G" };

cards.OrderBy(card =>
    card.Colours.Select(colour => coloursPriority.IndexOf(colour)).OrderBy(x => x).ToList(),
    CardColourComparer.Instance);

This takes advantage of the fact that OrderBy applies the keySelector delegate to each item once only. We use this to turn each card into a list containing the priority of each of its colours (higher priorities have lower values), ordered with the higher-priority ones first. We then sort these keys, using a custom comparer which compares two of these lists.

Note that this doesn't care about the order of the colours associated with each card: [W, U] will sort the same as [U, W]. To take the order into account (so [W] comes before [W, U] comes before [U, W], do this:

cards.OrderBy(card =>
    card.Colours.Select(colour => coloursPriority.IndexOf(colour)).ToList(),
    CardColourComparer.Instance);
canton7
  • 37,633
  • 3
  • 64
  • 77
  • I get an "No overload for method IndexOf takes 1 arguments" error. – nicktheone Feb 08 '19 at 11:11
  • @nicktheone fixed – canton7 Feb 08 '19 at 11:15
  • your code compiles but unfortunately it doesn't sort the list. – nicktheone Feb 08 '19 at 11:42
  • 1
    Yes it does: https://dotnetfiddle.net/EmlEhx. Obviously for your use-case you'll need to do `deck.cards = deck.cards.OrderBy(...).ToList()`, just as you did in your question. – canton7 Feb 08 '19 at 11:47
  • It seems that I have some issues on my side then. Thanks! – nicktheone Feb 08 '19 at 11:52
  • Note that the ordering for cards which have the same colour is indeterminate - OrderBy does not do a stable sort. You can add a call to `ThenBy` or `ThenByDescending` to add an additional sorting criterion if you need. – canton7 Feb 08 '19 at 11:53
  • Also ordering for cards with multiple colours doesn't work as desired. See https://dotnetfiddle.net/0tW53W for an example. The UW cards should all come after the W cards but don't... – Chris Feb 08 '19 at 12:16
  • @Chris I asked " Are you trying to say "If a card has multiple colours, then a single one of those colours shall be selected (the colour that is selected is the one that appears first in a priority list of colours), and the card shall be sorted on the basis of that single highest-priority colour, without taking any of the other colours into account"?" and he said "Yes". He very explicitly said that if a card has multiple colours, only the highest-priority one should be taken into account. – canton7 Feb 08 '19 at 12:17
  • 1
    @canton7: The question said " with cards having more than one color I'd like them being sorted based on a custom priority list (like if it's W and U put it between W cards)" which I interpreted as a typo on "between W and U cards". I'll leave it to the OP though to comment on whether they care about this or not. – Chris Feb 08 '19 at 12:21
  • I had no idea what he meant by that sentence, which is why I asked for clarification with my very clear statement, and he responded that my statement was correct. – canton7 Feb 08 '19 at 12:24
  • @canton7: Sadly my question: "Or is it simply something like that you sort alphabetically with mono-colour first and then multi-colour cards at the end of those with them ordered by second (or third, etc.) colour if appropriate?" was answered with "Your second example ("Or is it simply something like that you sort alphabetically [...]") is exactly the behavior I'd like.". So sadly there is still ambiguity. :) – Chris Feb 08 '19 at 12:37
  • 1
    I think your updated answer is perfect now! It also takes into account possibility of no colours which is great! – Chris Feb 08 '19 at 12:39
  • @Chris true, but my life's easier if I stick to just one of the problem definitions he said was correct >< Thanks for the review! – canton7 Feb 08 '19 at 12:41
1

You can get a list of ordered cards by using the Aggregate function as follows:

var result = deck.Cards
    .OrderBy(x => x.Colours.Aggregate((total, part) => total + part.ToLower()))
    .ToList();

This assumes that the cards with multiple colours have those in an ordered list.

e.g.

    card1.Colours = new List<string>() { "W", "X" };
    card2.Colours = new List<string>() { "W" };
    card3.Colours = new List<string>() { "U" };
    card4.Colours = new List<string>() { "U", "W" };

Will return the cards in the order:

"U", "UW", "W", "WX"

melkisadek
  • 1,043
  • 1
  • 14
  • 33
  • (Note that this doesn't take his custom colour priority list into account) – canton7 Feb 08 '19 at 12:19
  • 1
    Also, using Aggregate to concatenate strings is notoriously expensive, due to all those temporary strings. `string.Concat(x.Colours).ToLowerInvariant()` is a lot cheaper, if you even need the `ToLower` call at all. – canton7 Feb 08 '19 at 12:45