relliv
relliv

Reputation: 863

C# List Numeric Sorting

I've tried a lot of methods, but I've always had problems with string expressions that are space characters. Then I came across this blog post and thought it might be useful but I do not know how to use it unfortunately. I have a list,

Edit: Game instead of Crysis, Star Citizen, 34 Games, Call of Duty. I use the game name to give an example

Game 3.1 1

Game 3.2 10

Game 3.3 11

Game 3.2 9

Game 3.18 7

Game 3.27 12

Game 3.11.2 13

Game 3.2 2

Game 3.8 5

Game 3.10 7

For Example;

        List<GameVersion> GameVersionList = new List<GameVersion>();
        GameVersionList.Add(new GameVersion() { Name = "Game 3.1", Code = "1" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "10" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.3", Code = "11" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "9" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.18", Code = "7" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.27", Code = "12" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.11.2", Code = "13" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "2" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.8", Code = "5" });
        GameVersionList.Add(new GameVersion() { Name = "Game 3.10", Code = "7" });

        public class GameVersion
        {
            public string Name { get; set; }
            public string Code { get; set; }
        }

It should be like that;

Game 3.1 1

Game 3.2 10

Game 3.2 9

Game 3.2 2

Game 3.3 11

Game 3.8 5

Game 3.10 7

Game 3.11.2 13

Game 3.18 7

Game 3.27 12

I want to be sorted by name. So, how to use this CompareNumeric?

        public static int CompareNumeric(this string s, string other)
    {
        if (s != null && other != null &&
            (s = s.Replace(" ", string.Empty)).Length > 0 &&
            (other = other.Replace(" ", string.Empty)).Length > 0)
        {
            int sIndex = 0, otherIndex = 0;

            while (sIndex < s.Length)
            {
                if (otherIndex >= other.Length)
                    return 1;

                if (char.IsDigit(s[sIndex]))
                {
                    if (!char.IsDigit(other[otherIndex]))
                        return -1;

                    // Compare the numbers
                    StringBuilder sBuilder = new StringBuilder(), otherBuilder = new StringBuilder();

                    while (sIndex < s.Length && char.IsDigit(s[sIndex]))
                    {
                        sBuilder.Append(s[sIndex++]);
                    }

                    while (otherIndex < other.Length && char.IsDigit(other[otherIndex]))
                    {
                        otherBuilder.Append(other[otherIndex++]);
                    }

                    long sValue = 0L, otherValue = 0L;

                    try
                    {
                        sValue = Convert.ToInt64(sBuilder.ToString());
                    }
                    catch (OverflowException) { sValue = Int64.MaxValue; }

                    try
                    {
                        otherValue = Convert.ToInt64(otherBuilder.ToString());
                    }
                    catch (OverflowException) { otherValue = Int64.MaxValue; }

                    if (sValue < otherValue)
                        return -1;
                    else if (sValue > otherValue)
                        return 1;
                }
                else if (char.IsDigit(other[otherIndex]))
                    return 1;
                else
                {
                    int difference = string.Compare(s[sIndex].ToString(), other[otherIndex].ToString(), StringComparison.InvariantCultureIgnoreCase);

                    if (difference > 0)
                        return 1;
                    else if (difference < 0)
                        return -1;

                    sIndex++;
                    otherIndex++;
                }
            }

            if (otherIndex < other.Length)
                return -1;
        }

        return 0;
    }

Upvotes: 3

Views: 1639

Answers (5)

Serge
Serge

Reputation: 4036

If you have game names that can consist of more than one word and possibly no version number at all, then you need to implement something a bit more complex.

We will use a class that implements IComparer<string> interface for comparing game names.

The public int Compare(string x, string y) method will:

  • use Regex to split up game name into Name and Version parts
  • compare names - if names are not equal, sort by name (eg Alpha vs Bravo)
  • split up versions into an array using . as delimeter (eg 1.33.2 => {1, 33, 2})
  • compare each (sub-)version - if a pair is not equal, sort here (eg 1.33 vs 1.32)
  • if one version has a deeper sub-version level than the second, then sort using this (eg 3.2 vs 3.2.1)
class GameNameComparer : IComparer<string>
{
    static readonly Regex regx = new Regex(@"^(?<Name>.*) (?<Version>[\d\.]+)$", RegexOptions.ExplicitCapture);

    private static GameNameComparer instance;
    public static GameNameComparer Comparer
    {
        get
        {
            if (instance == null)
                instance = new GameNameComparer();
            return instance;
        }
    }

    private GameNameComparer() { }

    public int Compare(string x, string y)
    {
        var m1 = regx.Match(x);
        var m2 = regx.Match(y);

        if (m1.Success && m2.Success)
        {
            var name1 = m1.Groups["Name"].Value;
            var ver1 = m1.Groups["Version"].Value;
            var name2 = m2.Groups["Name"].Value;
            var ver2 = m2.Groups["Version"].Value;

            if (String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase))
            {
                var ver1Levels = ver1.Split(new char[] { '.' });
                var ver2Levels = ver2.Split(new char[] { '.' });

                for (int i = 0; i < Math.Min(ver1Levels.Length, ver2Levels.Length); i++)
                {
                    int ver1LevelNo = 0;
                    int ver2LevelNo = 0;
                    int compare = 0;

                    if (Int32.TryParse(ver1Levels[i], out ver1LevelNo) && Int32.TryParse(ver2Levels[i], out ver2LevelNo))
                    {
                        compare = ver1LevelNo.CompareTo(ver2LevelNo);
                        if (compare != 0)
                            return compare;
                    }

                    compare = ver1Levels[i].CompareTo(ver2Levels[i]);
                    if (compare != 0)
                        return compare;
                }

                return ver1Levels.Length.CompareTo(ver2Levels.Length);
            }

            return String.Compare(name1, name2, StringComparison.OrdinalIgnoreCase);
        }

        return String.Compare(x, y, StringComparison.OrdinalIgnoreCase);
    }
}

We can now use this class to sort the game list correctly:

GameVersionList = GameVersionList.OrderBy(g=>g.Name, GameNameComparer.Comparer).ToList();

Upvotes: 0

dotNET
dotNET

Reputation: 35400

There's no need to add so much complexity. Using LINQ, split the Name field on Space and take last element (number). You can then use Version class to perform sorting:

var Sorted = GameVersionList.OrderBy(g => new Version(g.Name.Split(' ').Last());

If you want to sort on the name part first and then by version, change the above slightly:

var Sorted = GameVersionList.OrderBy(g => g.Name).ThenBy(g => new Version(g.Name.Split(' ').Last());

Notice how the primary sort will not (effectively) be affected by the version number

Upvotes: 3

GBursali
GBursali

Reputation: 363

If every name is not the same as you commented, you can just take the substring after that space, convert it to double and sort it like

GameVersionList.OrderBy(x => double.Parse(
x.Name.Substring(
x.Name.IndexOf(' '))))

Upvotes: 1

Matthew Watson
Matthew Watson

Reputation: 109597

You can do this using the Windows API StrCmpLogicalW() to compare strings using a "natural" sort order.

You can encapsulate this in a List<T> extension:

using System.Runtime.InteropServices;

public static class ListExt
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    static extern int StrCmpLogicalW(string lhs, string rhs);

    public static void SortNatural<T>(this List<T> self, Func<T, string> stringSelector)
    {
        self.Sort((lhs, rhs) => StrCmpLogicalW(stringSelector(lhs), stringSelector(rhs)));
    }

    public static void SortNatural(this List<string> self)
    {
        self.Sort(StrCmpLogicalW);
    }
}

Then you can use it for your example like this:

static void Main()
{
    List<GameVersion> GameVersionList = new List<GameVersion>();
    GameVersionList.Add(new GameVersion() { Name = "Game 3.1", Code = "1" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "10" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.3", Code = "11" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "9" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.18", Code = "7" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.27", Code = "12" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.11.2", Code = "13" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.2", Code = "2" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.8", Code = "5" });
    GameVersionList.Add(new GameVersion() { Name = "Game 3.10", Code = "7" });

    GameVersionList.SortNatural(item => item.Name);

    foreach (var item in GameVersionList)
    {
        Console.WriteLine(item.Name + ": " + item.Code);
    }
}

The output is:

Game 3.1: 1
Game 3.2: 10
Game 3.2: 9
Game 3.2: 2
Game 3.3: 11
Game 3.8: 5
Game 3.10: 7
Game 3.11.2: 13
Game 3.18: 7
Game 3.27: 12

which matches your requirement.

Upvotes: 2

Sokui
Sokui

Reputation: 53

You can try OrderBy Name

var sortedList = GameVersionList.OrderBy(x=>x.Name).ToList();

Upvotes: 0

Related Questions