views:

196

answers:

3

Does anyone know how to localize date ranges using C#?

In particular, I want to generate "smart" date ranges, so that redundant information is eliminated.

Here are some examples in US English

  1. August - Sept, 2009
  2. August 2009
  3. August 1 - 9, 2009
  4. January 1 - March 3, 2009
  5. December 6, 2009 - January 8, 2010

Form what I can tell the .NET Framework has support for localizing dates, but not ranges of dates.

Using the information in System.Globalization.DateTimeFormatInfo for the Windows locales that CultureInfo supports, I was able to figure out (most of the time) how to do items #1 and #2. Item #2 is just DateTime.ToString(DateTimeFormatInfo.YearMonthFormat). Using YearMonthFormat I was also able to deduce formats to use for #1 for most languages. For the few that I couldn't I just duplicate the year.

Unfortunately, I can't figure out how to do items #3-#5 using the .NET Framework. Outlook formats ranges using those formats, so I was hoping there might be some Win32 APIs that would do it, but a Google search for "Win32 Date Range localization" yielded nothing useful.

I like the enhanced usability provided by "smart range formating", and I would like my customers that are not using English versions of Windows to get the same benefits.

Does any one know how to do that in a culture-dependent manner?

+1  A: 

Good question and it does seem to be something that the .NET framework and other languages seem to be missing too. I would guess the presentation of the results depends upon your application.

Outlook has very good date understanding as an input (as does Google calenders), but I haven't personally seen this form of expression as an output (e.g. displayed to the user).

Some recommendations:

  1. Stick with displaying a start date and an end date and don't worry about redundancy
  2. Roll your own..... :-(

I would guess that from your term 'localize' you plan to internationalize the output, if that is the case you are going to have a hard job catching all of the cases, it doesn't seem to be information stored in most of the typical internationalization data sets.

Concentrating on English from your example, you seem to have a number of rules already defined in the code:

  1. Year always displayed.
  2. Month (at least one of them) is also always displayed.
  3. Days are not displayed in case 3, this I would condition to mean that the start date is the 1st and the end is the 31st (e.g. every day of the month)
  4. Day and Month are shown if the start/end are different months.
  5. Year is additionally shown against the start date if the two dates differ by year.

I know the above is only looking at English, but in my view, it doesn't look too hard to roll yourself, too much to write in this editor though!

EDIT - I know this doesn't answer the original - but if anyone using a search engine finds this question and wants an English version...

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(DateRange.Generate(2009, 08, 01, 2009, 09, 31));
        Console.WriteLine(DateRange.Generate(2009, 08, 01, 2009, 08, 31));
        Console.WriteLine(DateRange.Generate(2009, 08, 01, 2009, 08, 09));
        Console.WriteLine(DateRange.Generate(2009, 01, 01, 2009, 03, 03));
        Console.WriteLine(DateRange.Generate(2009, 12, 06, 2010, 01, 08));

        // Same dates
        Console.WriteLine(DateRange.Generate(2009, 08, 01, 2009, 08, 01));
    }
}

static class DateRange
{
    private static string[] Months = {
                                         "January", "February", "March", "April",
                                         "May", "June", "July", "August",
                                         "September", "October", "November", "December"
                                     };
    public static string Generate(
        int startYear, int startMonth, int startDay,
        int endYear, int endMonth, int endDay)
    {
        bool yearsSame = startYear == endYear;
        bool monthsSame = startMonth == endMonth;
        bool wholeMonths = (startDay == 1 && IsLastDay(endDay, endMonth));

        if ( monthsSame && yearsSame && startDay == endDay)
        {
            return string.Format("{0} {1}, {2}", startDay, Month(startMonth), startYear);
        }

        if (monthsSame)
        {
            if (yearsSame)
            {
                return wholeMonths
                           ? string.Format("{0} {1}", Month(startMonth), endYear)
                           : string.Format("{0} {1} - {2}, {3}", Month(endMonth), startDay, endDay, endYear);
            }
            return wholeMonths
                       ? string.Format("{0}, {1} - {2}, {3}",
                                       Month(startMonth), startYear,
                                       Month(endMonth), endYear)
                       : string.Format("{0} {1}, {2} - {3} {4}, {5}",
                                       Month(startMonth), startDay, startYear,
                                       Month(endMonth), endDay, endYear);
        }

        if (yearsSame)
        {
            return wholeMonths
                       ? string.Format("{0} - {1}, {2}", Month(startMonth), Month(endMonth), endYear)
                       : string.Format("{0} {1} - {2} {3}, {4}",
                                       Month(startMonth), startDay,
                                       Month(endMonth), endDay,
                                       endYear);
        }
        return wholeMonths
                   ? string.Format("{0}, {1} - {2}, {3}",
                                   Month(startMonth), startYear,
                                   Month(endMonth), endYear)
                   : string.Format("{0} {1}, {2} - {3} {4}, {5}",
                                   Month(startMonth), startDay, startYear,
                                   Month(endMonth), endDay, endYear);
    }

    private static string Month(int month)
    {
        return Months[month - 1];
    }

    public static bool IsLastDay(int day, int month)
    {
        switch (month+1)
        {
            case 2:
                // Not leap-year aware
                return (day == 28 || day == 29);
            case 1: case 3: case 5: case 7: case 8: case 10: case 12:
                return (day == 31);
            case 4: case 6: case 9: case 11:
                return (day == 30);
            default:
                return false;
        }
    }
}

This yields the same output (almost, Sept becomes September in mine) as the original question:

August - September, 2009
August 1 - 31, 2009
August 1 - 9, 2009
January 1 - March 3, 2009
December 6, 2009 - January 8, 2010
Ray Hayes
I initially rolled my own for english. The code to do that is very simple. The problem with that is that the same date string can mean different things in different countries (1/2/2009 means something different in Sweden then it does in the US).So I need to display dates in the local date format, even if the UI text is not translated. I could probably roll my own for Spanish, in addition to English, but that's about it. I don't Japanese, Chinese, Russian, French, the various Indian languages, or any of the other languages Windows Supports.
Scott Wisniewski
Internationalisation is not something you'll manage to do on your own. My wife speaks several languages, but can officially translate into any of them (only out back to English) because the translations need to be approved by a native speaker/thinker. I knocked together a short trial myself but initially got different English results to you... but I'm from the UK and the US uses, in my view, the wrong date format... Should be "4 August" not "August 4", think about "Fourth of July", "July fourth" sounds wrong and abrupt!
Ray Hayes
Thinking about your example date 1/2/2009 - shouldn't be a problem assuming you use a DateTime, as long as you knew the local when you parsed the date string, you should be able to access thatDate.Month and thatDate.Day without concern about its original structure. You should also be able to get the month names in a local context too. So all you are left with is a need to internationalise the string.Format patterns (e.g. if some languages change their order). Obviously you would need to build a library of formats, left as an exercise....
Ray Hayes
Deducing those custom formats without knowledge of the culture being displayed is hard.Take the following date:2009年7月20日It happens that 年 denotes the year, 月 the month, and 日 the day. Using that knowledge, it's possible to deduce range formats for Chinese. That only possible because I was able to figure out that it's ok to use those chars in isolation (2009年7月 means july 2009). Without knowing a language, I can only figure this out if the YearMonthPattern has a close resemblance to the ShortDatePattern.There are several cultures where this is not the case.
Scott Wisniewski
I'm not saying you need to deduce the input date's culture without knowledge, your original question didn't have that complexity defined. If the date is already in a DateTime structure, culture is irrelevant for the input. For the output, you need a) knowledge of the culture, b) a set of String.Format templates to replace the hardcoded US-en ones (standard i18n problem for strings) and you need a replacement for my simple Get-Month method, ideally delegating that to the culture aware API.
Ray Hayes
You are going to have a hard job trying to reverse engineer the string formats from interrogating DateTime's string format logic, you really need a translation table to do that...
Ray Hayes
Hence why I posted the question.
Scott Wisniewski
A: 

The below code handles 4 and 5 if I get the idea of your code correctly

public string GetDateRangeString(DateTime dt1, DateTime dt2)
{
   DateTimeFormatInfo info = new DateTimeFormatInfo();

   string format1;
   string format2;
   format2 = info.YearMonthPattern;
   format1 = dt1.Year == dt2.Year ? format1 = info.MonthDayPattern :
                                               format2;
   return string.Format("{0} - {1}", dt1.ToString(format1), dt2.ToString(format2));
}
Rune FS
it would be nice if the down voter would leave a note of the reason :)
Rune FS
Scott Wisniewski
lol you're absolutely right I must have misread the last two lines (I actually checked my notes to see if the dates had been their yesterday which they of course had)
Rune FS
+2  A: 

I ended up defining my own range formats for this.

For the most part I derived them from the LongDatePattern. Where possible I verified the formats against Google Calendar, and / or native speakers.

I also generated 2 formats for time ranges (within the same day), one for within the same 12 hour period and another for times outside the same 12 hour period (for cultures that use 24 hour time these 2 formats are the same). Those are mostly based on the FullDateTimePattern.

In all cases I removed "day names" (monday, tuesday, etc) from all the formats where ever possible.

I would love to post them here, but it seems that the Stack Overflow folks have an irrational fear of html tables, because they don't allow table tags. The data is essentially a giant table. I have no interest in trying to simulate tables using CSS, so I just put the table up on my own site. You can access it here if you are interested:

http://www.transactor.com/misc/ranges.html

I do have some detailed notes on how I derived a lot of the formats, including what's been validated and what hasn't, if you are really interested. If you want those notes, just send an email to:

[email protected]

and I'd be happy to send them to you.

Scott Wisniewski
404 Not found!?
dtb
Any chance you could re-post the link, Scott? Or put it in a Google Spreadsheet?
frankadelic