Як сортувати рядки в алфавітному порядку, враховуючи значення, коли рядок є числовим?


100

Я намагаюся сортувати масив чисел, які є рядками, і я хотів би, щоб вони сортували числово.

Суть в тому, що я не можу перетворити числа в 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

EDIT: Вихід не може бути 090, 101, 102 ...

Оновлено зразок коду, щоб сказати "речі" замість "розмірів". Масив може бути приблизно таким:

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

Це означає, що його потрібно сортувати за алфавітом та за кількістю:

007, 90, bob, lauren, paul


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
Для даного вводу це дає такий же результат, як і відповідь рекурсивного, що передбачає PadLeft (). Я припускаю, що ваше вкладення насправді складніше, ніж показує цей приклад, і в цьому випадку користувацький порівняльник - це шлях.
Джефф Полсен

Ура. Це рішення працює і здається легким для читання та чистим способом реалізації. +1, щоб показати мені, ви можете використовувати IComparer на OrderBy :)
sf.

17
IsNumericМетод поганий, виняток приводу кодування завжди погано. Використовуйтеint.TryParse замість цього. Спробуйте свій код з великим списком, і це займе назавжди.
Nean Der Thal

Якщо це корисно, я додав розширення до цієї версії тут , який додає підтримку для сортування зі словами. Для моїх потреб розділення на пробіли було достатнім, і мені не потрібно було турбуватися про слова зі змішаним вживанням (наприклад, test12 vs test3),
matt.bungard

@NeanDerThal Я впевнений, що це лише повільна / погана обробка винятків у циклі, якщо ви налагоджуєтесь або отримуєте доступ до об’єкта Виняток.
Келлі Елтон

90

Просто колодка з нулями на однакову довжину:

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

+1 для простого рішення, нанизування буде (вже зроблено в редагуванні, приємно)
Marino Šimić

Хороша ідея, але наступна увага полягає в тому, що мені потрібно перетворити ці значення, тому "90" має бути "90", а не "090"
sf.

6
@sf: Спробуйте, результат може вам сподобатися. Пам'ятайте, що ключ замовлення - це не те, що замовляється. Якщо я сказав замовити список клієнтів за прізвищем, то я отримаю список клієнтів, а не список прізвищ. Якщо ви скажете замовити список рядків перетвореною рядком, то результат - упорядкований список оригінальних рядків, а не перетворених рядків.
Ерік Ліпперт

Мені довелося додати "size = size.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
Це подібно до варіанту накладки вище лише набагато кращого ІМО.
dudeNumber4

3
var size = size.OrderBy (x => x.Length) .ThenBy (x => x);
Філіп Девіс

1
Але це буде змішувати літерні рядки , як це: "b", "ab", "101", "103", "bob", "abcd".
Андрій

67

Значення - це рядок

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

Працює


2
Ця відповідь моя улюблена.
LacOniC

2
Дякую, я просто виявив, що виходить із методу "thenBy".
ganchito55

Це прекрасно працює для мого випадку використання, коли вхід знаходиться в форматі новогоstring[] { "Object 1", "Object 9", "Object 14" }
Тель

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 #
Kyle Delaney

Це було б ідеально, але це, на жаль, не справляється з негативними цифрами. -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 в 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, bob, lauren, paul


До речі, я використовував 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 чи не змінюється абсолютно нічого, ми могли б безпосередньо використати ціле значення знаку char, тому видаліть це -48, якщо це вас турбує ...
Marino Šimić

Значення 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-блок і може отримати користь TryParse.

А для тих, хто не впевнений: Цей Порівняльник сортуватиме значення так, що нечислові значення завжди додаються до кінця списку. Якщо хтось хоче їх на початку, другий та третій ifблоки повинні бути замінені.


Оскільки виклик методу TryParse, ймовірно, має певні накладні витрати, я б спочатку зберігав значення isNumeric для s1 та s2 у булеві значення, а замість цього робив порівняння. Таким чином вони не оцінюються багаторазово.
Оптавій

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);
}

Woha зовсім просте так? : 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.