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
.
<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
.
enum Task
{
All ,
AnyTimeZoneToAnyOtherTimeZone ,
EnumTimeZones ,
AnyTimeZoneToUTC ,
AnyTimeZoneToLocalTime ,
}
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.
public static string [ ] LoadTextFileFromEntryAssembly (
string pstrResourceName )
{
return LoadTextFileFromAnyAssembly (
pstrResourceName ,
Assembly.GetEntryAssembly ( ) );
}
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.
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 );
int intTotalBytesAsInt = ( int ) stroTheFile.Length;
byte [ ] abytWholeFile = new Byte [ intTotalBytesAsInt ];
int intBytesRead = stroTheFile.Read (
abytWholeFile ,
BEGINNING_OF_BUFFER ,
intTotalBytesAsInt );
stroTheFile.Close ( );
stroTheFile.Dispose ( );
stroTheFile = null;
if ( intBytesRead != intTotalBytesAsInt )
throw new InvalidDataException (
string.Format (
Properties.Resources.ERRMSG_EMBEDDED_RESOURCE_READ_ERROR ,
new object [ ]
{
strInternalName ,
intTotalBytesAsInt ,
intBytesRead ,
Environment.NewLine
} ) );
char [ ] achrWholeFile = new char [ intTotalBytesAsInt ];
for ( int intCurrentByte = BEGINNING_OF_BUFFER ;
intCurrentByte < intTotalBytesAsInt ;
intCurrentByte++ )
achrWholeFile [ intCurrentByte ] = ( char ) abytWholeFile [ intCurrentByte ];
return WizardWrx.TextBlocks.StringOfLinesToArray (
new string ( achrWholeFile ) ,
StringSplitOptions.RemoveEmptyEntries );
}
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.
private static string GetInternalResourceName (
string pstrResourceName ,
Assembly pasmSource )
{
foreach ( string strManifestResourceName in pasmSource.GetManifestResourceNames ( ) )
if ( strManifestResourceName.EndsWith ( pstrResourceName ) )
return strManifestResourceName;
return null;
}
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