Как отсортировать строки в алфавитном порядке с учетом значения, когда строка является числовой?


101

Я пытаюсь отсортировать массив чисел, которые являются строками, и я бы хотел, чтобы они сортировались численно.

Загвоздка в том, что я не могу преобразовать числа в int .

Вот код:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => x))
{
    Console.WriteLine(thing);
}

вывод: 101, 102, 103, 105, 90

Я бы хотел: 90, 101, 102, 103, 105

РЕДАКТИРОВАТЬ: вывод не может быть 090, 101, 102 ...

В примере кода обновлено слово «вещи» вместо «размеры». Массив может быть примерно таким:

string[] things= new string[] { "paul", "bob", "lauren", "007", "90" };

Это означает, что его нужно отсортировать по алфавиту и по номеру:

007, 90, боб, лорен, пол


8
Почему вы не можете преобразовать их в int?
Femaref

1
"размеры" могут быть чем-то другим, например "именем". Пример кода просто упрощен.
сф.

2
Будет ли какое-нибудь из чисел отрицательным? Все ли они будут целыми числами? Какой диапазон целых чисел?
Эрик Липперт

"вещи" могут быть строками любого типа. Я бы хотел, чтобы список был рассортирован логически для человека, не владеющего компьютером. Отрицательные числа должны быть перед положительными. Что касается длины строки, она не должна превышать 100 символов.
сф.

5
Как далеко ты хочешь зайти? Должен image10прийти после image2? Должен Januaryприйти раньше February?
svick

Ответы:


104

Передайте настраиваемый компаратор в OrderBy. Enumerable.OrderBy позволит вам указать любой компаратор, который вам нравится.

Это один из способов сделать это:

void Main()
{
    string[] things = new string[] { "paul", "bob", "lauren", "007", "90", "101"};

    foreach (var thing in things.OrderBy(x => x, new SemiNumericComparer()))
    {    
        Console.WriteLine(thing);
    }
}


public class SemiNumericComparer: IComparer<string>
{
    /// <summary>
    /// Method to determine if a string is a number
    /// </summary>
    /// <param name="value">String to test</param>
    /// <returns>True if numeric</returns>
    public static bool IsNumeric(string value)
    {
        return int.TryParse(value, out _);
    }

    /// <inheritdoc />
    public int Compare(string s1, string s2)
    {
        const int S1GreaterThanS2 = 1;
        const int S2GreaterThanS1 = -1;

        var IsNumeric1 = IsNumeric(s1);
        var IsNumeric2 = IsNumeric(s2);

        if (IsNumeric1 && IsNumeric2)
        {
            var i1 = Convert.ToInt32(s1);
            var i2 = Convert.ToInt32(s2);

            if (i1 > i2)
            {
                return S1GreaterThanS2;
            }

            if (i1 < i2)
            {
                return S2GreaterThanS1;
            }

            return 0;
        }

        if (IsNumeric1)
        {
            return S2GreaterThanS1;
        }

        if (IsNumeric2)
        {
            return S1GreaterThanS2;
        }

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

1
Для заданного ввода это дает тот же результат, что и ответ Recursive, который включает PadLeft (). Я предполагаю, что ваш ввод на самом деле более сложен, чем показано в этом примере, и в этом случае можно использовать настраиваемый компаратор.
Джефф Полсен

Ура. Это решение работает и кажется простым для чтения и понятным способом реализации. +1 за то, что показал мне, что вы можете использовать IComparer на OrderBy :)
sf.

17
IsNumericМетод плох, исключение привода кодирования всегда плохо. int.TryParseВместо этого используйте . Попробуйте свой код с большим списком, и это займет вечность.
Nean Der Thal

Если это полезно, я добавил сюда расширение к этой версии, которое добавляет поддержку сортировки по словам. Для моих нужд разделения на пробелы было достаточно, и мне не нужно было беспокоиться о словах смешанного использования (например, test12 vs test3),
matt.bungard

@NeanDerThal Я почти уверен, что это только медленная / плохая обработка большого количества исключений в цикле, если вы отлаживаете или обращаетесь к объекту Exception.
Келли Элтон

90

Просто добавьте нули к той же длине:

int maxlen = sizes.Max(x => x.Length);
var result = sizes.OrderBy(x => x.PadLeft(maxlen, '0'));

+1 за простое решение, придирки (уже сделано в редактировании, приятно)
Марино Шимич

Хорошая идея, но следующая загвоздка в том, что мне нужно отобразить эти значения так, чтобы «90» было «90», а не «090»
sf.

6
@sf: Попробуй, может тебе понравится результат. Помните, что заказывается не ключ заказа. Если я сказал упорядочить список клиентов по фамилии, то я получу список клиентов, а не список фамилий. Если вы говорите упорядочить список строк по преобразованной строке, то результатом будет упорядоченный список исходных строк, а не преобразованных строк.
Эрик Липперт

Мне пришлось добавить «sizes = sizes.OrderBy (...)», чтобы это работало. Это нормально или ответ надо отредактировать?
gorgabal

1
@gorgabal: В общем, переназначение sizesтоже не сработает, потому что результат другого типа. Ответ краткий, поскольку вторая строка показывает результат в виде выражения, но читатель должен что-то с ним сделать. Я добавил еще одно присвоение переменной, чтобы сделать это более понятным.
рекурсивный

74

А как насчет этого ...

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

var size = from x in sizes
           orderby x.Length, x
           select x;

foreach (var p in size)
{
    Console.WriteLine(p);
}

хе-хе, мне очень нравится этот - очень умный. Извините, если я не предоставил полный набор исходных данных
sf.

3
Это похоже на вариант с прокладкой выше, только намного лучше IMO.
dudeNumber4 01

3
var size = sizes.OrderBy (x => x.Length) .ThenBy (x => x);
Филлип Дэвис

1
Но это будет смешивать буквенные строки , как это: "b", "ab", "101", "103", "bob", "abcd".
Эндрю

68

Значение - это строка

List = List.OrderBy(c => c.Value.Length).ThenBy(c => c.Value).ToList();

Работает


3
Это мой любимый ответ.
LacOniC

2
Спасибо, я только что обнаружил, что выходит из метода ThenBy.
ganchito55

Это отлично подходит для моего случая использования, когда ввод находится в новом форматеstring[] { "Object 1", "Object 9", "Object 14" }
thelem

2
Это лучший ответ. Это работает, и это полезно для обучения. Спасибо !!
июль

1
Но это будет смешивать буквенные строки , как это: "b", "ab", "101", "103", "bob", "abcd".
Эндрю

13

В окнах есть встроенная функция StrCmpLogicalW, которая сравнивает в строках числа как числа вместо букв. Легко создать средство сравнения, которое обращается к этой функции и использует ее для сравнений.

public class StrCmpLogicalComparer : Comparer<string>
{
    [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string x, string y);

    public override int Compare(string x, string y)
    {
        return StrCmpLogicalW(x, y);
    }
}

Он работает даже со строками, которые содержат как текст, так и числа. Вот пример программы, которая покажет разницу между сортировкой по умолчанию и StrCmpLogicalWсортировкой

class Program
{
    static void Main()
    {
        List<string> items = new List<string>()
        {
            "Example1.txt", "Example2.txt", "Example3.txt", "Example4.txt", "Example5.txt", "Example6.txt", "Example7.txt", "Example8.txt", "Example9.txt", "Example10.txt",
            "Example11.txt", "Example12.txt", "Example13.txt", "Example14.txt", "Example15.txt", "Example16.txt", "Example17.txt", "Example18.txt", "Example19.txt", "Example20.txt"
        };

        items.Sort();

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine();

        items.Sort(new StrCmpLogicalComparer());

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
        Console.ReadLine();
    }
}

который выводит

Example1.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example2.txt
Example20.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt

Example1.txt
Example2.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example20.txt

Я бы хотел, чтобы было проще использовать системные библиотеки на C #
Кайл Делани

Это было бы идеально, но, к сожалению, он не обрабатывает отрицательные числа. -1 0 10 2отсортировано как0 -1 2 10
nphx

5

попробуй это

sizes.OrderBy(x => Convert.ToInt32(x)).ToList<string>();

Примечание: это будет полезно, когда все строки можно преобразовать в int .....


1
это своего рода преобразование строки в int.
Femaref

1
"размеры" также могут быть нечисловыми
sf.

Для «LINQ to SQL» не забывайте ToList()перед =>sizes.ToList().OrderBy(x => Convert.ToInt32(x))
А. Морел

5

Я думаю, это будет намного лучше, если в строке будет какое-то число. Надеюсь, это поможет.

PS: Я не уверен в производительности или сложных строковых значениях, но он работал хорошо примерно так:

lorem ipsum
lorem ipsum 1
lorem ipsum 2
lorem ipsum 3
...
lorem ipsum 20
lorem ipsum 21

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        int s1r, s2r;
        var s1n = IsNumeric(s1, out s1r);
        var s2n = IsNumeric(s2, out s2r);

        if (s1n && s2n) return s1r - s2r;
        else if (s1n) return -1;
        else if (s2n) return 1;

        var num1 = Regex.Match(s1, @"\d+$");
        var num2 = Regex.Match(s2, @"\d+$");

        var onlyString1 = s1.Remove(num1.Index, num1.Length);
        var onlyString2 = s2.Remove(num2.Index, num2.Length);

        if (onlyString1 == onlyString2)
        {
            if (num1.Success && num2.Success) return Convert.ToInt32(num1.Value) - Convert.ToInt32(num2.Value);
            else if (num1.Success) return 1;
            else if (num2.Success) return -1;
        }

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

    public bool IsNumeric(string value, out int result)
    {
        return int.TryParse(value, out result);
    }
}

Именно то, что я искал. Спасибо!
klugerama

4

Вы говорите, что не можете преобразовать числа в int, потому что массив может содержать элементы, которые нельзя преобразовать в int, но попытка не повредит:

string[] things = new string[] { "105", "101", "102", "103", "90", "paul", "bob", "lauren", "007", "90" };
Array.Sort(things, CompareThings);

foreach (var thing in things)
    Debug.WriteLine(thing);

Тогда сравните вот так:

private static int CompareThings(string x, string y)
{
    int intX, intY;
    if (int.TryParse(x, out intX) && int.TryParse(y, out intY))
        return intX.CompareTo(intY);

    return x.CompareTo(y);
}

Выход: 007, 90, 90, 101, 102, 103, 105, Боб, Лорен, Пол.


Кстати, я использовал Array.Sort для простоты, но вы можете использовать ту же логику в IComparer и OrderBy.
Ульф Кристиансен

Это решение кажется более быстрым, чем использование IComparer (мое мнение). Результат 15000, и я чувствую, что это дает разницу примерно в секунду.
Джейсон Фолья

3

Это кажется странным запросом и заслуживает странного решения:

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

foreach (var size in sizes.OrderBy(x => {
    double sum = 0;
    int position = 0;
    foreach (char c in x.ToCharArray().Reverse()) {
        sum += (c - 48) * (int)(Math.Pow(10,position));
        position++;
    }
    return sum;
}))

{
    Console.WriteLine(size);
}

Я имел ввиду, конечно, 0x30. Кроме того, массив все еще может содержать нечисловую строку, для которой решение даст интересные результаты.
Femaref

И обратите внимание, что -48 или нет абсолютно ничего не меняет, мы могли бы напрямую использовать целочисленное значение символа, поэтому удалите это -48, если оно вас беспокоит ...
Марино Шимич

Значение char - 0x30, если преобразовать его в int, оно все равно будет 0x30, что не является числом 0.
Femaref

Единственное, что преобразуется в целое число, - это удвоение, которое возвращается из Math.Pow
Марино Шимич

femaref не имеет значения, равен ли он нулю или нет, декадическая система позаботится об этом, это может быть, если вы хотите, единственное, что имеет значение, что числа находятся в порядке возрастания в наборе символов, и это меньше чем 10
Марино Шимич

3

На этом сайте обсуждается буквенно-цифровая сортировка и выполняется сортировка чисел в логическом смысле, а не в смысле ASCII. Он также принимает во внимание альфы вокруг него:

http://www.dotnetperls.com/alphanumeric-sorting

ПРИМЕР:

  • C: /TestB/333.jpg
  • 11
  • C: /TestB/33.jpg
  • 1
  • C: /TestA/111.jpg
  • 111F
  • C: /TestA/11.jpg
  • 2
  • C: /TestA/1.jpg
  • 111D
  • 22
  • 111Z
  • C: /TestB/03.jpg

  • 1
  • 2
  • 11
  • 22
  • 111D
  • 111F
  • 111Z
  • C: /TestA/1.jpg
  • C: /TestA/11.jpg
  • C: /TestA/111.jpg
  • C: /TestB/03.jpg
  • C: /TestB/33.jpg
  • C: /TestB/333.jpg

Код выглядит следующим образом:

class Program
{
    static void Main(string[] args)
    {
        var arr = new string[]
        {
           "C:/TestB/333.jpg",
           "11",
           "C:/TestB/33.jpg",
           "1",
           "C:/TestA/111.jpg",
           "111F",
           "C:/TestA/11.jpg",
           "2",
           "C:/TestA/1.jpg",
           "111D",
           "22",
           "111Z",
           "C:/TestB/03.jpg"
        };
        Array.Sort(arr, new AlphaNumericComparer());
        foreach(var e in arr) {
            Console.WriteLine(e);
        }
    }
}

public class AlphaNumericComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string s1 = x as string;
        if (s1 == null)
        {
            return 0;
        }
        string s2 = y as string;
        if (s2 == null)
        {
            return 0;
        }

        int len1 = s1.Length;
        int len2 = s2.Length;
        int marker1 = 0;
        int marker2 = 0;

        // Walk through two the strings with two markers.
        while (marker1 < len1 && marker2 < len2)
        {
            char ch1 = s1[marker1];
            char ch2 = s2[marker2];

            // Some buffers we can build up characters in for each chunk.
            char[] space1 = new char[len1];
            int loc1 = 0;
            char[] space2 = new char[len2];
            int loc2 = 0;

            // Walk through all following characters that are digits or
            // characters in BOTH strings starting at the appropriate marker.
            // Collect char arrays.
            do
            {
                space1[loc1++] = ch1;
                marker1++;

                if (marker1 < len1)
                {
                    ch1 = s1[marker1];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch1) == char.IsDigit(space1[0]));

            do
            {
                space2[loc2++] = ch2;
                marker2++;

                if (marker2 < len2)
                {
                    ch2 = s2[marker2];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch2) == char.IsDigit(space2[0]));

            // If we have collected numbers, compare them numerically.
            // Otherwise, if we have strings, compare them alphabetically.
            string str1 = new string(space1);
            string str2 = new string(space2);

            int result;

            if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]))
            {
                int thisNumericChunk = int.Parse(str1);
                int thatNumericChunk = int.Parse(str2);
                result = thisNumericChunk.CompareTo(thatNumericChunk);
            }
            else
            {
                result = str1.CompareTo(str2);
            }

            if (result != 0)
            {
                return result;
            }
        }
        return len1 - len2;
    }
}

2

Ответ Джеффа Полсена правильный, но его Comprarerможно значительно упростить до следующего:

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

        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)
    {
        int result;
        return Int32.TryParse(value, out result);
    }
}

Это работает, потому что единственное, что проверяется на результат, Comparer- это больше, меньше или равно нулю результат. Можно просто вычесть значения из другого, и не нужно обрабатывать возвращаемые значения.

Также IsNumericметод не должен использовать try-block и может извлечь выгоду из TryParse.

И для тех, кто не уверен: этот Comparer будет сортировать значения так, чтобы нечисловые значения всегда добавлялись в конец списка. Если кто-то хочет, чтобы они были вначале, ifнужно поменять местами второй и третий блоки.


Поскольку вызов метода TryParse, вероятно, имеет некоторые накладные расходы, я бы сначала сохранил значения isNumeric для s1 и s2 в логических значениях и вместо этого провел бы сравнение с ними. Таким образом, они не оцениваются несколько раз.
Optavius

1

Попробуй это :

string[] things= new string[] { "105", "101", "102", "103", "90" };

int tmpNumber;

foreach (var thing in (things.Where(xx => int.TryParse(xx, out tmpNumber)).OrderBy(xx =>     int.Parse(xx))).Concat(things.Where(xx => !int.TryParse(xx, out tmpNumber)).OrderBy(xx => xx)))
{
    Console.WriteLine(thing);
}

1
public class NaturalSort: IComparer<string>
{
          [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
          public static extern int StrCmpLogicalW(string x, string y);

          public int Compare(string x, string y)
          {
                 return StrCmpLogicalW(x, y);
          }
}

arr = arr.OrderBy (x => x, новый NaturalSort ()). ToArray ();

Причина, по которой мне это было нужно, заключалась в том, чтобы поместить его в каталог, имена файлов которого начинались с числа:

public static FileInfo[] GetFiles(string path)
{
  return new DirectoryInfo(path).GetFiles()
                                .OrderBy(x => x.Name, new NaturalSort())
                                .ToArray();
}

0
Try this out..  



  string[] things = new string[] { "paul", "bob", "lauren", "007", "90", "-10" };

        List<int> num = new List<int>();
        List<string> str = new List<string>();
        for (int i = 0; i < things.Count(); i++)
        {

            int result;
            if (int.TryParse(things[i], out result))
            {
                num.Add(result);
            }
            else
            {
                str.Add(things[i]);
            }


        }

Теперь отсортируйте списки и объедините их обратно ...

        var strsort = from s in str
                      orderby s.Length
                      select s;

        var numsort = from n in num
                     orderby n
                     select n;

        for (int i = 0; i < things.Count(); i++)
        {

         if(i < numsort.Count())
             things[i] = numsort.ElementAt(i).ToString();
             else
             things[i] = strsort.ElementAt(i - numsort.Count());               
               }

Я просто попытался внести свой вклад в этот интересный вопрос ...


0

Мое предпочтительное решение (если все строки только числовые):

// Order by numerical order: (Assertion: all things are numeric strings only) 
foreach (var thing in things.OrderBy(int.Parse))
{
    Console.Writeline(thing);
}

0
public class Test
{
    public void TestMethod()
    {
        List<string> buyersList = new List<string>() { "5", "10", "1", "str", "3", "string" };
        List<string> soretedBuyersList = null;

        soretedBuyersList = new List<string>(SortedList(buyersList));
    }

    public List<string> SortedList(List<string> unsoredList)
    {
        return unsoredList.OrderBy(o => o, new SortNumericComparer()).ToList();
    }
}

   public class SortNumericComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        int xInt = 0;
        int yInt = 0;
        int result = -1;

        if (!int.TryParse(x, out xInt))
        {
            result = 1;
        }

        if(int.TryParse(y, out yInt))
        {
            if(result == -1)
            {
                result = xInt - yInt;
            }
        }
        else if(result == 1)
        {
             result = string.Compare(x, y, true);
        }

        return result;
    }
}

Вы можете объяснить свой код? Ответы только на код могут быть удалены.
Вай Ха Ли

Сообщение Джеффа Полсена помогло мне внедрить IComparer <string>, чтобы исправить мою проблему с раздражением. .
Кумар

0

Расширение ответа Джеффа Полсена. Я хотел убедиться, что не имеет значения, сколько групп чисел или символов было в строках:

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        if (int.TryParse(s1, out var i1) && int.TryParse(s2, out var i2))
        {
            if (i1 > i2)
            {
                return 1;
            }

            if (i1 < i2)
            {
                return -1;
            }

            if (i1 == i2)
            {
                return 0;
            }
        }

        var text1 = SplitCharsAndNums(s1);
        var text2 = SplitCharsAndNums(s2);

        if (text1.Length > 1 && text2.Length > 1)
        {

            for (var i = 0; i < Math.Max(text1.Length, text2.Length); i++)
            {

                if (text1[i] != null && text2[i] != null)
                {
                    var pos = Compare(text1[i], text2[i]);
                    if (pos != 0)
                    {
                        return pos;
                    }
                }
                else
                {
                    //text1[i] is null there for the string is shorter and comes before a longer string.
                    if (text1[i] == null)
                    {
                        return -1;
                    }
                    if (text2[i] == null)
                    {
                        return 1;
                    }
                }
            }
        }

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

    private string[] SplitCharsAndNums(string text)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < text.Length - 1; i++)
        {
            if ((!char.IsDigit(text[i]) && char.IsDigit(text[i + 1])) ||
                (char.IsDigit(text[i]) && !char.IsDigit(text[i + 1])))
            {
                sb.Append(text[i]);
                sb.Append(" ");
            }
            else
            {
                sb.Append(text[i]);
            }
        }

        sb.Append(text[text.Length - 1]);

        return sb.ToString().Split(' ');
    }
}

Я также взял SplitCharsAndNums с SO-страницы после внесения в нее поправок для работы с именами файлов.


-1

Хотя это старый вопрос, я хотел бы дать решение:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => Int32.Parse(x) )
{
    Console.WriteLine(thing);
}

Вау, довольно просто, правда? : D


-1
namespace X
{
    public class Utils
    {
        public class StrCmpLogicalComparer : IComparer<Projects.Sample>
        {
            [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
            private static extern int StrCmpLogicalW(string x, string y);


            public int Compare(Projects.Sample x, Projects.Sample y)
            {
                string[] ls1 = x.sample_name.Split("_");
                string[] ls2 = y.sample_name.Split("_");
                string s1 = ls1[0];
                string s2 = ls2[0];
                return StrCmpLogicalW(s1, s2);
            }
        }

    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.