C#开发训练营

第9课:日期和时间

日期和时间是较复杂的复合型数据,在不同的环境中会有不同的处理方案。在.NET Framework平台中也定义了很多资源,接下来,我们从基本的DateTime和TimeSpan结构开始讨论。

DateTime和TimeSpan结构

在.NET Framework类库中,常用的日期和时间处理资源包括DateTime结构、TimeSpan结构等类型;其中,DateTime结构处理具体的日期和时间数据,而TimeSpan结构处理日期和时间的跨度。

使用DateTime构建日期和时间数据时,可以使用多个版本的构造函数,常用的包括:

  • DateTime(),获取的时间点为公元1年1月1日0时。
  • DateTime(int,int,int),使用年、月、日数据,时间为0点。
  • DateTime(int,int,int,int,int,int),使用年、月、日、时、分、秒数据。
  • DateTime(int,int,int,int,int,int,int),使用年、月、日、时、分、秒和毫秒数据。
  • DateTime(int,int,int,Calendar),使用年、月、日,并指定日历类型。

下面的代码,我们使用中国的农历日期构建DateTime数据,执行代码会显示2021年中秋节的阳历日期。

C#
using System;
using System.Globalization;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DateTime dt = new DateTime(2021, 8, 15, 
            new ChineseLunisolarCalendar());
        tWeb.WriteLine(dt.ToLongDateString());
    }
}

代码中指定的2021年8月15日,但指定的日历为中国的农历,即八月十五中秋节,本例执行会显示“2021年9月21日”。

获取系统当前时间时可以使用DateTime.Now属性;只需要日期数据时可以使用DateTime.Toay属性,其中的时间为0点。

对日期和时间进行推算时,可以使用DateTime中的一系列AddXXX()方法,如下面的代码,就是获取指定日期前10天和后10天的日期。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DateTime dt1 = new DateTime(2021, 8, 15);
        DateTime dt2 = dt1.AddDays(-10);
        DateTime dt3 = dt1.AddDays(10);
        tWeb.WriteLine(dt2.ToString());
        tWeb.WriteLine(dt3.ToString());
    }
}

代码执行结果如图1。

图1

常用的日期和时间推算方法包括:

  • AddYears()方法,按年计算,参数为int类型。
  • AddMonths()方法,按月计算,参数为int类型。
  • AddDays()方法,按天计算,参数为double类型。
  • AddHours()方法,按小时计算,参数为double类型。
  • AddMinutes()方法,按分钟计算,参数为double类型。
  • AddSeconds()方法,按秒计算,参数为double类型。
  • AddMilliseconds()方法,按毫秒计算,参数为double类型。
  • AddTicks()方法,按Tick值计算,参数为long类型。这里,Tick是DateTime处理时间的最小单位,1Tick等于100纳秒,万分之一毫秒。

计算两个DateTime值的间隔时可以直接使用减法运算,运算结果为TimeSpan类型,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DateTime dt1 = new DateTime(2021, 8, 15);
        DateTime dt2 = new DateTime(2021, 8, 5, 12, 0, 0);
        TimeSpan ts = dt1 - dt2;
        tWeb.WriteLine(ts.Days);
        tWeb.WriteLine(ts.TotalDays);
    }
}

代码执行结果如图2。

图2

可以看到,使用TimeSpan的Days属性时得到是整天数,而TotalDays属性获取的数据则是浮点数,包含了一天中的时间部分。

从TimeSpan结构中读取日期和时间数据的属性包括:

  • Days属性,获取整天数据,返回类型为int。
  • TotalDays属性,获取天数,返回类型为double。
  • Hours属性,获取整小时数,返回类型为int。
  • TotalHours属性,获取小时数,返回类型为double。
  • Minutes属性,获取整分钟数,返回类型为int。
  • TotalMinutes属性,获取分钟数,返回类型为double。
  • Seconds属性,获取整秒数,返回类型为int。
  • TotalSeconds属性,获取秒数,返回类型为double。
  • Milliseconds属性,获取整毫秒数,返回类型为int。
  • TotalMilliseconds属性,获取毫秒数,返回类型为double。
  • Ticks属性,获取Tick值,返回类型为long。

在DateTime数据中进行推算时也可以使用TimeSpan数据,此时,可以先使用TimeSpan结构的构造函数创建数据,如:

  • TimeSpan(long ticks),使用Tick值构建TimeSpan数据。
  • TimeSpan(int hours,int minutes,int seconds),使用小时、分种、秒构建TimeSpan数据。
  • TimeSpan(int days,int hours,int minutes,int seconds),使用天数、小时、分钟和秒数构建TimeSpan数据。
  • TimeSpan(int days,int hours,int minutes,int seconds,int milliseconds),使用天数、小时、分钟、秒和毫秒构建TimeSpan数据。

创建TimeSpan数据后,可以使用DateTime结构中的Add()方法进行日期和时间的推算,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DateTime dt1 = new DateTime(2021, 8, 15);
        TimeSpan ts = new TimeSpan(10, 12, 15, 30);
        DateTime dt2 = dt1.Add(ts);
        tWeb.WriteLine(dt2.ToString());
    }
}

代码中,dt2会在dt1的数据上添加10天12小时15分30秒,执行结果如图3。

图3

将DateTime数据转换为字符串形式时,可以使用如下方法:

  • ToLongDateString()方法,返回长格式日期字符串。
  • ToShortDateString()方法,返回短格式日期字符串。
  • ToLongTimeString()方法,返回长格式时间字符串。
  • ToShortTimeString()方法,返回短格式时间字符串。

图4中显示了中文环境下的显示格式。

图4

无参数的ToString()方法可以返回日期和时间的标准文本形式,也可以通过设置格式化参数显示指定格式的文本形式。

此外,对于日期和时间的文本格式,也可以根据需要进行自定义,在tDate类(/common/base/tDate.cs)中定义了一些方法来完成日期和时间格式化字符串的生成,如下面的代码。

C#
using System;

public static class tDate
{
    //  其它代码…
    // 日期和时间转文本格式
    public static string DateTimeStr(this DateTime dt)
    {
        return string.Format("{0:d4}-{1:d2}-{2:d2} {3:d2}:{4:d2}:{5:d2}",
            dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
    }
    //
    public static string DateStr(this DateTime dt)
    {
        return string.Format("{0:d4}-{1:d2}-{2:d2}",
            dt.Year, dt.Month, dt.Day);
    }
    //
    public static string TimeStr(this DateTime dt)
    {
        return string.Format("{0:d2}:{1:d2}:{2:d2}",
            dt.Hour, dt.Minute, dt.Second);
    }
    //
    public static string HourMinuteStr(this DateTime dt)
    {
        return string.Format("{0:d2}:{1:d2}", dt.Hour, dt.Minute);
    }
    //
}

代码中,tDate类定义了几个扩展方法,转换的日期和时间文本形式中不包含连接符号,其中,年份使用4位字符,月份、天、时、分、秒使用2位字符等,实际应用中可以根据需要定义相应的格式。

农历

在讨论DateTime构造函数时已经了解了如何通过农历日期构建DateTime结构数据,我们再回顾一下,如下面的代码显示了2022年春节的阳历日期。

C#
using System;
using System.Globalization;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DateTime dt = new DateTime(2022, 1, 1,
            new ChineseLunisolarCalendar());
        tWeb.WriteLine(dt.ToLongDateString());
    }
}

代码会显示“2022年2月1日”。

如果已有阳历日期,如何获取农历信息时,本节将通过封装tLunar类进行相关操作。下面的代码(/app_code/common/tool/tLunar.cs)就是tLunar类的基本定义。

C#
using System;
using System.Globalization;

public class tLunar
{
    // 构造函数
    public tLunar(DateTime dt)
    {
        Init(dt);
    }
    //
    public tLunar()
    {
        Init(DateTime.Now);
    }
    //
    public tLunar(int year, int month, int day)
    {
        Init(new DateTime(year, month, day));
    }
    // 工厂方法
    public static tLunar Get(DateTime dt)
    {
        return new tLunar(dt);
    }
    //
    public static tLunar Get()
    {
        return new tLunar(DateTime.Now);
    }
    //
    public static tLunar Get(int year, int month, int day)
    {
        return new tLunar(new DateTime(year, month, day));
    }
    //
    public int Year { get; private set; }  // 甲子,1-60
    public int TianGan { get; private set; } // 天干,1-10
    public int DiZhi { get; private set; } // 地支,1-12
    public int Month { get; private set; } // 农历月份
    public int Day { get; private set; } // 农历日
    public int LeapMonth { get; private set; } // 闰月
    //
    public DateTime Date { get; private set; } // 阳历日期
    // 初始化农历信息
    protected void Init(DateTime dt)
    {
        ChineseLunisolarCalendar cale = 
            new ChineseLunisolarCalendar();
        Year = cale.GetSexagenaryYear(dt);
        TianGan = cale.GetCelestialStem(Year);
        DiZhi = cale.GetTerrestrialBranch(Year);
        Month = cale.GetMonth(dt);
        Day = cale.GetDayOfMonth(dt);
        // 判断闰月时注意农历冬月和腊月在阳历第二年的情况
        if (dt.Month < 3 && Month > 10)
            LeapMonth = cale.GetLeapMonth(dt.Year - 1);
        else
            LeapMonth = cale.GetLeapMonth(dt.Year);
        //
        Date = dt;
    }
    // 其它代码…
}

tLunar类中,首先定义了几个构造函数和工厂方法,用于创建tLunar对象,版本包括:

  • tLunar(),使用系统当前日期进行初始化。
  • tLunar(DateTime dt),使用指定的日期进行初始化。
  • tLunar(int year, int month, int day),使用指定的阳历年、月、日进行初始化。

此外,GetByLunar()静态方法中可以返回由农历年、月、日指定的tLunar对象,如下面的代码。

C#
public static tLunar GetByLunar(int year, int month, int day)
{
    return new tLunar(new DateTime(year, month, day,
        new ChineseLunisolarCalendar()));
}

在tLunar类中,还定义了一系列的只读属性,分别是:

  • Year,甲子年份,范围在1到60之间。
  • TianGan属性,天干,范围在1到10之间。
  • DiZhi属性,地支,范围在1到12之间。十二地支还分别对应了一天中的十二个时辰,以及十二生肖。
  • Month属性,农历月份。由于农历会出现闰月,所月份的范围在1到13之间。
  • Day属性,农历日期,范围在1到30之间。
  • LeapMonth属性,表示第几个月为闰月,如LeapMonth为8表示第8个月为闰月,即闰七月。
  • Date属性,表示阳历日期数据,类型为DateTime。

对tLunar对象进行初始化时使用了Init()方法,其中主要使用的资源是ChineseLunisolarCalendar对象中的一些方法,包括:

  • GetSexagenaryYear()方法,通DateTime数据返回甲子数据,返回值在1到60之间,表示年份是一个甲子中的第几年。
  • GetCelestialStem()方法,根据甲子年份返回天干数据,返回值在1到10之间。
  • GetTerrestrialBranch()方法,根据甲子年份返回地支数据,返回值在1到12之间。
  • GetMonth()方法,根据DateTime数据返回农历月份,返回值在1到13之间。
  • GetDayOfMonth()方法,根据DateTime数据返回农历日期,返回值在1到30之间。
  • GetLeapMonth()方法,根据阳历年份值返回闰月值。这里有两点需要注意,一是参数使用的是阳历年份。二是当农历是冬月或腊月时,可能是阳历的第二年的一月或二月,在这种情况下,需要使用的参数就是阳历的前一年。

农历相关的数据会在对象初始化时进行计算,接下来可以使用一系列的属性获取农历信息的文本描述,首先看年份名称的获取,如下面的代码。

C#
// 年份名称
private static string[] yearNames = {"",
    "甲子","乙丑","丙寅","丁卯","戊辰","己巳","庚午","辛未","壬申","癸酉",
    "甲戌","乙亥","丙子","丁丑","戊寅","己卯","庚辰","辛巳","壬午","癸未",
    "甲申","乙酉","丙戌","丁亥","戊子","己丑","庚寅","辛卯","壬辰","癸巳",
    "甲午","乙未","丙申","丁酉","戊戌","己亥","庚子","辛丑","壬寅","癸卯",
    "甲辰","乙巳","丙午","丁未","戊申","己酉","庚戌","辛亥","壬子","癸丑",
    "甲寅","乙卯","丙辰","丁巳","戊午","己未","庚申","辛酉","壬戌","癸亥"};
//
public string YearName
{
    get { return yearNames[Year]; }
}

代码中,首先使用yearNames数组定义了甲子年份的名称,可以看到,年份名称是由十天二和十二地支循环组合而成。由于甲子年份的数据(Year属性)是1到60,所以,在yearNames数组的第一个成员使用了空字符串作为点位元素,这样,在YearName属性中就可以将Year属性作为索引值,直接从yearNams数组中返回甲子年份名称。

接下来是月份名称的获取,如下面的代码。

C#
// 月份名称
private static string[] monthNames =
    {"", "正月", "二月", "三月", "四月", "五月", "六月",
        "七月", "八月", "九月", "十月", "冬月", "腊月"};
//
public string MonthName
{
    get
    {
        if (LeapMonth <= 0 || Month < LeapMonth)
            return monthNames[Month];
        else if (Month == LeapMonth)
            return "闰" + monthNames[Month - 1];
        else
            return monthNames[Month - 1];
    }
}


MonthName属性中,当不存在闰月或当前月小于闰月时,直接返回农历月份名称。当前农历月份是LeapMonth属性值时,返回值会在前一月份名称前添加“润”字。当存在闰月并且月份大于LeapMonth值时,返回的月份名称应为Month属性值的前一个月。

下面的代码用于返回农历日期。

C#
// 日子名称
private static string[] dayNames =
    {"", "初一", "初二", "初三", "初四", "初五",
        "初六", "初七", "初八", "初九", "初十",
        "十一", "十二", "十三", "十四", "十五",
        "十六", "十七", "十八", "十九", "二十",
        "廿一", "廿二", "廿三", "廿四", "廿五",
        "廿六", "廿七", "廿八", "廿九", "三十"};
// 当前日期名称
public string DayName
{
    get { return dayNames[Day]; }
}


农历月份中,小月有29天,大月有30天,可以根据Day属性值直接从dayNames数组中返回日期名称。

下面的代码可以返回天干名称、地支名称,以及地支相关的生肖名称和时辰名称。

C#
// 天干名称
private static string[] tianganNames = {"",
    "甲","乙","丙","丁","戊","己","庚","辛","壬","癸" };
// 当前天干名称
public string TianGanName
{
    get { return tianganNames[TianGan]; }
}
// 地支名称
private static string[] dizhiNames = {"",
    "子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥" };
// 当前地支名称
public string DiZhiName
{
    get { return dizhiNames[DiZhi]; }
}
// 生肖对应地支
private static string[] shengxiaoNames = {"",
    "鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪" };
//
public string ShengXiaoName
{
    get { return shengxiaoNames[DiZhi]; }
}
// 0至23点给出时辰
private static string[] shiChenNames = {
    "子时","丑时","丑时","寅时","寅时","卯时","卯时",
    "辰时","辰时","巳时","巳时","午时","午时","未时","未时",
    "申时","申时","酉时","酉时","戌时","戌时","亥时","亥时","子时" };
//
public static string ShiChen(int hour)
{
    return shiChenNames[hour];
}
//
public static string ShiChen(DateTime dt)
{
    return ShiChen(dt.Hour);
}

tLunar类的最后定义了ToStr()方法,其功能是返回日期的完整农历信息。

C#
public string ToStr()
{
    return string.Format("{0}({1})年 {2}{3}",
        YearName, ShengXiaoName, MonthName, DayName);
}

下面的代码演示了tLunar类的基本应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        tLunar lunar = tLunar.Get(2021, 9, 21);
        tWeb.WriteLine("年份:" + lunar.YearName);
        tWeb.WriteLine("天干:" + lunar.TianGanName);
        tWeb.WriteLine("地支:" + lunar.DiZhiName);
        tWeb.WriteLine("月份:" + lunar.MonthName);
        tWeb.WriteLine("日期:" + lunar.DayName);
        tWeb.WriteLine("生肖:" + lunar.ShengXiaoName);
        tWeb.WriteLine(lunar.ToStr());
    }
}

页面显示结果如图5。

图5

最后来看除夕的查找方法。因为腊月也会有29天或30天的情况,所以,除夕可能是腊月廿十九或腊月三十;此时,可以按春节的前一天进行推算,如下面的代码,在tLunar类中使用GetChuXi()方法。

C#
public static tLunar GetChuXi(int year)
{
    DateTime dt = new DateTime(year, 1, 1, 
        new ChineseLunisolarCalendar());
    return new tLunar(dt.AddDays(-1));
}

下面的代码演示了GetChuXi()方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        tLunar lunar = tLunar.GetChuXi(2022);
        tWeb.WriteLine(lunar.ToStr());
        tWeb.WriteLine(lunar.Date.ToLongDateString());
        //
        tLunar chunjie = tLunar.GetByLunar(2022, 1, 1);
        tWeb.WriteLine(chunjie.ToStr());
        tWeb.WriteLine(chunjie.Date.ToLongDateString());
    }
}

代码执行结果如图6。

图6

本例,首先显示了2022年中除夕的日期,它对应的农历年份实际为2021年(辛丑年);然后,输出了是2022年(壬寅年)的春节,即正月初一的日期信息。获取其它的农历节日时,可以直接使用GetByLunar()方法创建tLunar对象,如下面的代码显示了2022年中的春节、端午节和中秋节的阳历日期。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        tWeb.WriteLine(
            tLunar.GetByLunar(2022, 1, 1).Date.ToLongDateString());
        tWeb.WriteLine(
            tLunar.GetByLunar(2022, 5, 5).Date.ToLongDateString());
        tWeb.WriteLine(
            tLunar.GetByLunar(2022, 8, 15).Date.ToLongDateString());
    }
}

页面显示结果如图7。

图7

更多日期和时间处理方案

C#应用开发中,日期和时间的处理是非常方便的,但需要注意应用开发中与非.NET Framework平台的数据交换问题。在不同的环境中,有一些常用的日期和时间处理方案,如:

  • 时间戳。使用整数保存日期和时间数据,表示距离基准时间的秒数,比如,以1970年1月1日0时为基准时间。
  • OLE自动化标准时间。以浮点数处理日期和时间,整数部分作为日期数据,小数部分作为时间数据,如Excel中使用的日期和时间处理方法。

SQLite数据库并没有内置日期和时间处理类型,在处理日期和时间数据时就需要采用适合的解决方案,后续课程会详细讨论。