时区实验:探索 Windows 时区





5.00/5 (3投票s)
加入我,一起踏上时区转换的冒险之旅。
引言
我最近完成的一个项目的需求之一是能够根据文件在本地计算机不同于它所报告时区的本地机器上的时间,来评估上传到 FTP 服务器上的文件的年龄。解决此类问题的普遍方法是将时间转换为 UTC,获取本地计算机的 UTC 时间,然后进行算术运算。
然而,这留下了一个问题:如何将计算结果报告给不习惯以 UTC 时间思考的受众。当我快速搜索 Google 时,直接找到了“在时区之间转换时间”一文,网址是 http://msdn.microsoft.com/en-us/library/bb397769(v=vs.90).aspx,以及 TimeZoneInfo.ConvertTime
方法,这让我感到惊喜。
背景
在我使用新的例程或类之前,我经常会构建一个小型控制台程序来对其进行测试,测试其极限,并寻找未记录或记录不善的陷阱。这就是这样一个项目。
虽然该项目的目的是演示 TimeZoneInfo.ConvertTime
及其近亲的功能,但由于它是一个可运行的程序,并且是从自定义 Visual Studio 项目模板构建的,因此其中还包含一些额外的亮点。
使用代码
包含的包是一个完整的工具包,包括所有必需的卫星程序集,以及“调试”和“发布”版本,它们“几乎”可以直接运行。
在使用该程序之前,您必须在 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>
在我的机器上,有两个文件位于目录 C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab\NOTES
。虽然您解压缩的存档将包含一个 NOTES 文件夹,但您几乎肯定会将存档解压缩到 C:\Documents and Settings\DAG\My Documents\Visual Studio 2010\Projects\_Laboratory\TimeZoneLab
以外的其他位置。在尝试运行程序之前,请更改 EdgeCaseInputFileName
和 EdgeCaseReportFileName
节点中的文件夹名称。
EdgeCaseInputFileName
是一个 TAB 分隔的输入文件。第一列包含 2014 年夏令时转换前后的一些选定时间。第二列包含有关旁边时间的描述性注释;这些注释会显示在报告中。EdgeCaseReportFileName
是一个 TAB 分隔的输出文件,适合快速导入 Microsoft Excel。前两列来自输入文件,其余列是计算得出的。
所有显示在报告中的内容也会显示在控制台上,尽管控制台输出经过格式化以适应可用屏幕宽度。
最后,WizardWrx.DLLServices2.dll.config
的“生成操作”设置为 Content
,并且生成引擎被指示在新时复制它。解释这个资源,以及为什么它以这种方式被隔离,超出了本文的范围。
关注点
LabelsForTasks.TXT
出现在项目源目录中,并被标记为“嵌入资源”,然后从中读取到一个字符串数组。该数组的下标巧合地对应于 Program.cs
中定义的 Task
枚举的整数值。
enum Task
{
All ,
AnyTimeZoneToAnyOtherTimeZone , // ConvertBetweenAnyTwoTimeZones
EnumTimeZones , // EnumerateTimeZones
AnyTimeZoneToUTC , // ConvertAnyTimeZoneToUTC
AnyTimeZoneToLocalTime , // ConvertAnyTimeZoneToLocalTime
} // enum Task
每个值旁边的行注释是 TimeZoneTasks
类中实现它的静态方法的名称。虽然我可以使用枚举值或方法名称来标记输出,但我选择在 LabelsForTasks.TXT
中提供更以人为本的描述,因为我最近才发现加载和使用此类列表是多么容易。
秘密武器在于 Util.cs
中定义的静态方法 LoadTextFileFromEntryAssembly
和 LoadTextFileFromAnyAssembly
。
LoadTextFileFromEntryAssembly
是 LoadTextFileFromAnyAssembly
的简单包装器;它接受一个包含文件非限定名称的字符串,该名称在源代码目录中出现。您可以将名称作为硬编码的字面量、字符串常量传入,或者将其放入常规字符串资源中,就像我在此程序中所做的那样(参见 TASK_LABEL_FILENAME
。),尽管我不推荐这样做,因为加载嵌入式资源比从字符串常量读取文件名成本要高得多。
public static string [ ] LoadTextFileFromEntryAssembly (
string pstrResourceName )
{
return LoadTextFileFromAnyAssembly (
pstrResourceName ,
Assembly.GetEntryAssembly ( ) );
} // public static string [ ] LoadTextFileFromEntryAssembly
LoadTextFileFromAnyAssembly
涉及的内容稍多一些。首先,它调用 GetInternalResourceName
来根据您提供的外部名称派生资源所知的内部名称。一旦有了正确的内部名称,它就会在 pasmSource
程序集的 GetManifestResourceStream
方法上调用,该方法返回用于将文本读入字符串数组的 Stream
对象。从这一点开始,您将处理常规的二进制流 I/O 操作,使用您应该熟悉的各种方法。
由于嵌入式资源通常很小,我选择一次性读取文件,然后将其转换为 Unicode 字符串后按行分割。将文本(现在是长字符串形式)转换为字符串数组,每个字符串是一行,并剥离了其终止符,是通过调用静态方法 WizardWrx.TextBlocks.StringOfLinesToArray
来实现的。由于我们不再需要 Unicode 字符串(该字符串是通过将字符数组 achrWholeFile
传递给字符串构造函数创建的),因此它直接传递给静态转换方法。
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
静态方法 WizardWrx.TextBlocks.StringOfLinesToArray 定义在 WizardWrx.SharedUtl2.dll
中,该 DLL 已包含在调试和发布版本目录中。
我将最重要的成分 GetInternalResourceName
留到最后。它非常简洁,利用了 Visual Studio 为此类嵌入式资源命名的方式。一切都取决于拥有一个对嵌入资源的程序集的引用。
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
如果资源嵌入在启动进程的程序集中,则 LoadTextFileFromEntryAssembly
会为您获取程序集,因此您只需要知道将文件复制到源文件树时为其指定的名称即可。将其标记为嵌入很容易;在解决方案资源管理器中查看文件的属性,然后将“生成操作”从“无”(默认值)更改为“嵌入资源”。Visual Studio 会处理其余的工作。
历史
2014/09/07 - 首次发布