4

I want to sort an list of string that sometimes contains numeric values

My list is like this:

  • Bedroom 1
  • Bedroom 2
  • Bedroom 10
  • Bathroom 1
  • Bathroom 2
  • Bathroom 10
  • 1 Light
  • 1 Paper

If I just use an orderby: roomList.OrderBy(x => x.Name) I get this list:

  • 1 Light
  • 1 Paper
  • Bathroom 1
  • Bathroom 10
  • Bathroom 2
  • Bedroom 1
  • Bedroom 10
  • Bedroom 2

Is it possible to get the list like this instead ?

  • 1 Light
  • 1 Paper
  • Bathroom 1
  • Bathroom 2
  • Bathroom 10
  • Bedroom 1
  • Bedroom 2
  • Bedroom 10

All list elements don't contain a number, and the list is about 1500 rows long

I have tryed to use this code, and it works fine on elements containing number, but not on just string elements:

public class SemiNumericComparer : IComparer<string>
    {
        public int Compare(string s1, string s2)
        {
            if (IsNumeric(s1) && IsNumeric(s2))
            {
                if (Convert.ToInt32(s1) > Convert.ToInt32(s2)) return 1;
                if (Convert.ToInt32(s1) < Convert.ToInt32(s2)) return -1;
                if (Convert.ToInt32(s1) == Convert.ToInt32(s2)) return 0;
            }

            if (IsNumeric(s1) && !IsNumeric(s2))
                return -1;

            if (!IsNumeric(s1) && IsNumeric(s2))
                return 1;

            return string.Compare(s1, s2, true);
        }

        public static bool IsNumeric(object value)
        {
            try
            {
                int i = Convert.ToInt32(value.ToString());
                return true;
            }
            catch (FormatException)
            {
                return false;
            }
        }
    }
Marcus Ohlsson
  • 267
  • 3
  • 13
  • 1
    what do you want to happen with "2 Light" and "10 Light"? What about "Bathroom Blue"? – Geoffrey Mar 29 '19 at 13:00
  • 4
    Related https://stackoverflow.com/questions/5093842/alphanumeric-sorting-using-linq – xdtTransform Mar 29 '19 at 13:04
  • 1
    Not sure if this is an option for you, but you could edit the strings to have leading zeros. `Bathroom 001`, `Bathroom 002`, etc. Alternatively, convert the numeric parts to that format at runtime, and then sort. – Ctrl S Mar 29 '19 at 13:04
  • 3
    https://stackoverflow.com/questions/248603/natural-sort-order-in-c-sharp/248613#248613 – xdtTransform Mar 29 '19 at 13:05
  • Both should work with some difference on "edge case" scenario. Try one on your datas if you have an issue be specific on the case. But I'm pretty sure that if it's not an completly arbritary rules, it's either a dupe or a buildin function exist. Human have been asking for ordered list of things since we start to have computer. I expect dev to a find an easy solution, the day after. – xdtTransform Mar 29 '19 at 13:18
  • @xdtTransform I thought so to, but after googleing around for two days, I still can't find any examples where the input can be both numbers and string or an combination – Marcus Ohlsson Apr 01 '19 at 08:02
  • Does the solution on this question or the duplicate failed to order your data? I know it's hard to read the ton of my previous comment but it's more a sarcasm than a build in solution already exist. Because the solution is either: use Natural Sort Order algorithm (not build in, long and can look complex). Or use a Dll that have diffenrent behavior on edge case(with weird ascii or old .net version / old Library version). And I wont even talk about Customer understanding of logic and ordering – xdtTransform Apr 01 '19 at 08:49

3 Answers3

3

Try following custom IComparable :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace ConsoleApplication107
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> input = new List<string>() { "1 Light", "1 Paper", "Bathroom 1", "Bathroom 2", "Bathroom 10", "Bedroom 1", "Bedroom 2", "Bedroom 10" };

            List<string> results = input.Select(x => new { s = x, order = new Order(x) }).OrderBy(x => x.order).Select(x => x.s).ToList();

        }
    }
    public class Order : IComparable<Order>
    {
        List<string> split { get; set; }

        public Order(string line)
        {
            split = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
        }

        public int CompareTo(Order other)
        {

            int min = Math.Min(split.Count, other.split.Count);
            int thisNumber = 0;
            int otherNumber = 0;
            for (int i = 0; i < min; i++)
            {
                if (split[i] != other.split[i])
                {
                    if ((int.TryParse(split[i], out thisNumber)))
                    {
                        if ((int.TryParse(other.split[i], out otherNumber)))
                        {
                            return thisNumber.CompareTo(otherNumber); //both numbers
                        }
                        else
                        {
                            return -1; // this is number other is string : this comes first
                        }
                    }
                    else
                    {
                        if ((int.TryParse(other.split[i], out otherNumber)))
                        {
                            return 1; //other is number this is string : other comes first
                        }
                        else
                        {
                            return split[i].CompareTo(other.split[i]);
                        }
                    }
                }
            }

            return split.Count.CompareTo(other.split.Count);

        }

    }
}
jdweng
  • 33,250
  • 2
  • 15
  • 20
2
        [SuppressUnmanagedCodeSecurity]
        internal static class SafeNativeMethods
        {
            [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
            public static extern int StrCmpLogicalW(string p1, string p2);
        }

        public sealed class StringComparer : IComparer<string>
        {
            public int Compare(string a, string b)
            {
                return SafeNativeMethods.StrCmpLogicalW(a, b);
            }
        }

and now you can use StringComparer on OrderBy :

            List<string> str = new List<string>
            {
                "Bedroom 1",
                "Bedroom 2",
                "Bedroom 10",
                "Bathroom 1",
                "Bathroom 2",
                "Bathroom 10",
                "1 Light",
                "1 Paper"
            };
            str = str.OrderBy(x => x, new StringComparer()).ToList();
godot
  • 3,422
  • 6
  • 25
  • 42
  • 3
    This will only work in a Windows environment. Also, I wouldn't recommend referencing kernel dll's if it can be (easily) avoided. – discy Mar 29 '19 at 13:19
  • 1
    Additionally to what @discy mentioned, it is noteworthy to mention that StrCmpLogicalW can behave differently on different Windows releases (which might or might not be desirable, depending on whether the sorting should match the sorting behavior of the Windows version on which the program is running or not). –  Mar 29 '19 at 13:21
1

You're looking for a "natural sorting" algorithm

For example: https://www.codeproject.com/Articles/22517/Natural-Sort-Comparer

discy
  • 410
  • 3
  • 13