11

I have a list that needs to be ordered in a specific way. I've currently solved it like this:

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

The files are going to be merged into a single PDF file and the point here is that certain files should come at the beginning, and the rest at the end.

I'm wondering if there is a better way to write this "pattern", cause it feels rather blah and would become even more blah if there were more cases.


Things I'd like to avoid, but not sure how: Multiple passes through the list, more StartsWith calls per file than necessary, more code than necessary, etc.

Basically I think I'd like an OrderByPredicates sort of thing which smartly fulfilled those criteria and whose API was used sort of like this:

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);
Svish
  • 152,914
  • 173
  • 462
  • 620
  • You know that the ordering is false, true, right? So ProjectDescription are last? – xanatos Mar 04 '15 at 13:37
  • 1
    @xanatos that's why the OP is using `OrderBy**Decending**` – D Stanley Mar 04 '15 at 13:39
  • I'm not sure I understand the problem with what you currently have, if the filename is what dictates it the order how else do you think this can be done? You could perhaps tag a custom property on the file and use that to indicate merge order or perhaps inspect the MIME type. – James Mar 04 '15 at 13:43
  • generate a priority mapping table, that you either load from code/db/config etc. – hazimdikenli Mar 04 '15 at 13:44
  • 1
    Your code works (I assume), is not _too_long, is very straightforward and easy to understand. Unless you expect a lot of maintenance on the ordering scheme I would leave it the way it is. – D Stanley Mar 04 '15 at 13:49
  • @DStanley I probably will, but was curious if there were any smart patterns on this sort of thing. – Svish Mar 04 '15 at 13:52

7 Answers7

7

Compact (except for a little helper method) and easy to extend:

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));
Sebastian Negraszus
  • 11,915
  • 7
  • 43
  • 70
  • 2
    Oh, basically the same as my answer - I didn't see this before I posted mine. :) The only real difference is I made mine a non-static class. – Matthew Watson Mar 04 '15 at 13:57
  • In the more general case where multiple predicates can match, this behaves differently than expected. – user253751 Mar 04 '15 at 18:38
3

Powers of two?

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

Note that I removed the Descending and used a reverse weight for the StartsWith.

It's probably even slower than yours, because this algo always require 3x StartsWith for each comparison, while yours could "block" at the first StartsWith

Note that I would probably do something like:

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

In this way I keep the orders in a string array. To make the code easier I haven't put a check for orders.Length > 0

xanatos
  • 109,618
  • 12
  • 197
  • 280
2

I would encapsulate the ordering logic in a separate class, for example:

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

Then if you need to add a new item, you just need to add it to the list of prefixes and no other code needs to change.

You would use it like this:

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

This is a lot more lines of code, of course, but it seems better encapsulated and easier to change.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
1

One way that I could see making this a little cleaner will add a little bit of overall complexity, but provide a cleaner ordering mechanism.

First, I would create an enum of the different types of files:

public enum FileType
{
    ProjectDescription,
    Budget,
    CV
}

Then, create a little wrapper for the files:

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

Finally, when you gather all of your files, you would set them up in the new class, and your query would go something like this:

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

You could always omit the ThenBy if you don't care.

Overall, with three it's a bit overkill, but if additional types of files are added to your process, this would give you the most flexibility, and allow for your query to remain unchanged.

krillgar
  • 12,596
  • 6
  • 50
  • 86
1

Based on several of your answers and some further thinking, I came up with this class, which I think turned out fairly clean. Should be quite generic and should be fairly easy to maintain even with higher numbers of predicates or changes in order.

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

Example usage based on my original question:

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

Alternatively one could encapsulate the order in a subclass to make the final code even cleaner:

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

Feedback in the comments welcome :)

Svish
  • 152,914
  • 173
  • 462
  • 620
1

Although I agree with the others that it would be best to encapsulate the order in another class, here is an attempt for your OrderByPredicates() as extension method:

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

With this extension method you can do exactly what you wanted:

using FileOrderExtensions;

var files = GetFiles()
  .OrderByPredicates(predicates)
  .ThenBy(x => x.Filename); 
Thomas F.
  • 717
  • 3
  • 13
  • Interesting! Think I prefer my solution using an `IComparer` since it can be injected into regular `OrderBy` and `ThenBy`, but that would definitely work. – Svish Mar 04 '15 at 15:12
  • Whatever you prefer. ;) I just want to point out that one of your goals was to avoid unnecessary calls to StartsWith() and with your comparer, you may have a lot more of these calls. – Thomas F. Mar 04 '15 at 15:22
  • Indeed ;) That's definitely true though. Any ideas on how to limit the number of `StartsWith` calls in my comparer? Cache the results? – Svish Mar 04 '15 at 15:47
  • Caching would be an option. But to be honest, I dislike the idea of a stateful comparer (keeping references of all compared objects). – Thomas F. Mar 04 '15 at 16:11
  • I definitely dislike the idea of caching as well, hehe. – Svish Mar 05 '15 at 15:10
1

This is as generic as it can be

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

and use it like that. If you want to merge the last "ThenBy" with your OrderByPredicates, just use a Func collection

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

You could give the function a already ordered collection, so that the implementation would be a lot simpler.

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

The main advantage is that you could then implement a "ThenByDescendingPredicates" function too.

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

But you actually need it to be descending, but what if you need some fields to be ascending and other not? (true for ascending and false for descending)

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

But it would be harder to use

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);
Bruno
  • 623
  • 2
  • 9
  • 24