2.5 时间和日期
一般操作系统是通过数字来处理时间的,这个数字在有的系统上是整数,在有的系统上是浮点数。除此之外,数字0代表的时间也有不同定义,再加上时区的差异,加大了时间处理的复杂度。在编程中,我们可能会遇到的时间如表2-7所示。
表2-7 编程常见的时间格式
代码清单2-26演示了在.NET时间与各种时间格式转换的方法。其中Windows系统为.NET的DateTime类型提供了原生函数转换,但与UNIX时间戳的转换需要做一些额外处理,第8行的To/FromUnixTimestamp两个函数演示了两者互换的方法。
代码清单2-26 .NET时间与各种时间格式的转换方法
// 源码位置:第2章\DateTimeDemo.cs // 编译命令:csc DateTimeDemo.cs 01 static void Main() 02 { 03 var date = new DateTime(2019, 1, 20, 17, 18, 20); 04 Console.WriteLine($"ticks: {date.Ticks}, oadate: {date.ToOADate()}," + 05 $" unix: {ToUnixTimestamp(date)}, file: {date.ToFileTime()}"); 06 } 07 08 static double ToUnixTimestamp(DateTime value) 09 { 10 value = value.ToUniversalTime(); 11 return value.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; 12 } 13 14 public static DateTime FromUnixTimestamp(double value) 15 { 16 var date = new DateTime(1970, 1, 1); 17 var utc = date.AddSeconds(value); 18 return utc.ToLocalTime(); 19 }
有兴趣的读者可以在Excel里查看OLE自动化时间转换,如图2-20所示。读者可以访问网址https://www.epochconverter.com/核对UNIX时间戳。
图2-20 在Excel里查看OLE自动化时间转换
由于时区的存在,同一时刻各地的时间是不一样的。除此之外,很多国家有夏令时(Daylight Saving)。设计夏令时的目的是尽可能地多利用白天的时间。具体操作是在春夏的某一天将时间往前拨1小时,而在秋天的某一个时刻又把时间拨回来,这样原来冬天的9点上班时间到了夏天的时候,虽然电子表系统都显示9点,但实际上是8点。对于夏令时的调整,有的国家是在固定的某一天调整,而更多的国家是规定某月的第几周的星期几开始调整,如规定三月的第三个周日开始调整。TimeZoneInfo类型就是用来处理时间调整细节的。为了消除时区和夏令时的差异给跨区域使用时间带来的混乱,人们定义了UTC(Universal Coordinated Time,世界协调时间)。在跨时区传输时间时,建议将时间转换成UTC时间传输,在另外一台机器上接收到之后再转换成本地时间。
TimeZoneInfo类型的静态方法GetSystemTimeZones可以获取.NET支持的所有时区信息,如代码清单2-27第2~3行的循环。使用时区名称初始化TimeZoneInfo实例,如第6行和第7行分别初始化北京时间和太平洋时间,后者是微软总部西雅图所在的时区。第9~10行将本地时间转换成UTC时间,再分别用北京时间和太平洋时间解析。可以看到,太平洋时间(2019/1/24 8:00:00)比北京时间(2019/1/25 0:00:00)晚16个小时。而第16行的时间是夏天的时间,转换后可以看到太平洋时间是2019/6/24 9:00:00,这是因为美国采用夏令时制度,所以时间有1个小时的调整。这里也可以看到各个国家对时间的处理是不一样的。中国一般只用一个时区——北京时间,美国习惯上分时区,这意味着在设计手机移动应用时,当用户坐飞机从北京到新疆伊犁,移动应用不需要调整时间,但用户从美国西雅图飞到纽约的话,移动应用就应该根据用户位置动态调整时间了。
代码清单2-27 时区TimeZoneInfo使用示例
// 源码位置:第2章\DateTimeDemo.cs // 编译命令:csc DateTimeDemo.cs 01 // 获取所有的时区信息 02 foreach (var z in TimeZoneInfo.GetSystemTimeZones()) 03 Console.WriteLine($"{z.Id}: {z.DisplayName}"); 04 05 // 北京时间 06 TimeZoneInfo bjtz = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); 07 // 微软总部西雅图时间 08 TimeZoneInfo mstz=TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); 09 // var date = DateTime.UtcNow; 10 date = new DateTime(2019, 1, 25).ToUniversalTime(); 11 var bjtime = TimeZoneInfo.ConvertTimeFromUtc(date, bjtz); 12 Console.WriteLine($"北京时间:{bjtime}"); 13 var mstime = TimeZoneInfo.ConvertTimeFromUtc(date, mstz); 14 Console.WriteLine($"微软时间:{mstime}"); 15 16 date = new DateTime(2019, 6, 25).ToUniversalTime(); 17 bjtime = TimeZoneInfo.ConvertTimeFromUtc(date, bjtz); 18 Console.WriteLine($"北京时间:{bjtime}"); 19 mstime = TimeZoneInfo.ConvertTimeFromUtc(date, mstz); 20 Console.WriteLine($"微软时间:{mstime}");
除了时区差异,世界各地(主要是亚洲)使用的日历也是有差异的,如中国分农历和公历,公历也就是世界上常用的格力高历。在.NET中,GregorianCalendar类是公历,而农历是ChineseLunisolarCalendar。代码清单2-28演示了农历的用法,如2019年春节是2月5日,公历的月份是2月,但是农历获取的月份则是1月。第19~21行演示了根据时间获取干支纪年的方法。
代码清单2-28 日历的用法
01 // 天干 02 enum CelestialStem 03 { 04 甲 = 1, 乙, 丙, 丁, 戊, 05 己, 庚, 辛, 壬, 癸 06 } 07 08 // 地支 09 enum TerrestrialBranch 10 { 11 子 = 1, 丑, 寅, 卯, 辰, 巳, 12 午, 未, 申, 酉, 戌, 亥 13 } 14 15 date = new DateTime(2019, 2, 5); 16 var 公历 = new GregorianCalendar(); 17 var 农历 = new ChineseLunisolarCalendar(); 18 19 var 干支 = 农历.GetSexagenaryYear(date); 20 var 天干 = (CelestialStem)农历.GetCelestialStem(干支); 21 var 地支 = (TerrestrialBranch)农历.GetTerrestrialBranch(干支); 22 Console.WriteLine( 23 $"公历:{公历.GetMonth(date)},农历:{农历.GetMonth(date)}" + 24 $",干支:{天干}{地支}");