Естественный порядок сортировки в C #

129

У кого-нибудь есть хороший ресурс или есть образец сортировки в естественном порядке на C # для FileInfoмассива? Реализую IComparerинтерфейс в своих сортах.

Майкл Книскерн
источник

Ответы:

149

Проще всего сделать 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
Отличный ответ. Предостережение: это не будет работать с Win2000 для тех немногих людей, которые все еще работают в этой операционной системе. С другой стороны, между блогом Каплана и документацией MSDN достаточно намеков, чтобы создать аналогичную функцию.
Крис Чарабарук,
9
Это не переносимо, работает только в Win32, но не работает в Linux / MacOS / Silverlight / Windows Phone / Metro
linquize
20
@linquize - Он сказал, что .NET не Mono, поэтому Linux / OSX на самом деле не проблема. Windows Phone / Metro не существовало в 2008 году, когда был опубликован этот ответ. А как часто вы выполняете файловые операции в Silverlight? Так что для OP и, вероятно, большинства других людей это был подходящий ответ. В любом случае вы можете дать лучший ответ; вот как работает этот сайт.
Грег Бич
6
Это не означает, что исходный ответ был неправильным. Я просто добавляю дополнительную информацию с актуальной информацией
linquize
2
К вашему сведению, если вы наследуете от Comparer<T>вместо реализации IComparer<T>, вы получите встроенную реализацию IComparer(неуниверсального) интерфейса, который вызывает ваш общий метод, для использования в API, которые его используют. Это тоже в принципе бесплатно: просто удалите «I» и измените public int Compare(...)на public override int Compare(...). То же самое для IEqualityComparer<T>и EqualityComparer<T>.
Joe Amenta
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()создавать коллекции строк без каких-либо чисел ( для пустого перечислимого выдает an InvalidOperationException).

Мэтью Хорсли
источник
1
+1 Он не только самый лаконичный, но и самый быстрый, который я видел. за исключением принятого ответа, но я не могу использовать его из-за зависимости от машины. Он отсортировал более 4 миллионов значений примерно за 35 секунд.
Gene S
4
Это красиво и невозможно прочитать. Я предполагаю, что преимущества Linq будут означать (по крайней мере) лучшую среднюю и лучшую производительность, поэтому я думаю, что собираюсь пойти на это. Несмотря на отсутствие ясности. Большое спасибо @Matthew Horsley
Ян Грейнджер
1
Это очень хорошо, но есть ошибка для некоторых десятичных чисел, в моем примере сортировка k8.11 vs 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();
JD
источник
Разделы цифр необходимо сравнивать по разделам, т. Е. «Abc12b» должно быть меньше «abc123».
SOUser
Вы можете попробовать следующие данные: 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 ","
SOUser
@XichenLi Спасибо за хороший тестовый пример. Если вы позволите проводнику Windows сортировать эти файлы, вы получите разные результаты в зависимости от того, какую версию Windows вы используете. Мой код сортирует эти имена идентично Server 2003 (и, предположительно, XP), но отличается от Windows 8. Если у меня будет возможность, я попытаюсь выяснить, как это делает Windows 8, и обновлю свой код.
JD
3
Есть ошибка. Index Out Of Range
Linquize
3
Отличное решение! Когда я тестировал его в обычном сценарии с примерно 10 000 файлов, он был быстрее, чем пример регулярного выражения Мэтью, и примерно такой же по производительности, как StrCmpLogicalW (). В приведенном выше коде есть небольшая ошибка: "while (strA [jA] == zeroA) jA ++;" и "while (strB [jB] == zeroB) jB ++;" должно быть "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" и «while (jB <strB.Length && strB [jB] == zeroB) jB ++;». В противном случае строки, содержащие только нули, вызовут исключение.
kuroki
22

Чистое решение C # для linq orderby:

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
В сообщении блога за IComparer упоминается Джастин Джонс ( codeproject.com/KB/string/NaturalSortComparer.aspx ), а не Паскаль Ганай.
Джеймс МакКормак,
1
Незначительное примечание: это решение игнорирует пробелы, которые не совпадают с тем, что делают окна, и не так хорошо, как приведенный ниже код Мэтью Хорсли. Таким образом, вы можете получить, например, 'string01' 'string 01' 'string 02' 'string02' (что выглядит некрасиво). Если вы удалите разделение пробелов, строки будут упорядочены в обратном порядке, т.е. строка 01 стоит перед строкой 01, что может быть или неприемлемо.
Майкл Паркер,
Это работало для адресов, например «1 Smith Rd», «10 Smith Rd», «2 Smith Rd» и т. Д. - Сортировка естественным образом. Да! Хороший!
Петр Кула
Кстати, я заметил (и комментарии к этой связанной странице также, кажется, указывают), что аргумент Type <T> совершенно не нужен.
jv-dev
18

Ответ Мэтьюса Хорсли - это самый быстрый метод, который не меняет поведения в зависимости от того, на какой версии 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);

Это занимает 450 мс для сортировки 100 000 строк по сравнению с 300 мс для сравнения строк .net по умолчанию - довольно быстро!

Майкл Паркер
источник
2
Это стоит прочитать в отношении вышеизложенного - Компиляция и повторное использование в регулярных выражениях
mungflesh
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
mpen
источник
Мне это нравится. Это легко понять и не требует Linq.
11

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

Транзитивное сравнение всегда будет сообщать, что a <c, если a <b и b <c. Существует функция, которая выполняет естественное сравнение порядка сортировки, которое не всегда соответствует этому критерию, но я не могу вспомнить, является ли это StrCmpLogicalW или что-то еще.

Джонатан Гилберт
источник
У вас есть доказательства этого утверждения? После поиска в Google я не могу найти никаких указаний на то, что это правда.
mhenry1384
1
Я испытал эти бесконечные циклы с StrCmpLogicalW.
чт,
Элемент обратной связи Visual Studio 236900 больше не существует, но вот более свежий, подтверждающий проблему: 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"

Надеюсь, это поможет.

Picsonald
источник
6

Добавляя к ответу Грега Бича (потому что я только что это искал), если вы хотите использовать это из Linq, вы можете использовать файл, OrderByкоторый принимает файл IComparer. Например:

var items = new List<MyItem>();

// fill items

var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer());
Wilka
источник
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"]
mshsayem
источник
Удален 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);
    }
Vorspire
источник
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();
girishkatta9
источник
Могу ли я узнать причину -1 в этом ответе?
girishkatta9