Bug in DateTime.ToString("T") and DateTime.ToString("G")?

Just set the formats in .NET as you like. For example:

var clonedProvider = (CultureInfo)CultureInfo.CurrentCulture.Clone();

clonedProvider.DateTimeFormat.LongTimePattern = "HH-mm':'ss";
clonedProvider.DateTimeFormat.ShortDatePattern = "dd'/'MM-yyyy";

Then:

mydate.ToString("T", clonedProvider);
mydate.ToString("G", clonedProvider);

Note that I put the colon : and the slash / into single quotes (apostrophes ') to prevent them from being translated into whatever separator your culture has from the outset. I just want literal colon and slash.

If you don't want to write clonedProvider everywhere, change the culture permanently on your current thread:

Thread.CurrentThread.CurrentCulture = CultureInfo.ReadOnly(clonedProvider);

Do you have many threads in your application?


Edit after comment:

If you want to see how the OS settings has affected your .NET format provider object, just inspect the strings:

DateTimeFormatInfo.CurrentInfo.ShortDatePattern
DateTimeFormatInfo.CurrentInfo.LongTimePattern

and so on. I suppose your current format provider has UseUserOverride set to true, so the user settings from Windows will be visible.

There is no limit to the number of "separators" the user could have typed in. For example someone might use "ddd;dd-MM,yyyy". So there are three separators there. So you will have to examine the string yourself to see how many "separators" and "components" are there, and which characters the user uses as separator in each place.


Reading your question carefully, and relating to your example, I see that you typed HH-mm:ss in the Windows setting. That has got a problem with it. When translated to .NET syntax, the first separator - becomes the time separator. Then the next separator, the colon, in .NET is a "wildcard" meaning "substitute with time separator". So that colon is translated to a dash as well.

You should have typed, in Windows settings,

HH-mm':'ss

where again you protect the colon with single quotes (apostrophes).

Now what if one of your users uses a non-standard separator first, and then later uses the standard separator : (or /) without quoting the latter in single quotes? Well, in that case you are right, there is a difference between the behavior in Windows and that in .NET. Apparently users should not type formats like that. You could call this a bug.


Getting separators

As stated by Jeppe Stig Nielson (maybe upvote for him), there is no good way to get the second time or date separator, because in a format string like

HH-mm/HH:mm-HH/mm

there can be multiple of them, even with the same semantics (e.g. between hours and minutes).

Microsoft bug report

I have registered at Microsoft Connect and filed the bug as DateTime.ToString("T") and DateTime.ToString("G"). If you have a Microsoft Connect account, you can vote whether or not you can reproduce the bug.

SSCCE to reproduce the bug

using System;
using System.Globalization;

namespace DateTimeToString
{
    class Program
    {
        // Tested on
        // Microsoft Windows 7 Enterprise x64 Version 6.1.7601 Service Pack 1 Build 7601
        // I should have all official updates installed at the time of writing (2014-03-11)
        //
        // Microsoft Visual Studio Premium 2012 Version 11.0.61030.00 Update 4
        // Microsoft .NET Framework Version 4.5.50938
        //
        // Type: Console application x86
        // Target framework: .NET 4 Client Profile
        static void Main()
        {
            if (DateTimeFormatInfo.CurrentInfo.LongTimePattern != "HH-mm:ss" ||
                DateTimeFormatInfo.CurrentInfo.ShortDatePattern != "dd.MM/yyyy")
            {
                Console.WriteLine("Configure long time format to MM-mm:ss to reproduce the time bug.");
                Console.WriteLine("Configure short date format to dd.MM/yyyy to reproduce the date bug.");
                Console.WriteLine("Control Panel/Region and Language/Additional settings");
                return;
            }

            var dateTime = DateTime.Now;
            Console.WriteLine("Expected: " + dateTime.ToString("HH'-'mm':'ss"));
            Console.WriteLine("Actual  : " + dateTime.ToString("T"));
            Console.WriteLine();

            Console.WriteLine("Expected: " + dateTime.ToString("dd'.'MM'/'yyyy HH'-'mm':'ss"));
            Console.WriteLine("Actual  : " + dateTime.ToString("G"));
            Console.WriteLine();

            Console.WriteLine("Expected: " + dateTime.ToString("HH'-'mm':'ss"));
            Console.WriteLine("Actual  : " + dateTime.ToLongTimeString());
            Console.WriteLine();

            Console.WriteLine("Expected: " + dateTime.ToString("dd'.'MM'/'yyyy"));
            Console.WriteLine("Actual  : " + dateTime.ToShortDateString());
            Console.ReadLine();
        }
    }
}

Workaround

As a workaround, we can use the native methods GetTimeFormat and GetDateFormat.

static class Program
{
    static void Main()
    {
        var systemTime = new SystemTime(DateTime.Now);

        Console.WriteLine("ShortDatePattern (as reported by .NET): " + DateTimeFormatInfo.CurrentInfo.ShortDatePattern);
        var sbDate = new StringBuilder();
        GetDateFormat(0, 0, ref systemTime, null, sbDate, sbDate.Capacity);
        Console.WriteLine("Date string (as reported by kernel32) : " + sbDate);
        Console.WriteLine();

        Console.WriteLine("LongTimePattern (as reported by .NET) : " + DateTimeFormatInfo.CurrentInfo.LongTimePattern);
        var sbTime = new StringBuilder();
        GetTimeFormat(0, 0, ref systemTime, null, sbTime, sbTime.Capacity);
        Console.WriteLine("Time string (as reported by kernel32) : " + sbTime);

        Console.ReadKey();
    }

    [DllImport("kernel32.dll")]
    private static extern int GetDateFormat(int locale, uint dwFlags, ref SystemTime sysTime,
        string lpFormat, StringBuilder lpDateStr, int cchDate);

    [DllImport("kernel32.dll")]
    private static extern int GetTimeFormat(uint locale, uint dwFlags, ref SystemTime time, 
        string format, StringBuilder sb, int sbSize);


    [StructLayout(LayoutKind.Sequential)]
    private struct SystemTime
    {
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Year;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Month;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort DayOfWeek;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Day;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Hour;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Minute;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Second;
        [MarshalAs(UnmanagedType.U2)] private readonly ushort Milliseconds;

        public SystemTime(DateTime dateTime)
        {
            Year = (ushort) dateTime.Year;
            Month = (ushort) dateTime.Month;
            DayOfWeek = (ushort) dateTime.DayOfWeek;
            Day = (ushort) dateTime.Day;
            Hour = (ushort) dateTime.Hour;
            Minute = (ushort) dateTime.Minute;
            Second = (ushort) dateTime.Second;
            Milliseconds = (ushort) dateTime.Millisecond;
        }
    }
}

Tags:

C#

.Net

Datetime