1

I have a list of 100 elements, which are looking something like that:

  1. dog
  2. cat (2)
  3. bird (34)
  4. cat + dog (11)
  5. dog (5)

Additionally I have a specific required order, lets say:

string[] order = {"dog", "bird", "cat", "cat + dog"};

I need my method to sort by aforementioned order and then by numbers, to get as a result:

  1. dog
  2. dog (5)
  3. bird (34)
  4. cat (2)
  5. cat + dog (11)

Currently i have something like that:

bool equal = collection
  .OrderBy(i => Array.IndexOf(order, i.Split('(').First()))
  .ThenBy(i => i.Split('(').Last().Replace(")", " "))
  .SequenceEqual(collection2);

But it doesn't work. ThenBy overlaps first sorting. Also upon entering int.Parse to the ThenBy brackets i'm getting an exception.

Help me to achieve this.

Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
Czarek B
  • 21
  • 2
  • 1
    Are you planning to make a custom compare function? – PepitoSh Jun 14 '18 at 09:19
  • 4
    Possible duplicate of [Alphanumeric sorting using LINQ](https://stackoverflow.com/questions/5093842/alphanumeric-sorting-using-linq) – Lucifer Jun 14 '18 at 09:20
  • 2
    *"list of 100 elements"* is the element literally the string "cat (2)" or is that an element with properties `cat` and `2` – Jamiec Jun 14 '18 at 09:21
  • Does parsing your i.Split('(').Last().Replace(")", " ") into a number help? i.e. bool equal = collection .OrderBy(i => Array.IndexOf(order, i.Split('(').First())) .ThenBy(i => int.Parse(i.Split('(').Last().Replace(")", " "))) .SequenceEqual(collection2); – asidis Jun 14 '18 at 09:24
  • @Lucifer He has another step: ordering an array by another array – xanatos Jun 14 '18 at 09:25
  • @Jamiec yes, it's literally a string – Czarek B Jun 14 '18 at 09:29
  • @asidis I am getting an exception "Invalid input string format" and i have no idea why – Czarek B Jun 14 '18 at 09:31
  • https://stackoverflow.com/questions/50812105/c-sharp-linq-order-a-list-with-a-reference-list/50812288#50812288 ?? – Lucifer Jun 14 '18 at 09:31
  • @asidis Use this one: `new Regex(@"^(?.*?)( \((?[0-9]+)\))?$")` – xanatos Jun 14 '18 at 10:05

3 Answers3

1

I suggest splitting initial lines into anonymous class instances: having this done (and debugged) you can put just

.OrderBy(item => Array.IndexOf(order, item.name))
.ThenBy(item => item.count)

Implementation:

  List<string> collection = new List<string> {
    "dog",
    "cat (2)",
    "bird (34)",
    "cat + dog (11)",
    "dog (5)",
  };

  string[] order = { "dog", "bird", "cat", "cat + dog" };

  var result = collection
    .Select(item => item.Split('('))
    .Select(parts => parts.Length == 1 // do we have "(x)" part?
       ? new { name = parts[0].Trim(), count = 1 } // no
       : new { name = parts[0].Trim(), count = int.Parse(parts[1].Trim(')')) }) // yes
    .OrderBy(item => Array.IndexOf(order, item.name)) // now it's easy to work with data
    .ThenBy(item => item.count)
    .Select(item => item.count == 1 // back to the required format
       ? $"{item.name}"
       : $"{item.name} ({item.count})")
    .ToList();

 Console.WriteLine( string.Join(Environment.NewLine, result));

Outcome:

dog
dog (5)
bird (34)
cat (2)
cat + dog (11)

Edit: your code amended Trim() added within OrderBy; ThenBy redesigned

  var result = collection
    .OrderBy(i => Array.IndexOf(order, i.Split('(').First().Trim())) // Trim
    .ThenBy(i => i.Contains('(')                                     // two cases:
       ? int.Parse(i.Split('(').Last().Replace(")", ""))             // with "(x)" part
       : 1)                                                          // without
    .ToList();
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
1

Quite the same answer than @Dmitry Bychenko using regex:

var collection = new List<string> {
    "dog",
    "cat (2)",
    "bird (34)",
    "cat + dog (11)",
    "dog (5)",
};

string[] order = { "dog", "bird", "cat", "cat + dog" };

var regex = new Regex("^(?<name>.*?)\\s*(\\((?<number>[0-9]+)\\))?$");

var result = collection
  .Select(i =>
    {
      var match = regex.Match(i);
      return new {
          content = i,
          name = match.Groups["name"].Value,
          number = int.TryParse(match.Groups["number"].Value, out int number) 
            ? number 
            : 1 };
    })
  .OrderBy(item => Array.IndexOf(order, item.name))
  .ThenBy(item => item.number)
  .Select(i => i.content)
  .ToList();

Console.WriteLine(string.Join(Environment.NewLine, result));
Console.ReadLine();
asidis
  • 1,374
  • 14
  • 24
  • You can add `\s*` into pattern `var regex = new Regex(@"(?.*)\s*\\((?[0-9]+)\\)");` and get rid of `Trim()`: `.OrderBy(item => Array.IndexOf(order, item.name))` – Dmitry Bychenko Jun 14 '18 at 10:14
  • Since `default(int) == 0` I doubt that it should be used; probably it should be `1`: `... ? number : 1` - if quantity - `(number)` - is omitted then *one* animal mentioned: `"dog"` - one dog, `"dog (5)"` - five dogs. – Dmitry Bychenko Jun 14 '18 at 10:16
  • @DmitryBychenko you are right, it will make the code better ;-) I've also added a non greedy expression for the name or it will keep the spaces. – asidis Jun 14 '18 at 10:32
0

No checks done... The data is perfect or everything goes boom:

var res = (from x in collection
           let ix = x.LastIndexOf(" (")
           orderby Array.IndexOf(order, ix != -1 ? x.Remove(ix) : x),
               ix != -1 ? int.Parse(x.Substring(ix + 2, x.Length - 1 - (ix + 2))) : 0
           select x).ToArray();

Note the dual handling of ix != -1. In the second line of the orderby (that is the ThenBy() in functional LINQ), if ix == -1 then the value is : 0)

xanatos
  • 109,618
  • 12
  • 197
  • 280