tags:

views:

113

answers:

3

Hello,

I have a program that find all .csv reports file in a directory. This reports are stored in a List

List<string> _items = new List<string>();

this reports are named like this:

"month year.csv" (example: "Avgust 2010.csv")

I would like to sort my reports by month.

Months are in this order:

januar, februar, marec, april, maj, junij, julij, avgust, september, oktober, november, december

How can I do that?

Br, Wolfy

+4  A: 

It sounds like you should write a method to parse a filename into a DateTime. For example, it might take off the extension, split the rest by the space, and then parse the second part as the year and look up the month name in a table.

Once you've got that method, you can just do:

_items = _items.OrderBy(FilenameToDate).ToList();
Jon Skeet
No, this is right - it is method group.
Archeg
@generic No, this is right. The intent is to pass the delegate into OrderBy so it can be called during sorting, not to call it an pass an undefined result to OrderBy
AHM
Woops, yes. It's early.
GenericTypeTea
FilenameToDate could be DateTime.Parse, when cutting off the extension.
Jens
@Jens: Assuming there's an appropriate .NET culture with exactly the right month names.
Jon Skeet
Can someone write this FilenameToDate method?
Wolfy
@Wolfy: I've given you each of the steps required. Try it yourself, and post if you have *specific* problems. Stack Overflow doesn't exist to write your code for you, but to help you write it yourself.
Jon Skeet
@Wolfy, a `FilenameToDate` lambda is in my answer, and another lambda that allows for sort in place. @Jon, in fairness when I read the question I assumed it was the bit that `FilenameToDate` does that caused Wolfy difficulty rather than the `OrderBy`.
Jon Hanna
Like you said... FilenameToDate is my problem... I tryed to do it by myself, but I really don't get it.
Wolfy
@Wolfy: So show us what you've tried, and what's going wrong. "I really don't get it" isn't a specific problem.
Jon Skeet
I tried to do my "FilenameToDate" method, but it did not worked... Don't be angry, maybe this problen is not a real problem for you, but for me was a realy big problem...
Wolfy
@Wolfy: Again, "It did not worked" isn't specific, which means we can't help you. If you could edit your question with your *attempt* at the method, we could try to help you discover what's wrong. But just saying you've tried and failed leaves us no way of helping you effectively.
Jon Skeet
"It did not worked" is not specific, because I needed an answer to how to sort this list, not how to sort this list in a specific way... Jon Hanna gave me this answer, and I accepted it, because this was what I needed.
Wolfy
@Wolfy, to lean towards Jon's position a bit (it's not that I just like disagreeing with everyone, honest!) do you get why my code works, and how it relates to Jon's? If not, it would be well worth making sure you do before moving on.
Jon Hanna
I'm working on that... Be sure that I will understand this, before moving on!Don't misunderstand me, I appreciate all your answers, and I always try to learn something from all your answers, not only with the accepted one.
Wolfy
@Wolfy, feel free to add questions on my answer. The important thing is learning the general reasoning behind the code rather than having a specific answer.
Jon Hanna
A: 

Something like:

class ReportProperties
{
    public string Location { get; set; }

    public int Month { get; set; }
    public int Year { get; set; }
}

private ReportProperties GetReportProperties(string location)
{
    ReportProperties result = null;

        string monthYear = System.IO.Path.GetFileNameWithoutExtension(location);
        string[] parts = monthYear.Split(' ');
        if (parts.Length == 2)
        {
            int? month = 0;

// Maybe you can use the culture info to parse the month's name, otherwise:

            switch (parts[0])
            {
                case "januar":
                    month = 1;
                    break;

                // ... repeat for each month
            }

            int year = 0;
            if (int.TryParse(parts[1], out year))
            {
                result = new ReportProperties() { Location = location, Year = year, Month = month.Value };
            }
        }

    return result;
}


List<ReportProperties> reportsProperties = new List<ReportProperties>();

foreach (string location in _items)
{
    ReportProperties properties = GetReportProperties(location);
    if (properties != null)
    {
        reportsProperties.Add(properties);
    }
}


var sortedItems = reportsProperties.OrderBy(item => item.Year).ThenBy(item => item.Month);
vc 74
I would *definitely* use a `Dictionary<string, int>` instead of a switch statement. I'd also use a simple `DateTime` rather than a custom type.
Jon Skeet
Agreed (I absolutely did not focus on performance). The dictionary would probably be useful elsewhere unless the month names are some culture's month names so I'd also suggest a static method using a static dictionary to perform the conversion.
vc 74
@Jon, @vc, I'd do neither. There's already code in the framework for parsing month names into dates, and its efficient enough for most uses, so I'd consider writing ones own to be a premature optimisation unless it had proven a bottleneck already.
Jon Hanna
@ Jon H, I did not know his culture was an 'official' one (BTW how did you figure it was Slovene?), my comment was based on the assumption it's a custom parsing logic. It would be good to know how many files he has to handle and if the conversion logic is required elsewhere.
vc 74
Ah, well how I knew it was Slovene was that it was clearly a calendar based on the Roman calendar as it looks pretty close to the English calendar names (indeed, closer than many European languages, e.g. the Irish and Welsh are quite different), so select, right-click, google, gets the full answer, but I was confident that there *was* a full answer the minute I saw how close they were to English.
Jon Hanna
+1 for proper googling then
vc 74
+2  A: 

The most compact complete solution would be something like:

CultureInfo sloveneCultureInfo = new CultureInfo("sl-SI");
_items = _items.OrderBy(fn => DateTime.ParseExact("01 " + fn, @"dd MMMM yyyy\.\c\s\v", sloveneCultureInfo)).ToList();

The above assigns this list back to itself. If you were going to do so, then you could gain in performance for relatively little increase in code complexity by sorting in-place:

CultureInfo sloveneCultureInfo = new CultureInfo("sl-SI");
_items.Sort((x, y) => DateTime.ParseExact("01 " + x, @"dd MMMM yyyy\.\c\s\v",sloveneCultureInfo).CompareTo(DateTime.ParseExact("01 " + y, @"dd MMMM yyyy\.\c\s\v", sloveneCultureInfo)));

(If you are running this on a system running in a Slovene locale, then you could use the CurrentCulture rather than the constructed sloveneCultureInfo above).

Further improvements can be made by using a comparer class rather than a lambda, and indeed the comparison itself could be more efficient than the above, but I wouldn't worry about this unless the sort proved to be a bottle-neck.

Edit: A break down on just what works here.

There are two convenient ways of sorting a list (strictly, one way of sorting a list and another of sorting any IEnumerable or IQueryable).

OrderBy takes a parameter that computes a sort key, and returns an IOrderedEnumerable<T> (or IOrderedQueryable<T> from now on I'm going to ignore the fact that this can be done on IQueryable as well, as the principle is the same). This class is one that in most ways acts like an IEnumerable<T> that is sorted by the key, with the only difference being that if you do a subsequent ThenBy on it, it keeps ordered by the first key, and hence lets you have multi-stage orderings.

So, in the code:

var x = _items.OrderBy(SomeMethod);

SomeMethod will return values that can be sorted, and on the basis of this, x will be given an IOrderedEnumerable<T> sorted accordingly. For example if SomeMethod took a string and returned the length of the string, then we could use it to sort a list of strings by length.

If we were then going to iterate through the _items, then this is our job done. If we wanted to have a new list then we could call Tolist() on the results, and that would return such a list.

The other approach, that only works on lists, is to call Sort(). There is a parameterless form that just sorts according to the default comparison of the type in question (if there is one), a form that takes an IComparer<T> object (more involved than necessary in most cases, though sometimes very useful) and a form that takes either a delegate or a lambda (strictly it takes a delegate, but we can use a lambda to create that delegate).

This delegate must receive two values of the type of the list's items and return a number that is negative if the first should be sorted earlier than the latter, zero if they are equivalent and a positive number otherwise.

Sort() changes the list its done on. This has an efficiency gain, but is clearly disastrous if you need to hold onto the original sort order too.

Okay, so far we've got two ways to sort a list. Both of them need a way to turn your file names into something that can be sorted on.

Since the file names relate to dates, and since dates are already sortable, the most sensible approach is to obtain those dates.

We can't create a date directly from the file name, because Avgust 2010 isn't a date, just a month-year value and there isn't a month year class in the BCL (there may be in other libraries but let's not gild lilies).

However, every month has a first day, and therefore we can create a valid date from "01 " concatenated with the file name. So our first step is to say we will act on "01 " + fn where fn is the file name.

This gives us strings in the form of e.g. "01 Avgust 2010.csv". Examining how date parsing works, we know we can use dd for the two-digit date, MMMM for the full name of the month in the relevant language (Slovenian in this case) and yyyy for the full year. We just need to add on \.\c\s\v to mean that it will be followed with a ".csv" that we don't parse, and we're set. We can hence turn the file name into the first of its month with DateTime.ParseExact("01 " + fn, @"dd MMMM yyyy\.\c\s\v", new Culture("sl-SI")) where fn is the file name. To make this a lambda expression we use fn => DateTime.ParseExact("01 " + fn, @"dd MMMM yyyy\.\c\s\v", new Culture("sl-SI")).

The first approach is now complete, we call _items.OrderBy(fn => DateTime.ParseExact("01 " + fn, @"dd MMMM yyyy\.\c\s\v", new Culture("sl-SI"))) and depend upon OrderBy knowing how to sort DateTime values.

For the second approach we need to take two values and compare them ourselves. We can do this by calling CompareTo() on the DateTimes returned by ParseExact.

Finally, we move the CultureInfo construction out to initialise a variable called sloveneCultureInfo to avoid wasteful calls to the multiple creations of essentially the same object.

Jon Hanna
Added an explanation of why this works.
Jon Hanna
Thanks, this realy helped me to understand this...
Wolfy