6

I have a control which shows a list of all PDFs in a selected folder. The names of most of these PDFs (for example, meeting minutes) begin with a date. I want these PDFs shown in descending order so that the most recent PDFs are shown at the top.

In the same folder I also have some PDFs whose names do not contain a date (for example, policies). I want these files to be ordered alphabetically.

Here is an example of the kind of list I have and how I want it to be ordered:

  • 2019-01-12 Meeting minutes.pdf
  • 2018-11-19 Meeting agenda.pdf
  • 2018-06-02 Meeting minutes.pdf
  • 2017-12-13 Meeting agenda.pdf
  • 2017-04-27 Meeting minutes.pdf
  • Privacy policy.pdf
  • Safeguarding policy.pdf
  • Welfare policy.pdf

And here is what I have tried:

private void GenerateFolder()
{
    folder_path = Server.MapPath(BaseFolder);
    _folder_view = new StringBuilder();

    if (Directory.Exists(folder_path))
    {
        DirectoryInfo info = new DirectoryInfo(folder_path);
        FileInfo[] files = info.GetFiles().OrderByDescending(p => p.FullName).ToArray();

        foreach (FileInfo file in files)
        { 
            doStuff();
        }
    }

    folder_view.Text = _folder_view.ToString();
}

With this code, the PDFs whose names begin with a date are ordered in descending order (by date) which is what I want. But the other PDFs whose names do not begin with a date are not ordered the way I would like (alphabetically). Is there a way to accomplish my dual-sorting objective?

DavidRR
  • 18,291
  • 25
  • 109
  • 191
  • You need to us OrderByDescending using Date along with Full Name. So use ThenByDescending. – jdweng Apr 18 '19 at 11:47
  • 1
    Perhaps using this regex solution: https://stackoverflow.com/questions/8443524/using-directory-getfiles-with-a-regex-in-c , you could generate two partial lists which you would then combine. – LocEngineer Apr 18 '19 at 11:56
  • 3
    The canonical way to do this would be to write a custom [`IComparer`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=netframework-4.7.2) that implements your custom sorting logic. You can then pass that in to the proper overload of [`OrderBy`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderby?view=netframework-4.7.2#System_Linq_Enumerable_OrderBy__2_System_Collections_Generic_IEnumerable___0__System_Func___0___1__System_Collections_Generic_IComparer___1__). That may be inefficient though, because of having to constantly... – Bradley Uffner Apr 18 '19 at 12:00
  • 2
    ...A more performant solution would be to parse the filenames in to a custom class, breaking apart the date and the name, implementing an `IComparer` on *that*. – Bradley Uffner Apr 18 '19 at 12:03
  • `.OrderByDescending(...).ThenBy(...);` – Zazaeil Apr 18 '19 at 12:36

2 Answers2

4

I would create a custom class that parses out the date and the rest of the filename in to separate properties, you can then use OrderByDescending and ThenBy to sort on these individual properties.

public class ParsedFilename
{
    public ParsedFilename(string filename)
    {
        FullName = filename;
        if (filename.Length >= 12 &&
            DateTime.TryParse(filename.Substring(0, 10), out var date))
        {
            Date = date;
            Name = filename.Substring(11);
        }
        else
        {
            Date = null;
            Name = filename;
        }
    }

    public DateTime? Date { get; }
    public string Name { get; }
    public string FullName { get; }
}

You can use it like this:

var data = new List<string>(new[]
    {
        "2019-01-12 Meeting minutes.pdf",
        "Safeguarding policy.pdf",
        "2017-04-27 Meeting minutes.pdf",
        "2018-06-02 Meeting minutes.pdf",
        "2017-12-13 Meeting agenda.pdf",
        "Privacy policy.pdf",
        "Welfare policy.pdf",
        "2018-11-19 Meeting agenda.pdf"
    });

var parsedData = data.Select(d => new ParsedFilename(d));

var sortedData = parsedData.OrderByDescending(d => d.Date)
                           .ThenBy(d => d.Name);

var output = sortedData.Select(d => d.FullName);

It produces the following output:

2019-01-12 Meeting minutes.pdf
2018-11-19 Meeting agenda.pdf
2018-06-02 Meeting minutes.pdf
2017-12-13 Meeting agenda.pdf
2017-04-27 Meeting minutes.pdf
Privacy policy.pdf
Safeguarding policy.pdf
Welfare policy.pdf

Depending on the formats of the filenames in your directory, you may have to add some more robust parsing.

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
1

In a comment, LocEngineer suggested (paraphrasing):

Start by using a regex-based technique as shown by answers to this question to determine those file names that begin with dates. And then generate two partial lists which you would then combine.

In the spirit of that suggestion, here's a demonstration of one possible solution:

public void CustomFileNameOrderingDemo()
{
    var files = new[]
    {
        "2017-04-27 Meeting minutes.pdf",
        "2018-11-19 Meeting agenda.pdf",
        "2019-01-12 Meeting minutes.pdf",
        "2018-06-02 Meeting minutes.pdf",
        "2017-12-13 Meeting agenda.pdf",
        "Safeguarding policy.pdf",
        "Welfare policy.pdf",
        "Privacy policy.pdf",
    };

    var filesWithDates = FindFilesWithDates().OrderByDescending(f => f).ToList();
    var filesWithoutDates = files.Except(filesWithDates).OrderBy(f => f);
    var result = filesWithDates.Concat(filesWithoutDates);

    IEnumerable<string> FindFilesWithDates()
    {
        return files.Where(f => Regex.IsMatch(f, @"^[0-9]{4}-[0-9]{2}-[0-9]{2} "));
    }
}
DavidRR
  • 18,291
  • 25
  • 109
  • 191