Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#3.5

Time Zone Lab: An Expedition into Windows Time Zones

5.00/5 (3 votes)
7 Sep 2014CPOL5 min read 15K   1  
Join me for an adventure in time zone conversion.

Introduction

One of the requirements of a project that I just completed was to be able to evaluate the age of a file that had been deposited on a FTP server, based on a time reported in a different time zone than the one used on the local machine. The generally accepted approach to such a problem is to convert the time to UTC, get the UTC time from the local machine, and do the arithmetic.

However, that leaves the issue of reporting the findings to an audience that is not accustomed to thinking in terms of UTC time. I was pleasantly surprised when a quick Google search led directly to ""Converting Times Between Time Zones," at http://msdn.microsoft.com/en-us/library/bb397769(v=vs.90).aspx, and the TimeZoneInfo.ConvertTime method.

Background

Before I use a new routine or class, I often construct a small console program to put it through its paces, test its limits, and look for undocummented or poorly documented booby traps. This is one such project.

Although the intent of the project is to demonstrate the capabilities of TimeZoneInfo.ConvertTime and its close relatives, since it is a working program, built from a custom Visual Studio project template, there are a few extra goodies

Using the code

The included package is a complete kit, including all required satellite assemblies, and both Debug and Release builds that are almost ready to run.

Before you can use the program, you must make one change in TimeZoneLab.exe.config.

XML
<TimeZoneLab.Properties.Settings>
    <setting name="EdgeCaseInputFileName" serializeAs="String">
        <value>C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab\NOTES\TimeZoneConversionEdgeCases.TXT</value>
    </setting>
    <setting name="EdgeCaseReportFileName" serializeAs="String">
        <value>C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab\NOTES\TimeZoneConversionEdgeCases.RPT</value>
    </setting>
</TimeZoneLab.Properties.Settings>

On my machine, two files live in directory C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab\NOTES. Although your extracted archive will have a NOTES folder, you will almost certainly extract the archive somewhere other than C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab. Change the folder names in the EdgeCaseInputFileName and EdgeCaseReportFileName nodes before you try to run the program.

  • EdgeCaseInputFileName is a TAB delimited input file. The first column contains selected times just before and after the 2014 daylight Saving Time transitions. The second column contains a descriptive note about the time next to it; these appear in the report.
  • EdgeCaseReportFileName is a TAB deliimited output file, suitable for quick importing into Microsoft Excel. The first two columns come from the input file, and the rest are computed.

Everything that goes into the report also appears on the console, although the console output is formatted to fit in the available screen width.

Finally, WizardWrx.DLLServices2.dll.config has its Build Action set to Content, and the build engine is instructed to copy it into the output directory if it is newer. Explaining this resource, and why it is segregated in this fashion, is beyond the scope of this article.

Points of Interest

LabelsForTasks.TXT, which appears in the project source directory, and is marked as an Embedded Resource, is read from that resource into an array of strings. The subscript of that array, conicidentally, maps to the integer values of the Task enumeration, defined in Program.cs.

C#
enum Task
{
    All ,
    AnyTimeZoneToAnyOtherTimeZone ,   // ConvertBetweenAnyTwoTimeZones
    EnumTimeZones ,                   // EnumerateTimeZones
    AnyTimeZoneToUTC ,                // ConvertAnyTimeZoneToUTC
    AnyTimeZoneToLocalTime ,          // ConvertAnyTimeZoneToLocalTime
}   // enum Task

The line comment next to each value is the name of the static method of class TimeZoneTasks that implements it. While I could have used used the enumeration values or the method names to label the output, I chose instead to provide the more human focused descriptions in LabelsForTasks.TXT, since I had recently figured out how trivially easy it is to load and uses such lists.

The secret sauce is in static methods, LoadTextFileFromEntryAssembly and LoadTextFileFromAnyAssembly, both defined in Util.cs.

LoadTextFileFromEntryAssembly is a simple wrapper around LoadTextFileFromAnyAssembly; it takes a string that contains the unqualified name of the file, as it appears in the source code directory. You can pass in the name as a hard coded literal, a string constant, or put it into a regular string resource, as I did in this program (See TASK_LABEL_FILENAME.), although I don't recommend it, since loading embedded resources is a lot more expensive than reading the file name from a string constant.

C#
public static string [ ] LoadTextFileFromEntryAssembly (
    string pstrResourceName )
{
    return LoadTextFileFromAnyAssembly (
        pstrResourceName ,
        Assembly.GetEntryAssembly ( ) );
}   // public static string [ ] LoadTextFileFromEntryAssembly

LoadTextFileFromAnyAssembly is a bit more involved. First, it calls GetInternalResourceName to derive the internal name by which the resource is known from the external name provided by you. Once it has the correct internal name, it calls the GetManifestResourceStream method on Assembly pasmSource, which returns the Stream object that you use to read the text into the string array. From this point on, you are dealing with routine binary stream I/O operations., using methods that should be familliar to you.

Since embedded reources tend to be pretty small, I chose to read the file in one gulp, then split it into individual lines after converting the incoming byte array into a Unicode string. Converting the text, now in the form of a long string, into an array of strings, each of which is a line, stripped of its terminator, is accomplished by calling static method WizardWrx.TextBlocks.StringOfLinesToArray. Since we have no further use for the Unicode string, which is created by passing character array achrWholeFile through a string constructor, it is fed directly into the static conversion method.

C#
private static string [ ] LoadTextFileFromAnyAssembly (
    string pstrResourceName ,
    Assembly pasmSource )
{
    string strInternalName = GetInternalResourceName (
        pstrResourceName ,
        pasmSource );

    if ( strInternalName == null )
        throw new Exception (
            string.Format (
                Properties.Resources.ERRMSG_EMBEDDED_RESOURCE_NOT_FOUND ,
                pstrResourceName ,
                pasmSource.FullName ) );

    Stream stroTheFile = pasmSource.GetManifestResourceStream ( strInternalName );

    //  ----------------------------------------------------------------
    //  The character count is used several times, always as an integer.
    //  Cast it once, and keep it, since implicit casts create new local
    //  variables.
    //
    //  The integer is immediately put to use, to allocate a byte array,
    //  which must have room for every character in the input file.
    //  ----------------------------------------------------------------

    int intTotalBytesAsInt = ( int ) stroTheFile.Length;
    byte [ ] abytWholeFile = new Byte [ intTotalBytesAsInt ];
    int intBytesRead = stroTheFile.Read (
        abytWholeFile ,                         // Buffer sufficient to hold it.
        BEGINNING_OF_BUFFER ,                   // Read from the beginning of the file.
        intTotalBytesAsInt );                   // Swallow the file whole.

    //  ----------------------------------------------------------------
    //  Though its backing store is a resource embedded in the assembly,
    //  it must be treated like any other stream. Investigating in the
    //  Visual Studio Debugger showed me that it is implemented as an
    //  UnmanagedMemoryStream. That "unmanaged" prefix is a clarion call
    //  that the stream must be cloaed, disposed, and destoryed.
    //  ----------------------------------------------------------------

    stroTheFile.Close ( );
    stroTheFile.Dispose ( );
    stroTheFile = null;

    //  ----------------------------------------------------------------
    //  In the unlikely event that the byte count is short (or long),
    //  the program must croak. Since the three items that we want to
    //  include in the report are stored in local variables, including
    //  the reported file length, we can go ahead and close the stream
    //  before the count of bytes read is evaluated. HOWEVER, you must
    //  USE them, or you get a null reference exception that masks the
    //  real error.
    //  ----------------------------------------------------------------

    if ( intBytesRead != intTotalBytesAsInt )
        throw new InvalidDataException (
            string.Format (
                Properties.Resources.ERRMSG_EMBEDDED_RESOURCE_READ_ERROR ,
                new object [ ]
                {
                    strInternalName ,
                    intTotalBytesAsInt ,
                    intBytesRead ,
                    Environment.NewLine
                } ) );

    //  ----------------------------------------------------------------
    //  The file is stored in single-byte ASCII characters. The native
    //  character set of the Common Language Runtime is Unicode. A new
    //  array of Unicode characters serves as a translation buffer which
    //  is filled a character at a time from the byte array.
    //  ----------------------------------------------------------------

    char [ ] achrWholeFile = new char [ intTotalBytesAsInt ];

    for ( int intCurrentByte = BEGINNING_OF_BUFFER ;
              intCurrentByte < intTotalBytesAsInt ;
              intCurrentByte++ )
        achrWholeFile [ intCurrentByte ] = ( char ) abytWholeFile [ intCurrentByte ];

    //  ----------------------------------------------------------------
    //  The character array converts to a Unicode string in one fell
    //  swoop. Since the new string vanishes when StringOfLinesToArray
    //  returns, the constructor call is nested in StringOfLinesToArray,
    //  which splits the lines of text, with their DOS line termiators,
    //  into the required array of strings.
    //
    //  Ideally, the blank line should be removed. However, since the
    //  RemoveEmptyEntries member of the StringSplitOptions enumeration
    //  does it for me, I may as well use it, and save myself the future
    //  agrravation, when I will have probably why it happens.
    //  ----------------------------------------------------------------

    return WizardWrx.TextBlocks.StringOfLinesToArray (
        new string ( achrWholeFile ) ,
        StringSplitOptions.RemoveEmptyEntries );
}   // private static string [ ] LoadTextFileFromAnyAssembly

Static method WizardWrx.TextBlocks.StringOfLinesToArray is defined in WizardWrx.SharedUtl2.dll, which is included in the debug and release build directories.

I saved the most important ingredient, GetInternalResourceName, for last. It is really short, but sweet, taking advantage of the way Visual Studio names embedded resources of this kind. Everything depends on having a reference to the assembly into which the resource is embedded. 

C#
private static string GetInternalResourceName (
    string pstrResourceName ,
    Assembly pasmSource )
{
    foreach ( string strManifestResourceName in pasmSource.GetManifestResourceNames ( ) )
        if ( strManifestResourceName.EndsWith ( pstrResourceName ) )
            return strManifestResourceName;

    return null;
}   // private static string GetInternalResourceName

If the resource is embedded in the assembly that starts the process, LoadTextFileFromEntryAssembly gets the assembly for you, so the only thing you need to know is the name that you gave to the file when you copied it into the source code tree. Marking it for embedding is easy; view the Properites of the file in the Solution Explorer, and change the Build Action from None (the default) to Embedded Resource. Visual Studio takes it from there.

History

2014/09/07- First publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)