Более короткий способ упорядочить список по логическим функциям

У меня есть список, который нужно упорядочить определенным образом. Я в настоящее время решил это следующим образом:

var files = GetFiles()
  .OrderByDescending(x => x.Filename.StartsWith("ProjectDescription_"))
  .ThenByDescending(x => x.Filename.StartsWith("Budget_"))
  .ThenByDescending(x => x.Filename.StartsWith("CV_"))
  .ToArray();

Файлы будут объединены в один файл PDF, и дело здесь в том, что определенные файлы должны начинаться в начале, а остальные - в конце.

Мне интересно, есть ли лучший способ написать этот "шаблон", потому что он чувствует себя довольно бла-бла-бла-бла-бла-бла, если бы было больше случаев.


Вещи, которые я хотел бы избежать, но не знаю, как: Несколько проходов по списку, больше StartsWith вызовов на файл, чем необходимо, больше кода, чем необходимо, и т.д.

В принципе, мне кажется, что я хотел бы использовать OrderByPredicates вещь, которая умело выполнила эти критерии и API которой был использован примерно так:

var predicates = new Func<boolean, File>[] {
  x => x.Filename == "First"
  x => x.Filename.StartsWith("Foo_"),
  x => x.Filename.StartsWith("Bar_"),
};

var files = GetFiles()
  .OrderByPredicates(predicates)
  .ThenBy(x => x.Filename);

Ответ 1

Компактный (за исключением небольшого вспомогательного метода) и легко расширяемый:

private static readonly string[] Prefixes = {"ProjectDescription_", "Budget_", "CV_"};

public static int PrefixIndex(string name)
{
  for (int i = 0; i < Prefixes.Length; i++)
  {
    if (name.StartsWith(Prefixes[i]))
    {
      return i;
    }
  }
  return int.MaxValue;
}

// ...

var files = GetFiles().OrderBy(x => PrefixIndex(x.Name));

Ответ 2

Силы двух?

var files = GetFiles()
  .Order(x => (x.Filename.StartsWith("ProjectDescription_") ? 4 : 0) + 
              (x.Filename.StartsWith("Budget_") ? 2 : 0) +
              (x.Filename.StartsWith("CV_") ? 1 : 0))
  .ToArray()

Обратите внимание, что я удалил Descending и использовал обратный вес для StartsWith.

Это, вероятно, даже медленнее, чем у вас, потому что для каждого сравнения всегда требуется 3x StartsWith, в то время как ваш может "блокировать" при первом StartsWith

Обратите внимание, что я, вероятно, сделаю что-то вроде:

string[] orders = new string[] { "ProjectDescription_", "Budget_", "CV_" };

var files = GetFiles()
    .OrderByDescending(x => x.Filename.StartsWith(orders[0]));

for (int i = 1; i < orders.Length; i++) {
    files = files.ThenByDescending(x => x.Filename.StartsWith(orders[i]));
}

var files2 = files.ToArray();

Таким образом, я сохраняю порядок в массиве строк. Чтобы сделать код проще, я не поставил чек на orders.Length > 0

Ответ 3

Я бы инкапсулировал логику упорядочения в отдельный класс, например:

class FileNameOrderer
{
    public FileNameOrderer()
    {
        // Add new prefixes to the following list in the order you want:

        orderedPrefixes = new List<string>
        {
            "CV_",
            "Budget_",
            "ProjectDescription_"
        };
    }

    public int Ordinal(string filename)
    {
        for (int i = 0; i < orderedPrefixes.Count; ++i)
            if (filename.StartsWith(orderedPrefixes[i]))
                return i;

        return orderedPrefixes.Count;
    }

    private readonly List<string> orderedPrefixes;
}

Затем, если вам нужно добавить новый элемент, вам просто нужно добавить его в список префиксов, а другой код не нужно изменять.

Вы бы использовали его следующим образом:

var orderer = new FileNameOrderer();
var f = files.OrderBy(x => orderer.Ordinal(x.Filename)).ToArray();

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

Ответ 4

Один из способов, с помощью которого я мог бы сделать это немного чище, добавит немного общей сложности, но обеспечит более чистый механизм упорядочения.

Во-первых, я бы создал перечисление различных типов файлов:

public enum FileType
{
    ProjectDescription,
    Budget,
    CV
}

Затем создайте небольшую оболочку для файлов:

public class FileWrapper
{
    public FileType FileType { get; set; }
    public string FileName { get; set; }
}

Наконец, когда вы соберете все свои файлы, вы должны установить их в новом классе, и ваш запрос будет выглядеть примерно так:

var files = GetFiles().OrderBy(f => (int)f.FileType)
                      .ThenBy(f => f.FileName)
                      .Select(f => f.FileName);

Вы всегда можете опустить ThenBy, если вам все равно.

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

Ответ 5

Основываясь на нескольких ваших ответах и ​​некоторых дальнейших размышлениях, я придумал этот класс, который, я думаю, оказался довольно чистым. Должно быть довольно общим и должно быть довольно легко поддерживать даже с большим количеством предикатов или изменениями в порядке.

public class OrderedPredicatesComparer<T> : IComparer<T>
{
    private readonly Func<T, bool>[] ordinals;
    public OrderedPredicatesComparer(IEnumerable<Func<T, bool>> predicates)
    {
        ordinals = predicates.ToArray();
    }

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

    private int GetOrdinal(T item)
    {
        for (int i = 0; i < ordinals.Length; i++)
            if (ordinals[i](item))
                return i - ordinals.Length;
        return 0;
    }
}

Пример использования, основанный на моем исходном вопросе:

var ordering = new Func<string, bool>[]
    {
        x => x.StartsWith("ProjectDescription_"),
        x => x.StartsWith("Budget_"),
        x => x.StartsWith("CV_"),
    };

var files = GetFiles()
    .OrderBy(x => x.Filename, new OrderedPredicatesComparer<string>(ordering))
    .ThenBy(x => x.Filename)
    .ToArray();

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

public class MySpecificOrdering : OrderedPredicatesComparer<string>
{
    private static readonly Func<string, bool>[] order = new Func<string, bool>[]
        {
            x => x.StartsWith("ProjectDescription_"),
            x => x.StartsWith("Budget_"),
            x => x.StartsWith("CV_"),
        };

    public MySpecificOrdering() : base(order) {}
}

var files = GetFiles()
    .OrderBy(x => x.Filename, new MySpecificOrdering())
    .ThenBy(x => x.Filename)
    .ToArray();

Обратная связь в комментариях приветствуется:)

Ответ 6

Хотя я согласен с другими, что лучше всего инкапсулировать заказ в другом классе, вот попытка для вашего OrderByPredicates() как метода расширения:

public static class FileOrderExtensions
{
  public static IOrderedEnumerable<File> OrderByPredicates(this IEnumerable<File> files, Func<File, bool>[] predicates)
  {
    var lastOrderPredicate = new Func<File, bool>(file => true);

    var predicatesWithIndex = predicates
      .Concat(new [] { lastOrderPredicate })
      .Select((predicate, index) => new {Predicate = predicate, Index = index});

    return files
      .OrderBy(file => predicatesWithIndex.First(predicateWithIndex => predicateWithIndex.Predicate(file)).Index);
  }
}

С помощью этого метода расширения вы можете выполнить именно то, что вы хотели:

using FileOrderExtensions;

var files = GetFiles()
  .OrderByPredicates(predicates)
  .ThenBy(x => x.Filename); 

Ответ 7

Это так же общее, как это может быть

public static IOrderedEnumerable<T> OrderByPredicates<T, U>(this IEnumerable<T> collection, IEnumerable<Func<T, U>> funcs)
{
    if(!funcs.Any())
    {
        throw new ArgumentException();
    }
    return funcs.Skip(1)
       .Aggregate(collection.OrderBy(funcs.First()), (lst, f) => lst.ThenBy(f));
}

и использовать его так. Если вы хотите объединить последний "ThenBy" с вашими OrderByPredicates, просто используйте коллекцию Func

var predicates = new Func<File, bool>[]
{
    x => x.FileName == "First",
    x => x.FileName.StartsWith("Foo_"),
    x => x.FileName.StartsWith("Bar_")
};
var files = GetFiles()
            .OrderByPredicates(predicates)
            .ThenBy(x => x.Filename);

Вы можете дать функции уже упорядоченной коллекции, так что реализация будет намного проще.

public static IOrderedEnumerable<T> ThenByPredicates<T,U>(this IOrderedEnumerable<T> collection, IEnumerable<Func<T, U>> funcs)
{
    return funcs.Aggregate(collection, (lst, f) => lst.ThenBy(f));
}

Основное преимущество заключается в том, что вы могли бы также реализовать функцию "ThenByDescendingPredicates".

GetFiles().OrderByDescending(x=>...).ThenByPredicates(predicates).ThenByPredicatesDescending(descendingsPredicate);

Но вам действительно нужно, чтобы он нисходил, но что, если вам нужно, чтобы какие-то поля были восходящими, а другие нет? (true для восходящего и ложного для нисходящего)

public static IOrderedEnumerable<T> OrderByPredicates<T, U>(this IOrderedEnumerable<T> collection, IEnumerable<KeyValuePair<bool, Func<T, U>>> funcs)
{

    if(!funcs.Any())
    {
        throw new ArgumentException();
    }
    var firstFunction = funcs.First();
    return funcs.Skip(1).Aggregate(
         firstFunction.Key?collection.OrderBy(firstFunction.Value):collection.OrderByDescending(firstFunction.Value)
        , (lst, f) => f.Key ? lst.ThenBy(f.Value) : lst.ThenByDescending(f.Value));
}

Но было бы труднее использовать

var predicates = new KeyValuePair<bool, Func<File, bool>>[] {
          new KeyValuePair<bool, Func<string, bool>>(false, x => x.FileName == "First"),
          new KeyValuePair<bool, Func<string, bool>>(false, x => x.FileName.StartsWith("Foo_")),
          new KeyValuePair<bool, Func<string, bool>>(false, x => x.FileName.StartsWith("Bar_")),
        };

var files = GetFiles()
            .OrderByPredicates(predicates)
            .ThenBy(x => x.Filename);