Search Unity

Sorting Numerical List

Discussion in 'Scripting' started by wilsongferguson, Mar 29, 2020.

  1. wilsongferguson

    wilsongferguson

    Joined:
    Mar 29, 2020
    Posts:
    3
    I have 576 objects all named, "Person " plus a number from 1-576. Then I made a list of every object and tried to sort it with System.Linq. Instead of sorting it by "Person 1" "Person 2"... "Person 10" etc, it sorts it by "Person 1" "Person 10" "Person 100"... "Person 2" It's treating person 100 as person 1 instead of 100 and it's treating person 2 as coming after any person with 1 as the first digit.

    Code (CSharp):
    1. List<GameObject> temp = new List<GameObject>();
    2.         temp.AddRange(GameObject.FindGameObjectsWithTag("Person").OrderBy(go => go.name));
     
  2. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,187
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    there is no easy way to do this, as you have to write a custom sorting comparer which can be quite cumbersome.
    but there is a hack that you could apply.

    if your names always have a space between the word and the number, you can split the two and pad the number so that all numbers are of the same length, but have leading zeroes.
    Code (csharp):
    1. var gos = GameObject.FindGameObjectsWithTag("Person");
    2. const int DIGITS = 4; // how many total digits you'd like
    3. string[] paddedNames = new string[gos.Length];
    4.  
    5. for(int i = 0; i < gos.Length; i++) {
    6.   var name = gos[i].name;
    7.   name.SplitOnce(name.IndexOf(" "), out var word, out var num);
    8.   paddedNames[i] = $"{word} {num.PadLeft(DIGITS, '0')}";
    9. }
    10.  
    11. Array.Sort(paddedNames);
    12. // paddedNames are now properly sorted (but have zeroes)
    Code (csharp):
    1. // this is a general purpose extension (should go into a separate static class)
    2. static public void SplitOnce(this string s, int index, out string left, out string right) {
    3.   if(index < 0 || index >= s.Length - 1) { left = s; right = string.Empty; }
    4.     else { left = s.Substring(0, index); right = s.Substring(index + 1); }
    5. }
    you can also do something like this (probably even better, because no zero padding)
    Code (csharp):
    1. var gos = GameObject.FindGameObjectsWithTag("Person");
    2. var sortedList = new SortedList<int, int>();
    3.  
    4. for(int i = 0; i < gos.Length; i++) {
    5.   var name = gos[i].name;
    6.   name.SplitOnce(name.IndexOf(" "), out _, out var num);
    7.   sortedList.Add(int.Parse(num), i);
    8.   // here we use sorted list that is auto-sorted,
    9.   // but we store the original index as well
    10. }
    11.  
    12. var finalList = new List<GameObject>();
    13. foreach(var item in sortedList) {
    14.   // because this foreach is in sorted order
    15.   // we can use the stored index to fetch the original GO
    16.   finalList.Add(gos[item.Value]);
    17. }
    18.  
    19. // you can do finalList.ToArray() if you need an array
    (edit: this has to be modified to work with the words as well, it's just an idea;
    final solution is down, so keep reading)

    if this still isn't good enough for you try to look for alphanumeric (or natural) sorting comparer.
    it is easy to run, you just do
    Code (csharp):
    1. GameObject.FindGameObjectsWithTag("Person").OrderBy(go => go.name, new SomeComparer());
    or you can use Array.Sort on the result without having to use Linq
    Code (csharp):
    1. var result = GameObject.FindGameObjectsWithTag("Person");
    2. Array.Sort(result, new SomeComparer());
    but you have to code the entire class that does this
    you may want to check out this (the answer by J.D. with 28 likes)
    or this (the answer by VVS with 4 likes)

    finally, and personally I think this one is a winner
    you can make a very small comparison method, and use List.Sort instead
    Code (csharp):
    1. var gos = GameObject.FindGameObjectsWithTag("Person");
    2. var list = new List<GameObject>(gos);
    3. list.Sort((a, b) => NaturalComparer(a.name, b.name));
    4. // DONE
    5.  
    6. static int NaturalComparer(string a, string b) {
    7.   a.SplitOnce(a.IndexOf(" "), out _, out var num1);
    8.   b.SplitOnce(b.IndexOf(" "), out _, out var num2);
    9.   int n1 = int.Parse(num1), n2 = int.Parse(num2);
    10.   if(n1 < n2) return -1;
    11.   if(n1 > n2) return +1;
    12.   return 0;
    13. }
     
    Last edited: Mar 29, 2020
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    the last comparer only takes into account the numbers, which isn't sufficient for your case
    so let's make it smarter
    Code (csharp):
    1. static int NaturalComparer(string a, string b) {
    2.   // check if there are spaces (returns -1 if none were found)
    3.   int i1 = a.IndexOf(" "), i2 = b.IndexOf(" ");
    4.  
    5.   // split the strings (works with -1 just fine)
    6.   a.SplitOnce(i1, out var text1, out var num1);
    7.   b.SplitOnce(i2, out var text2, out var num2);
    8.  
    9.   // compare the textual parts
    10.   int c = text1.CompareTo(text2);
    11.  
    12.   // if there were spaces and textual parts were equal
    13.   // compare numbers only
    14.   if(i1 >= 0 && i2 >= 0 && c == 0) {
    15.     // if you had a space between two words, this will blow up
    16.     // so be careful (in that case, it has to be smarter than this)
    17.     int n1 = int.Parse(num1), n2 = int.Parse(num2);
    18.     if(n1 < n2) return -1; else if(n1 > n2) return +1;
    19.     return 0;
    20.   }
    21.  
    22.   // otherwise sort machine-like alphabetically
    23.   return c;
    24. }
    this is probably the nimblest natural sorter you'll ever come across on the internet.
    yes I'm the humblestest of persons, I know. (pshh it's quite limited, but I said it's hacky)
     
    Last edited: Mar 29, 2020
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    If you don't want to rely on spaces or any such delimiter, you can try using Regex

    i.e.
    Code (csharp):
    1. var num = Regex.Match(a, @"\d+$"); // will find digits in your string