Порядок натурального сортування в C #


129

Хтось має хороший ресурс або надає зразок натурального порядку сортування в C # для FileInfoмасиву? Я реалізую IComparerінтерфейс у своїх видах.

Відповіді:


148

Найпростіше зробити це лише P / Invoke вбудованої функції в Windows, і використовувати її як функцію порівняння у вашому IComparer:

[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern int StrCmpLogicalW(string psz1, string psz2);

Майкл Каплан має кілька прикладів того, як ця функція працює тут , і про зміни, внесені для Vista, щоб зробити її більш інтуїтивно зрозумілою. Плюсом цієї функції є те, що вона матиме таку ж поведінку, як і версія Windows, на якій вона працює, однак це означає, що вона відрізняється між версіями Windows, тому вам потрібно врахувати, чи це проблема для вас.

Тож повна реалізація буде чимось на зразок:

[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    public static extern int StrCmpLogicalW(string psz1, string psz2);
}

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

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

8
Чудова відповідь. Caveat: Це не буде працювати з Win2000, для тих декількох людей, які все ще працюють з операційною системою. З іншого боку, між блогом Kaplan та документацією MSDN є достатньо підказок, щоб створити подібну функцію.
Кріс Чарабарук

9
Це не портативно, працює лише в Win32, але не працює в Linux / MacOS / Silverlight / Windows Phone / Metro
лінки

20
@linquize - Він сказав .NET не Mono, тому Linux / OSX насправді не викликає занепокоєння. Windows Phone / Metro не існувало в 2008 році, коли ця відповідь була розміщена. І як часто ви робите файлові операції в Silverlight? Тож для ОП, і, мабуть, більшості інших людей, це була відповідна відповідь. У будь-якому випадку ви можете надати кращу відповідь; ось так працює цей сайт.
Грег Бук

6
Це не означає, що початкова відповідь була неправильною. Я просто додаю додаткову інформацію з актуальною інформацією
linquize

2
FYI, якщо ви успадковуєте, Comparer<T>а не реалізуєте IComparer<T>, ви отримуєте вбудовану реалізацію IComparer(не загальний) інтерфейс, який викликає ваш загальний метод, для використання в API, які замість цього використовують. В принципі, це теж безкоштовно: просто видаліть "Я" та змініть public int Compare(...)на public override int Compare(...). Те саме для IEqualityComparer<T>і EqualityComparer<T>.
Джо Амента

75

Я просто подумав, що я додам до цього (з найкоротшим рішенням, яке я міг знайти):

public static IOrderedEnumerable<T> OrderByAlphaNumeric<T>(this IEnumerable<T> source, Func<T, string> selector)
{
    int max = source
        .SelectMany(i => Regex.Matches(selector(i), @"\d+").Cast<Match>().Select(m => (int?)m.Value.Length))
        .Max() ?? 0;

    return source.OrderBy(i => Regex.Replace(selector(i), @"\d+", m => m.Value.PadLeft(max, '0')));
}

Вищенаведені розміщення будь-яких чисел у рядку до максимальної довжини всіх чисел у всіх рядках і використовує отриманий рядок для сортування.

Передача до ( int?) - це можливість колекціонування рядків без будь-яких цифр ( .Max()на порожніх перелічених кидках InvalidOperationException).


1
+1 Мало того, що це найкоротше, це найшвидше, що я бачив. за винятком прийнятої відповіді, але я не можу використовувати її через машинні залежності. Він сортував понад 4 мільйони значень приблизно за 35 секунд.
Gene S

4
Це і красиво, і неможливо прочитати. Я припускаю, що переваги Linq означатимуть (принаймні) найкращі середні показники та найкращі показники, тому я думаю, що я збираюся піти з цим. Незважаючи на відсутність чіткості. Велике спасибі @Matthew Horsley
Ian Grainger

1
Це дуже добре, але є помилка для певних десяткових чисел, моїм прикладом було сортування k8.11 проти k8.2. Щоб виправити це, я реалізував наступний регулярний вираз: \ d + ([\.,] \ D)?
devzero

2
Також потрібно враховувати довжину другої групи (десяткова крапка + десяткових знаків), коли ви додаєте цей код m.Value.PadLeft (max, '0')
devzero

3
Я думаю, ви можете використовувати .DefaultIfEmpty().Max()замість кастингу int?. Також варто зробити це, source.ToList()щоб уникнути перерахування перелічених.
Teejay

30

Жодна з існуючих реалізацій не виглядала чудово, тому я написав власну. Результати майже ідентичні сортуванню, використовуваному сучасними версіями Windows Explorer (Windows 7/8). Єдині відмінності, які я бачив, - це 1), хоча Windows раніше (наприклад, XP) обробляв номери будь-якої довжини, тепер вона обмежена 19 цифрами - моя необмежена, 2) Windows дає непослідовні результати з певними наборами цифр Unicode - моя робота штрафом (хоча він не чисельно порівнює цифри сурогатних пар; а також Windows) та 3) міна не може розрізнити різні типи ваг не первинного сортування, якщо вони зустрічаються в різних розділах (наприклад, "e-1é" vs " é1e- "- розділи до і після числа мають діакритичну та пунктуаційну вагові різниці).

public static int CompareNatural(string strA, string strB) {
    return CompareNatural(strA, strB, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase);
}

public static int CompareNatural(string strA, string strB, CultureInfo culture, CompareOptions options) {
    CompareInfo cmp = culture.CompareInfo;
    int iA = 0;
    int iB = 0;
    int softResult = 0;
    int softResultWeight = 0;
    while (iA < strA.Length && iB < strB.Length) {
        bool isDigitA = Char.IsDigit(strA[iA]);
        bool isDigitB = Char.IsDigit(strB[iB]);
        if (isDigitA != isDigitB) {
            return cmp.Compare(strA, iA, strB, iB, options);
        }
        else if (!isDigitA && !isDigitB) {
            int jA = iA + 1;
            int jB = iB + 1;
            while (jA < strA.Length && !Char.IsDigit(strA[jA])) jA++;
            while (jB < strB.Length && !Char.IsDigit(strB[jB])) jB++;
            int cmpResult = cmp.Compare(strA, iA, jA - iA, strB, iB, jB - iB, options);
            if (cmpResult != 0) {
                // Certain strings may be considered different due to "soft" differences that are
                // ignored if more significant differences follow, e.g. a hyphen only affects the
                // comparison if no other differences follow
                string sectionA = strA.Substring(iA, jA - iA);
                string sectionB = strB.Substring(iB, jB - iB);
                if (cmp.Compare(sectionA + "1", sectionB + "2", options) ==
                    cmp.Compare(sectionA + "2", sectionB + "1", options))
                {
                    return cmp.Compare(strA, iA, strB, iB, options);
                }
                else if (softResultWeight < 1) {
                    softResult = cmpResult;
                    softResultWeight = 1;
                }
            }
            iA = jA;
            iB = jB;
        }
        else {
            char zeroA = (char)(strA[iA] - (int)Char.GetNumericValue(strA[iA]));
            char zeroB = (char)(strB[iB] - (int)Char.GetNumericValue(strB[iB]));
            int jA = iA;
            int jB = iB;
            while (jA < strA.Length && strA[jA] == zeroA) jA++;
            while (jB < strB.Length && strB[jB] == zeroB) jB++;
            int resultIfSameLength = 0;
            do {
                isDigitA = jA < strA.Length && Char.IsDigit(strA[jA]);
                isDigitB = jB < strB.Length && Char.IsDigit(strB[jB]);
                int numA = isDigitA ? (int)Char.GetNumericValue(strA[jA]) : 0;
                int numB = isDigitB ? (int)Char.GetNumericValue(strB[jB]) : 0;
                if (isDigitA && (char)(strA[jA] - numA) != zeroA) isDigitA = false;
                if (isDigitB && (char)(strB[jB] - numB) != zeroB) isDigitB = false;
                if (isDigitA && isDigitB) {
                    if (numA != numB && resultIfSameLength == 0) {
                        resultIfSameLength = numA < numB ? -1 : 1;
                    }
                    jA++;
                    jB++;
                }
            }
            while (isDigitA && isDigitB);
            if (isDigitA != isDigitB) {
                // One number has more digits than the other (ignoring leading zeros) - the longer
                // number must be larger
                return isDigitA ? 1 : -1;
            }
            else if (resultIfSameLength != 0) {
                // Both numbers are the same length (ignoring leading zeros) and at least one of
                // the digits differed - the first difference determines the result
                return resultIfSameLength;
            }
            int lA = jA - iA;
            int lB = jB - iB;
            if (lA != lB) {
                // Both numbers are equivalent but one has more leading zeros
                return lA > lB ? -1 : 1;
            }
            else if (zeroA != zeroB && softResultWeight < 2) {
                softResult = cmp.Compare(strA, iA, 1, strB, iB, 1, options);
                softResultWeight = 2;
            }
            iA = jA;
            iB = jB;
        }
    }
    if (iA < strA.Length || iB < strB.Length) {
        return iA < strA.Length ? 1 : -1;
    }
    else if (softResult != 0) {
        return softResult;
    }
    return 0;
}

Підпис відповідає Comparison<string>делегату:

string[] files = Directory.GetFiles(@"C:\");
Array.Sort(files, CompareNatural);

Ось клас обгортки для використання в якості IComparer<string>:

public class CustomComparer<T> : IComparer<T> {
    private Comparison<T> _comparison;

    public CustomComparer(Comparison<T> comparison) {
        _comparison = comparison;
    }

    public int Compare(T x, T y) {
        return _comparison(x, y);
    }
}

Приклад:

string[] files = Directory.EnumerateFiles(@"C:\")
    .OrderBy(f => f, new CustomComparer<string>(CompareNatural))
    .ToArray();

Ось хороший набір імен файлів, які я використовую для тестування:

Func<string, string> expand = (s) => { int o; while ((o = s.IndexOf('\\')) != -1) { int p = o + 1;
    int z = 1; while (s[p] == '0') { z++; p++; } int c = Int32.Parse(s.Substring(p, z));
    s = s.Substring(0, o) + new string(s[o - 1], c) + s.Substring(p + z); } return s; };
string encodedFileNames =
    "KDEqLW4xMiotbjEzKjAwMDFcMDY2KjAwMlwwMTcqMDA5XDAxNyowMlwwMTcqMDlcMDE3KjEhKjEtISox" +
    "LWEqMS4yNT8xLjI1KjEuNT8xLjUqMSoxXDAxNyoxXDAxOCoxXDAxOSoxXDA2NioxXDA2NyoxYSoyXDAx" +
    "NyoyXDAxOCo5XDAxNyo5XDAxOCo5XDA2Nio9MSphMDAxdGVzdDAxKmEwMDF0ZXN0aW5nYTBcMzEqYTAw" +
    "Mj9hMDAyIGE/YTAwMiBhKmEwMDIqYTAwMmE/YTAwMmEqYTAxdGVzdGluZ2EwMDEqYTAxdnNmcyphMSph" +
    "MWEqYTF6KmEyKmIwMDAzcTYqYjAwM3E0KmIwM3E1KmMtZSpjZCpjZipmIDEqZipnP2cgMT9oLW4qaG8t" +
    "bipJKmljZS1jcmVhbT9pY2VjcmVhbT9pY2VjcmVhbS0/ajBcNDE/ajAwMWE/ajAxP2shKmsnKmstKmsx" +
    "KmthKmxpc3QqbTAwMDNhMDA1YSptMDAzYTAwMDVhKm0wMDNhMDA1Km0wMDNhMDA1YSpuMTIqbjEzKm8t" +
    "bjAxMypvLW4xMipvLW40P28tbjQhP28tbjR6P28tbjlhLWI1Km8tbjlhYjUqb24wMTMqb24xMipvbjQ/" +
    "b240IT9vbjR6P29uOWEtYjUqb245YWI1Km/CrW4wMTMqb8KtbjEyKnAwMCpwMDEqcDAxwr0hKnAwMcK9" +
    "KnAwMcK9YSpwMDHCvcK+KnAwMipwMMK9KnEtbjAxMypxLW4xMipxbjAxMypxbjEyKnItMDAhKnItMDAh" +
    "NSpyLTAwIe+8lSpyLTAwYSpyLe+8kFwxIS01KnIt77yQXDEhLe+8lSpyLe+8kFwxISpyLe+8kFwxITUq" +
    "ci3vvJBcMSHvvJUqci3vvJBcMWEqci3vvJBcMyE1KnIwMCEqcjAwLTUqcjAwLjUqcjAwNSpyMDBhKnIw" +
    "NSpyMDYqcjQqcjUqctmg2aYqctmkKnLZpSpy27Dbtipy27Qqctu1KnLfgN+GKnLfhCpy34UqcuClpuCl" +
    "rCpy4KWqKnLgpasqcuCnpuCnrCpy4KeqKnLgp6sqcuCppuCprCpy4KmqKnLgqasqcuCrpuCrrCpy4Kuq" +
    "KnLgq6sqcuCtpuCtrCpy4K2qKnLgrasqcuCvpuCvrCpy4K+qKnLgr6sqcuCxpuCxrCpy4LGqKnLgsasq" +
    "cuCzpuCzrCpy4LOqKnLgs6sqcuC1puC1rCpy4LWqKnLgtasqcuC5kOC5lipy4LmUKnLguZUqcuC7kOC7" +
    "lipy4LuUKnLgu5UqcuC8oOC8pipy4LykKnLgvKUqcuGBgOGBhipy4YGEKnLhgYUqcuGCkOGClipy4YKU" +
    "KnLhgpUqcuGfoOGfpipy4Z+kKnLhn6UqcuGgkOGglipy4aCUKnLhoJUqcuGlhuGljCpy4aWKKnLhpYsq" +
    "cuGnkOGnlipy4aeUKnLhp5UqcuGtkOGtlipy4a2UKnLhrZUqcuGusOGutipy4a60KnLhrrUqcuGxgOGx" +
    "hipy4bGEKnLhsYUqcuGxkOGxlipy4bGUKnLhsZUqcuqYoFwx6pilKnLqmKDqmKUqcuqYoOqYpipy6pik" +
    "KnLqmKUqcuqjkOqjlipy6qOUKnLqo5UqcuqkgOqkhipy6qSEKnLqpIUqcuqpkOqplipy6qmUKnLqqZUq" +
    "cvCQkqAqcvCQkqUqcvCdn5gqcvCdn50qcu+8kFwxISpy77yQXDEt77yVKnLvvJBcMS7vvJUqcu+8kFwx" +
    "YSpy77yQXDHqmKUqcu+8kFwx77yO77yVKnLvvJBcMe+8lSpy77yQ77yVKnLvvJDvvJYqcu+8lCpy77yV" +
    "KnNpKnPEsSp0ZXN02aIqdGVzdNmi2aAqdGVzdNmjKnVBZS0qdWFlKnViZS0qdUJlKnVjZS0xw6kqdWNl" +
    "McOpLSp1Y2Uxw6kqdWPDqS0xZSp1Y8OpMWUtKnVjw6kxZSp3ZWlhMSp3ZWlhMip3ZWlzczEqd2Vpc3My" +
    "KndlaXoxKndlaXoyKndlacOfMSp3ZWnDnzIqeSBhMyp5IGE0KnknYTMqeSdhNCp5K2EzKnkrYTQqeS1h" +
    "Myp5LWE0KnlhMyp5YTQqej96IDA1MD96IDIxP3ohMjE/ejIwP3oyMj96YTIxP3rCqTIxP1sxKl8xKsKt" +
    "bjEyKsKtbjEzKsSwKg==";
string[] fileNames = Encoding.UTF8.GetString(Convert.FromBase64String(encodedFileNames))
    .Replace("*", ".txt?").Split(new[] { "?" }, StringSplitOptions.RemoveEmptyEntries)
    .Select(n => expand(n)).ToArray();

Розрядні розділи потрібно порівнювати по розділах, тобто "abc12b" має бути меншим, ніж "abc123".
ДНЯ

Ви можете спробувати такі дані: public string [] filenames = {"-abc12.txt", " abc12.txt", "1abc_2.txt", "a0000012.txt", "a0000012c.txt", "a000012.txt" , "a000012b.txt", "a012.txt", "a0000102.txt", "abc1_2.txt", "abc12 .txt", "abc12b.txt", "abc123.txt", "abccde.txt", " b0000.txt "," b00001.txt "," b0001.txt "," b001.txt "," c0000.txt "," c0000c.txt "," c00001.txt "," c000b.txt "," d0. 20.2b.txt "," d0.1000c.txt "," d0.2000y.txt "," d0.20000.2b.txt ","
ДУГО

@XichenLi Дякую за хороший тестовий випадок. Якщо ви дозволите Windows Explorer сортувати ці файли, ви отримаєте різні результати залежно від того, яку версію Windows ви використовуєте. Мій код сортує ці імена ідентично Server 2003 (і, мабуть, XP), але відрізняється від Windows 8. Якщо я отримаю можливість, я спробую розібратися, як це робить Windows 8, і оновити свій код.
JD

3
Має помилку. Індекс поза межами діапазону
обмежте

3
Чудове рішення! Коли я орієнтував його в звичайному сценарії з приблизно 10 000 файлами, він був швидшим, ніж приклад регексу Метью, і приблизно такий же, як StrCmpLogicalW (). У наведеному вище коді є незначна помилка: "while (strA [jA] == zeroA) jA ++;" і "поки (strB [jB] == zeroB) jB ++;" має бути "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" та "поки (jB <strB.Length && strB [jB] == zeroB) jB ++;". В іншому випадку рядки, що містять лише нулі, викинуть виняток.
kuroki

22

Чистий розчин C # для замовлення linq:

http://zootfroot.blogspot.com/2009/09/natural-sort-compare-with-linq-orderby.html

public class NaturalSortComparer<T> : IComparer<string>, IDisposable
{
    private bool isAscending;

    public NaturalSortComparer(bool inAscendingOrder = true)
    {
        this.isAscending = inAscendingOrder;
    }

    #region IComparer<string> Members

    public int Compare(string x, string y)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IComparer<string> Members

    int IComparer<string>.Compare(string x, string y)
    {
        if (x == y)
            return 0;

        string[] x1, y1;

        if (!table.TryGetValue(x, out x1))
        {
            x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)");
            table.Add(x, x1);
        }

        if (!table.TryGetValue(y, out y1))
        {
            y1 = Regex.Split(y.Replace(" ", ""), "([0-9]+)");
            table.Add(y, y1);
        }

        int returnVal;

        for (int i = 0; i < x1.Length && i < y1.Length; i++)
        {
            if (x1[i] != y1[i])
            {
                returnVal = PartCompare(x1[i], y1[i]);
                return isAscending ? returnVal : -returnVal;
            }
        }

        if (y1.Length > x1.Length)
        {
            returnVal = 1;
        }
        else if (x1.Length > y1.Length)
        { 
            returnVal = -1; 
        }
        else
        {
            returnVal = 0;
        }

        return isAscending ? returnVal : -returnVal;
    }

    private static int PartCompare(string left, string right)
    {
        int x, y;
        if (!int.TryParse(left, out x))
            return left.CompareTo(right);

        if (!int.TryParse(right, out y))
            return left.CompareTo(right);

        return x.CompareTo(y);
    }

    #endregion

    private Dictionary<string, string[]> table = new Dictionary<string, string[]>();

    public void Dispose()
    {
        table.Clear();
        table = null;
    }
}

2
Цей код, зрештою, з codeproject.com/KB/recipes/NaturalComparer.aspx (який не орієнтований на LINQ).
mhenry1384

2
Повідомлення в блозі кредитує Джастіна Джонса ( codeproject.com/KB/string/NaturalSortComparer.aspx ) для IComparer, а не Паскаля Ганея.
Джеймс Маккормак

1
Незначна примітка, це рішення ігнорує пробіли, які не є такими, як це робить Windows, і не так добре, як код Меттью Хорслі нижче. Таким чином, ви можете отримати, наприклад, 'string01' 'string 01' 'string 02' 'string02' (що виглядає некрасиво). Якщо ви видалите зачистку пробілів, він наказує рядки назад, тобто 'string01' надходить до 'рядка 01', що може бути, а може і не бути прийнятним.
Майкл Паркер

Це працювало для адрес, тобто "1 Smith Rd", "10 Smith Rd", "2 Smith Rd" тощо. Так! Хороший!
Пьотр Кула

До речі, я помітив (і коментарі на цій пов’язаній сторінці також, схоже, вказують), що аргумент типу <T> абсолютно не потрібний.
jv-dev

18

Відповідь Matthews Horsleys - це найшвидший метод, який не змінює поведінку залежно від того, на якій версії Windows працює ваша програма. Однак це може бути навіть швидше, створивши регулярний вираз один раз і використовуючи RegexOptions.Compiled. Я також додав можливість вставлення порівняння рядків, щоб ви могли проігнорувати регістр при необхідності та трохи покращити читабельність.

    public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
    {
        var regex = new Regex(@"\d+", RegexOptions.Compiled);

        int maxDigits = items
                      .SelectMany(i => regex.Matches(selector(i)).Cast<Match>().Select(digitChunk => (int?)digitChunk.Value.Length))
                      .Max() ?? 0;

        return items.OrderBy(i => regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
    }

Використовувати

var sortedEmployees = employees.OrderByNatural(emp => emp.Name);

Для сортування 100000 рядків у порівнянні з 300 мс для порівняння рядків .net за замовчуванням потрібно 450 мс - досить швидко!



16

Моє рішення:

void Main()
{
    new[] {"a4","a3","a2","a10","b5","b4","b400","1","C1d","c1d2"}.OrderBy(x => x, new NaturalStringComparer()).Dump();
}

public class NaturalStringComparer : IComparer<string>
{
    private static readonly Regex _re = new Regex(@"(?<=\D)(?=\d)|(?<=\d)(?=\D)", RegexOptions.Compiled);

    public int Compare(string x, string y)
    {
        x = x.ToLower();
        y = y.ToLower();
        if(string.Compare(x, 0, y, 0, Math.Min(x.Length, y.Length)) == 0)
        {
            if(x.Length == y.Length) return 0;
            return x.Length < y.Length ? -1 : 1;
        }
        var a = _re.Split(x);
        var b = _re.Split(y);
        int i = 0;
        while(true)
        {
            int r = PartCompare(a[i], b[i]);
            if(r != 0) return r;
            ++i;
        }
    }

    private static int PartCompare(string x, string y)
    {
        int a, b;
        if(int.TryParse(x, out a) && int.TryParse(y, out b))
            return a.CompareTo(b);
        return x.CompareTo(y);
    }
}

Результати:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

Мені це подобається. Це легко зрозуміти і не вимагає Linq.

11

Вам потрібно бути обережним - я туманно згадую, що читав, що StrCmpLogicalW, або щось подібне, не було строго транзитивним, і я помітив методи NET, які іноді застрягають у нескінченних петлях, якщо функція порівняння порушує це правило.

Перехідне порівняння завжди повідомляє, що a <c, якщо a <b і b <c. Існує функція, яка робить порівняння природного порядку сортування, яке не завжди відповідає цьому критерію, але я не можу згадати, чи це StrCmpLogicalW чи щось інше.


Чи є у вас якісь докази цього твердження? Після гуглень я не можу знайти жодних ознак того, що це правда.
mhenry1384

1
Я випробував ці нескінченні петлі з StrCmpLogicalW.
THD


Елемента 236900 зворотного зв’язку Visual Studio більше не існує, але ось сучасніший, який підтверджує проблему: connect.microsoft.com/VisualStudio/feedback/details/774540/… Він також дає змогу вирішити : CultureInfoмає властивість CompareInfo, і об'єкт, який він повертає, може постачати вас SortKeyоб'єктами. Вони, у свою чергу, можна порівняти та гарантувати транзитивність.
Джонатан Гілберт

9

Це мій код для сортування рядка, що містить як альфа, так і числові символи.

По-перше, цей метод розширення:

public static IEnumerable<string> AlphanumericSort(this IEnumerable<string> me)
{
    return me.OrderBy(x => Regex.Replace(x, @"\d+", m => m.Value.PadLeft(50, '0')));
}

Потім просто використовуйте його в будь-якому місці коду, як це:

List<string> test = new List<string>() { "The 1st", "The 12th", "The 2nd" };
test = test.AlphanumericSort();

Як це працює? Замінивши нулі:

  Original  | Regex Replace |      The      |   Returned
    List    | Apply PadLeft |    Sorting    |     List
            |               |               |
 "The 1st"  |  "The 001st"  |  "The 001st"  |  "The 1st"
 "The 12th" |  "The 012th"  |  "The 002nd"  |  "The 2nd"
 "The 2nd"  |  "The 002nd"  |  "The 012th"  |  "The 12th"

Працює з кількома числами:

 Alphabetical Sorting | Alphanumeric Sorting
                      |
 "Page 21, Line 42"   | "Page 3, Line 7"
 "Page 21, Line 5"    | "Page 3, Line 32"
 "Page 3, Line 32"    | "Page 21, Line 5"
 "Page 3, Line 7"     | "Page 21, Line 42"

Сподіваюся, що це допоможе.


6

Додавши до відповіді Грега Бука (тому що я щойно шукав це), якщо ви хочете використовувати це від Linq, ви можете використовувати те, OrderByщо бере IComparer. Наприклад:

var items = new List<MyItem>();

// fill items

var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer());

2

Ось відносно простий приклад, який не використовує P / Invoke і уникає будь-якого розподілу під час виконання.

internal sealed class NumericStringComparer : IComparer<string>
{
    public static NumericStringComparer Instance { get; } = new NumericStringComparer();

    public int Compare(string x, string y)
    {
        // sort nulls to the start
        if (x == null)
            return y == null ? 0 : -1;
        if (y == null)
            return 1;

        var ix = 0;
        var iy = 0;

        while (true)
        {
            // sort shorter strings to the start
            if (ix >= x.Length)
                return iy >= y.Length ? 0 : -1;
            if (iy >= y.Length)
                return 1;

            var cx = x[ix];
            var cy = y[iy];

            int result;
            if (char.IsDigit(cx) && char.IsDigit(cy))
                result = CompareInteger(x, y, ref ix, ref iy);
            else
                result = cx.CompareTo(y[iy]);

            if (result != 0)
                return result;

            ix++;
            iy++;
        }
    }

    private static int CompareInteger(string x, string y, ref int ix, ref int iy)
    {
        var lx = GetNumLength(x, ix);
        var ly = GetNumLength(y, iy);

        // shorter number first (note, doesn't handle leading zeroes)
        if (lx != ly)
            return lx.CompareTo(ly);

        for (var i = 0; i < lx; i++)
        {
            var result = x[ix++].CompareTo(y[iy++]);
            if (result != 0)
                return result;
        }

        return 0;
    }

    private static int GetNumLength(string s, int i)
    {
        var length = 0;
        while (i < s.Length && char.IsDigit(s[i++]))
            length++;
        return length;
    }
}

Він не ігнорує провідні нулі, тому 01приходить після 2.

Тест відповідного блоку:

public class NumericStringComparerTests
{
    [Fact]
    public void OrdersCorrectly()
    {
        AssertEqual("", "");
        AssertEqual(null, null);
        AssertEqual("Hello", "Hello");
        AssertEqual("Hello123", "Hello123");
        AssertEqual("123", "123");
        AssertEqual("123Hello", "123Hello");

        AssertOrdered("", "Hello");
        AssertOrdered(null, "Hello");
        AssertOrdered("Hello", "Hello1");
        AssertOrdered("Hello123", "Hello124");
        AssertOrdered("Hello123", "Hello133");
        AssertOrdered("Hello123", "Hello223");
        AssertOrdered("123", "124");
        AssertOrdered("123", "133");
        AssertOrdered("123", "223");
        AssertOrdered("123", "1234");
        AssertOrdered("123", "2345");
        AssertOrdered("0", "1");
        AssertOrdered("123Hello", "124Hello");
        AssertOrdered("123Hello", "133Hello");
        AssertOrdered("123Hello", "223Hello");
        AssertOrdered("123Hello", "1234Hello");
    }

    private static void AssertEqual(string x, string y)
    {
        Assert.Equal(0, NumericStringComparer.Instance.Compare(x, y));
        Assert.Equal(0, NumericStringComparer.Instance.Compare(y, x));
    }

    private static void AssertOrdered(string x, string y)
    {
        Assert.Equal(-1, NumericStringComparer.Instance.Compare(x, y));
        Assert.Equal( 1, NumericStringComparer.Instance.Compare(y, x));
    }
}

2

Я фактично реалізував його як метод розширення, StringComparerщоб ви могли зробити це, наприклад:

  • StringComparer.CurrentCulture.WithNaturalSort() або
  • StringComparer.OrdinalIgnoreCase.WithNaturalSort().

Отримана IComparer<string>може використовуватися у всіх місцях , як OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>і т.д. І ви все ще можете легко Твік чутливість до регістру, культурі і т.д.

Реалізація досить тривіальна, і вона повинна бути досить ефективною навіть у великих послідовностях.


Я також опублікував це як крихітний пакет NuGet , тож ви можете просто зробити:

Install-Package NaturalSort.Extension

Код, що включає коментарі документації XML та пакет тестів , доступний у сховищі NaturalSort.Extension GitHub .


Весь код такий (якщо ви ще не можете використовувати C # 7, просто встановіть пакет NuGet):

public static class StringComparerNaturalSortExtension
{
    public static IComparer<string> WithNaturalSort(this StringComparer stringComparer) => new NaturalSortComparer(stringComparer);

    private class NaturalSortComparer : IComparer<string>
    {
        public NaturalSortComparer(StringComparer stringComparer)
        {
            _stringComparer = stringComparer;
        }

        private readonly StringComparer _stringComparer;
        private static readonly Regex NumberSequenceRegex = new Regex(@"(\d+)", RegexOptions.Compiled | RegexOptions.CultureInvariant);
        private static string[] Tokenize(string s) => s == null ? new string[] { } : NumberSequenceRegex.Split(s);
        private static ulong ParseNumberOrZero(string s) => ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var result) ? result : 0;

        public int Compare(string s1, string s2)
        {
            var tokens1 = Tokenize(s1);
            var tokens2 = Tokenize(s2);

            var zipCompare = tokens1.Zip(tokens2, TokenCompare).FirstOrDefault(x => x != 0);
            if (zipCompare != 0)
                return zipCompare;

            var lengthCompare = tokens1.Length.CompareTo(tokens2.Length);
            return lengthCompare;
        }
        
        private int TokenCompare(string token1, string token2)
        {
            var number1 = ParseNumberOrZero(token1);
            var number2 = ParseNumberOrZero(token2);

            var numberCompare = number1.CompareTo(number2);
            if (numberCompare != 0)
                return numberCompare;

            var stringCompare = _stringComparer.Compare(token1, token2);
            return stringCompare;
        }
    }
}

2

Ось наївний однорядковий LINQ спосіб без зворотного випромінювання (запозичений з python):

var alphaStrings = new List<string>() { "10","2","3","4","50","11","100","a12","b12" };
var orderedString = alphaStrings.OrderBy(g => new Tuple<int, string>(g.ToCharArray().All(char.IsDigit)? int.Parse(g) : int.MaxValue, g));
// Order Now: ["2","3","4","10","11","50","100","a12","b12"]

Видалено Dump () і призначить var, і це працює як шарм!
Arne S

@ArneS: Це було написано на LinQPad; і я забув зняти Dump(). Дякуємо, що вказали.
mshsayem

1

Розширюючи декілька попередніх відповідей і використовуючи методи розширення, я придумав наступне, що не має застережень щодо потенційного багаторазового перерахування, або проблем, пов'язаних з використанням декількох об'єктів регулярних виразів, або без потреби викликати регулярний вираз, як говориться, він використовує ToList (), який може заперечувати переваги у великих колекціях.

Селектор підтримує загальне введення тексту, щоб дозволити призначити будь-якого делегата, елементи в колекції джерел мутуються селектором, потім перетворюються на рядки з ToString ().

    private static readonly Regex _NaturalOrderExpr = new Regex(@"\d+", RegexOptions.Compiled);

    public static IEnumerable<TSource> OrderByNatural<TSource, TKey>(
        this IEnumerable<TSource> source, Func<TSource, TKey> selector)
    {
        int max = 0;

        var selection = source.Select(
            o =>
            {
                var v = selector(o);
                var s = v != null ? v.ToString() : String.Empty;

                if (!String.IsNullOrWhiteSpace(s))
                {
                    var mc = _NaturalOrderExpr.Matches(s);

                    if (mc.Count > 0)
                    {
                        max = Math.Max(max, mc.Cast<Match>().Max(m => m.Value.Length));
                    }
                }

                return new
                {
                    Key = o,
                    Value = s
                };
            }).ToList();

        return
            selection.OrderBy(
                o =>
                String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0')))
                     .Select(o => o.Key);
    }

    public static IEnumerable<TSource> OrderByDescendingNatural<TSource, TKey>(
        this IEnumerable<TSource> source, Func<TSource, TKey> selector)
    {
        int max = 0;

        var selection = source.Select(
            o =>
            {
                var v = selector(o);
                var s = v != null ? v.ToString() : String.Empty;

                if (!String.IsNullOrWhiteSpace(s))
                {
                    var mc = _NaturalOrderExpr.Matches(s);

                    if (mc.Count > 0)
                    {
                        max = Math.Max(max, mc.Cast<Match>().Max(m => m.Value.Length));
                    }
                }

                return new
                {
                    Key = o,
                    Value = s
                };
            }).ToList();

        return
            selection.OrderByDescending(
                o =>
                String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0')))
                     .Select(o => o.Key);
    }

1

Ось натхненний рішенням Майкла Паркера, ось IComparerреалізація, за допомогою якої ви можете скористатися будь-яким із способів замовлення linq:

private class NaturalStringComparer : IComparer<string>
{
    public int Compare(string left, string right)
    {
        int max = new[] { left, right }
            .SelectMany(x => Regex.Matches(x, @"\d+").Cast<Match>().Select(y => (int?)y.Value.Length))
            .Max() ?? 0;

        var leftPadded = Regex.Replace(left, @"\d+", m => m.Value.PadLeft(max, '0'));
        var rightPadded = Regex.Replace(right, @"\d+", m => m.Value.PadLeft(max, '0'));

        return string.Compare(leftPadded, rightPadded);
    }
}

0

У нас виникла потреба в природному виді для обробки тексту за такою схемою:

"Test 1-1-1 something"
"Test 1-2-3 something"
...

З якоїсь причини, коли я вперше подивився на SO, я не знайшов цю посаду і реалізував нашу власну. У порівнянні з деякими рішеннями, представленими тут, хоча вони схожі за концепцією, це може мати користь, можливо, бути більш простою і зрозумілою. Однак, хоча я намагався подивитися на вузькі місця продуктивності, це все ще набагато повільніше реалізація, ніж за замовчуванням OrderBy().

Ось метод розширення, який я реалізую:

public static class EnumerableExtensions
{
    // set up the regex parser once and for all
    private static readonly Regex Regex = new Regex(@"\d+|\D+", RegexOptions.Compiled | RegexOptions.Singleline);

    // stateless comparer can be built once
    private static readonly AggregateComparer Comparer = new AggregateComparer();

    public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> source, Func<T, string> selector)
    {
        // first extract string from object using selector
        // then extract digit and non-digit groups
        Func<T, IEnumerable<IComparable>> splitter =
            s => Regex.Matches(selector(s))
                      .Cast<Match>()
                      .Select(m => Char.IsDigit(m.Value[0]) ? (IComparable) int.Parse(m.Value) : m.Value);
        return source.OrderBy(splitter, Comparer);
    }

    /// <summary>
    /// This comparer will compare two lists of objects against each other
    /// </summary>
    /// <remarks>Objects in each list are compare to their corresponding elements in the other
    /// list until a difference is found.</remarks>
    private class AggregateComparer : IComparer<IEnumerable<IComparable>>
    {
        public int Compare(IEnumerable<IComparable> x, IEnumerable<IComparable> y)
        {
            return
                x.Zip(y, (a, b) => new {a, b})              // walk both lists
                 .Select(pair => pair.a.CompareTo(pair.b))  // compare each object
                 .FirstOrDefault(result => result != 0);    // until a difference is found
        }
    }
}

Ідея полягає в тому, щоб розділити початкові рядки на блоки цифр і нецифрових знаків ( "\d+|\D+"). Оскільки це потенційно дороге завдання, воно виконується лише один раз за запис. Тоді ми використовуємо порівняльник об'єктів, що порівнюються (вибачте, я не можу знайти більш правильний спосіб сказати це). Він порівнює кожен блок з його відповідним блоком в іншому рядку.

Я хотів би отримати зворотний зв’язок про те, як це можна покращити та які основні вади. Зауважте, що технічна підтримка важлива для нас на даний момент, і ми зараз не використовуємо це у надзвичайно великих наборах даних.


1
Цей збій відбувається, коли він намагається порівняти структурно різні струни - наприклад, порівняння "a-1" з "a-2" працює чудово, але порівняння "a" з "1" це не так, оскільки "a" .CompareTo (1) кидає виняток.
jimrandomh

@jimrandomh, ти прав. Цей підхід був специфічним для наших моделей.
Ерік Ліпранді

0

Версія, яку легше читати / підтримувати.

public class NaturalStringComparer : IComparer<string>
{
    public static NaturalStringComparer Instance { get; } = new NaturalStringComparer();

    public int Compare(string x, string y) {
        const int LeftIsSmaller = -1;
        const int RightIsSmaller = 1;
        const int Equal = 0;

        var leftString = x;
        var rightString = y;

        var stringComparer = CultureInfo.CurrentCulture.CompareInfo;

        int rightIndex;
        int leftIndex;

        for (leftIndex = 0, rightIndex = 0;
             leftIndex < leftString.Length && rightIndex < rightString.Length;
             leftIndex++, rightIndex++) {
            var leftChar = leftString[leftIndex];
            var rightChar = rightString[leftIndex];

            var leftIsNumber = char.IsNumber(leftChar);
            var rightIsNumber = char.IsNumber(rightChar);

            if (!leftIsNumber && !rightIsNumber) {
                var result = stringComparer.Compare(leftString, leftIndex, 1, rightString, leftIndex, 1);
                if (result != 0) return result;
            } else if (leftIsNumber && !rightIsNumber) {
                return LeftIsSmaller;
            } else if (!leftIsNumber && rightIsNumber) {
                return RightIsSmaller;
            } else {
                var leftNumberLength = NumberLength(leftString, leftIndex, out var leftNumber);
                var rightNumberLength = NumberLength(rightString, rightIndex, out var rightNumber);

                if (leftNumberLength < rightNumberLength) {
                    return LeftIsSmaller;
                } else if (leftNumberLength > rightNumberLength) {
                    return RightIsSmaller;
                } else {
                    if(leftNumber < rightNumber) {
                        return LeftIsSmaller;
                    } else if(leftNumber > rightNumber) {
                        return RightIsSmaller;
                    }
                }
            }
        }

        if (leftString.Length < rightString.Length) {
            return LeftIsSmaller;
        } else if(leftString.Length > rightString.Length) {
            return RightIsSmaller;
        }

        return Equal;
    }

    public int NumberLength(string str, int offset, out int number) {
        if (string.IsNullOrWhiteSpace(str)) throw new ArgumentNullException(nameof(str));
        if (offset >= str.Length) throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be less than the length of the string.");

        var currentOffset = offset;

        var curChar = str[currentOffset];

        if (!char.IsNumber(curChar))
            throw new ArgumentException($"'{curChar}' is not a number.", nameof(offset));

        int length = 1;

        var numberString = string.Empty;

        for (currentOffset = offset + 1;
            currentOffset < str.Length;
            currentOffset++, length++) {

            curChar = str[currentOffset];
            numberString += curChar;

            if (!char.IsNumber(curChar)) {
                number = int.Parse(numberString);

                return length;
            }
        }

        number = int.Parse(numberString);

        return length;
    }
}

-2

Дозвольте пояснити свою проблему і як я зміг її вирішити.

Проблема: - Сортування файлів на основі FileName з об'єктів FileInfo, які витягнуті з каталогу.

Рішення: - Я вибрав імена файлів з FileInfo і обрізав ".png" частину імені файлу. Тепер просто зробіть List.Sort (), який сортує назви файлів у порядку природного сортування. На основі мого тестування я виявив, що наявність .png порушує порядок сортування. Подивіться на наведений нижче код

var imageNameList = new DirectoryInfo(@"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();

Чи можу я знати причину -1 у цій відповіді?
girishkatta9
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.