- Boost程序库完全开发指南:深入C++”准”标准库(第5版)
- 罗剑锋
- 4181字
- 2020-08-27 16:45:18
2.5 处理日期
我们首先研究date_time库的日期处理部分,因为日期只涉及“年、月、日”,较时间少处理“时、分、秒”三个量,相当于数轴上的整数,要容易学习一些。
date_time库的日期基于格里高利历,支持从“1400-01-01”到“9999-12-31”之间的日期计算(很遗憾,它不能处理公元前的日期,不能用来研究古老的历史)。它位于名字空间boost::gregorian,需要包含的头文件如下:
2.5.1 日期
date是date_time库处理日期的核心类,使用一个32位的整数作为内部存储,以天为单位表示时间点概念。它的类摘要如下:
date是一个轻量级的对象,处理效率很高,可以被拷贝传值。date也全面支持比较操作和流输入输出,因此我们完全可以把它当成一个类似于int、string的基本类型来使用。
2.5.2 创建日期对象
有很多种方式可以创建日期对象。
空的构造函数会创建一个值为not_a_date_time的无效日期;顺序传入年、月、日值则创建一个对应日期的date对象。例如:
date也允许从一个字符串产生,这需要使用工厂函数from_string()或from_undelimited_string()。前者使用分隔符(斜杠或连字符)分隔年、月、日格式的字符串,后者则是无分隔符的纯字符串。例如:
day_clock是一个天级别的时钟,它也是一个工厂类,调用它的静态成员函数local_day()或universal_day()会返回一个当天的日期对象,分别是本地日期和UTC日期。day_clock内部使用了C标准库的函数localtime()和gmtime(),因此local_day()的行为依赖操作系统的时区设置。例如:
我们也可以使用特殊的时间概念枚举special_values来创建一些特殊的日期,在某些情形下(如无限期)会很有用:
使用cout将它们输出,显示如下:
-infinity+infinity not-a-date-time 9999-Dec-31 1400-Jan-01
如果在创建日期对象时使用了非法值,如日期超出了从1400-01-01到9999-12-31的范围,或者使用了不存在的月份或日期,那么date_time库会抛出异常(而不是转换为一个无效日期),可以使用what()获得具体的错误信息。
下面的date对象构造均会抛出异常:
2.5.3 访问日期
date的对外接口很像C语言中的tm结构,可以获取它保存的年、月、日、星期等成分,但date还提供了更多的操作。
成员函数year()、month()和day()分别返回日期的年、月、日:
成员函数year_month_day()返回一个date::ymd_type结构,可以一次性地获取年、月、日数据:
成员函数day_of_week()返回date的星期数,0表示星期天。day_of_year()返回date是当年的第几天(最大值是366)。end_of_month()返回当月的最后一天的date对象:
成员函数week_number()返回date所在的周是当年的第几周,其范围是0~53:
date还有5个is_xxx()函数,用于检验日期是否是一个特殊日期,具体如下。
■ is_infinity():是否是一个无限日期。
■ is_neg_infinity():是否是一个负无限日期。
■ is_pos_infinity():是否是一个正无限日期。
■ is_not_a_date():是否是一个无效日期。
■ is_special():是否是任意一个特殊日期。
它们的用法如下:
2.5.4 日期的输出
可以将date对象很方便地转换成字符串,它提供了3个自由函数。
■ to_simple_string():转换为YYYY-mmm-DD格式的字符串,其中,mmm为3字符的英文月份名。
■ to_iso_string():转换为YYYYMMDD格式的数字字符串。
■ to_iso_extended_string():转换为YYYY-MM-DD格式的数字字符串。
date也支持流输入输出,默认使用YYYY-mmm-DD格式。例如:
程序的运行结果如下:
2.5.5 转换C结构
date支持与C语言中的tm结构相互转换,转换的规则和函数如下。
■ to_tm(date):date转换到tm。将tm的时、分、秒成员(tm_hour/tm_min/tm_sec)均置为0,将夏令时标志tm_isdst置为-1(表示未知)。
■ date_from_tm(datetm):tm转换到date。只使用年、月、日3个成员(tm_year/tm_mon/tm_mday),其他成员均被忽略。
下面的代码示范了date结构与tm结构的相互转换:
2.5.6 日期长度
日期长度是以天为单位的时长,是度量时间长度的一个标量。它与日期不同,其值可以是任意整数,可正可负。基本的日期长度类是date_duration,它的类摘要如下:
date_duration可以使用构造函数创建一个日期长度,成员函数days()返回时长的天数,如果传入特殊时间枚举值则会构造出一个特殊时长对象。is_special()和is_negative()可以判断date_duration对象是否为特殊值,是否为负值。unit()返回时长的最小单位,即date_duration(1)。
date_duration支持全序比较操作(==、!=、<、<=等),也支持完全的加减法和递增递减操作,用起来很像一个整数。此外date_duration还支持除法运算,可以除以一个整数,但不能除以另一个date_duration,它不支持其他的数学运算,如乘法、取模、取余等。
date_time库为date_duration定义了一个常用的typedef:days,这个新名字更好地说明了date_duration的含义——它可以用来计量天数。
示范days(date_duration)用法的代码如下:
为了方便计算时间长度,date_time库还提供了months、years、weeks3个时长类,分别用来表示月、年和星期,它们的含义与days类似,但其行为不太相同。
months和years全面支持加减乘除运算,使用成员函数number_of_months()和number_of_years()可获得表示的月数和年数。weeks是date_duration的子类,除构造函数以7为单位以外,其他的行为与days完全相同,可以说它是days的近义词。
示范这3个时长类的基本用法的代码如下:
2.5.7 日期运算
date支持加减运算,但两个date对象的加法操作是无意义的(date_time库会以编译错误的方式通知我们),date主要用来与时长概念进行运算。
例如,下面的代码计算了从2000年1月1日到2017年11月18日的天数,并执行其他的日期运算:
日期与特殊日期长度、特殊日期与日期长度进行运算的结果也是特殊日期:
在与months、years这两个时长类进行计算时要注意:如果日期是月末的最后一天,那么加减月或年会得到同样的月末时间,这是合乎生活常识的。但当天数是月末的28或29时,如果加减月份到2月份,那么随后的运算就总是月末操作,原来的天数信息就会丢失。例如:
使用days则不会出现这样的问题,如果担心weeks、months、years这些时长类被无意使用进而扰乱了代码,可以undef宏BOOST_DATE_TIME_OPTIONAL_GREGORIAN_TYPES,这将使date_time库不包含它们的定义头文件<boost/date_time/gregorian/greg_duration_types.hpp>。
2.5.8 日期区间
date_time库使用date_period来表示日期区间的概念,它是时间轴上的一个左闭右开的区间,其端点是两个date对象。日期区间的左边界必须小于右边界,否则date_period将表示一个无效的日期区间。
date_period的类摘要如下:
date_period可以指定区间的两个端点构造区间,也可以指定左端点再加上时长构造区间,通常后一种方法比较常用,这相当于在生活中从某天开始的一个周期。例如:
成员函数begin()和last()返回日期区间的两个端点,而end()返回last()后的第一天,与标准容器中的end()含义相同,这是一个“逾尾的位置”。length()返回日期区间的长度,以天为单位。如果在构造日期区间时使用了左大右小的端点或日期长度是0,那么is_null()函数将返回true。例如:
date_period可以进行全序比较运算,但它依据的并不是日期区间的长度,而是区间的端点,即第一个区间的end()和第二个区间的begin(),判断这两个区间在时间轴上的位置大小。如果两个日期区间相交或包含,那么比较操作就无意义。
date_period还支持输入输出操作符,默认的输入输出格式是一个[YYYY-mmm-DD/YYYY-mmm-DD]形式的字符串。例如:
2.5.9 日期区间运算
date_period同date、days一样,也支持很多运算。
成员函数shift()和expand()可以变动区间:shift()将日期区间平移n天而长度不变,expand()将日期区间向两端延伸n天,相当于区间长度增加2n天。例如:
date_period可以使用成员函数判断某个日期是否在区间内,还可以计算日期区间的交集。
■ is_before()/is_after():日期区间是否在日期前或后。
■ contains():日期区间是否包含另一个区间或日期。
■ intersects():两个日期区间是否存在交集。
■ intersection():返回两个区间的交集,如果无交集,则返回一个无效区间。
■ is_adjacent():两个日期区间是否相邻。
示范这几个成员函数用法的代码如下:
date_period提供了两种并集操作。
■ merge():返回两个日期区间的并集,如果日期区间无交集或不相邻,则返回无效区间。
■ span():合并两个日期区间及两者间的间隔,相当于广义的merge()。
示范这两个并集函数的用法和区别的代码如下:
2.5.10 日期迭代器
date_time库为日期处理提供了日期迭代器的概念,可以用简单的递增或递减操作符连续访问日期,这些日期迭代器包括day_iterator、week_iterator、month_iterator和year_iterator,它们分别以天、周、月和年为单位。
日期迭代器的用法基本类似,都需要在构造时传入一个起始日期和增减步长(可以是一天、两周或N个月等,默认是1个单位),然后就可以用operator++、operator--变化日期。迭代器相当于一个date对象的指针,可以用解引用操作符*获得日期迭代器当前的日期对象,也可以用->直接调用日期对象的成员函数。
为了方便用户使用,日期迭代器还重载了比较操作符,不需要用解引用操作符就可以直接与其他日期对象比较大小。
示范日期迭代器的基本用法的代码如下:
需要提醒读者注意的是,虽然day_iterator、week_iterator的名字叫迭代器,但它并不符合标准迭代器的定义,如果没有difference_type、pointer、reference等内部类型定义,就不能使用std::advance()或operator+=来前进或后退。例如:
2.5.11 其他功能
boost::gregorian::gregorian_calendar类提供了格里高利历的一些操作函数,这些操作函数基本上在被date类内部使用,用户很少会用到。但它也提供了几个有用的静态函数:成员函数is_leap_year()可以判断年份是否是闰年;end_of_month_day()可以给定年份和月份,并返回该月的最后一天。例如:
date_time库还提供了很多有用的日期生成器,如某个月的最后一个星期天或第一个星期一等,它们封装了一些常用但计算起来又比较麻烦的时间概念,限于篇幅本书不进行过多的介绍。
2.5.12 综合运用
date_time库比较复杂,有很多的概念和对应的类,因为日期处理本身就存在很多复杂的因素。本节将综合运用与日期相关的所有类,给出一些具体的使用例子。
1.显示月历
我们实现一个打印月历的功能。用户指定一个日期,即可得到该月的起始日期和结束日期,然后我们构造一个日期迭代器,使用for循环打印出日期。
2.简单的日期计算
下面的程序可以计算一个人18岁的生日是星期几,当月有几个星期天,当年有多少天:
程序的运行结果如下:
其实计算当年的天数没有必要累加每月的天数,简单地判断该年是否是闰年即可。例如:
3.计算信用卡的免息期
我们实现一个较复杂的程序,计算信用卡的免息期。
先来简单了解一下计算信用卡的免息期的规则:使用信用卡的当天称为消费日,信用卡每月有一个记账日,在记账日之后有一个固定的免息还款期限,通常为20天,因此每笔信用卡交易的免息期就是消费日到下一个记账日的时间再加上还款期限,最长可以达到50天。
我们使用credit_card来代表信用卡,它保存了信用卡的基本信息,包括发卡银行名和记账日:
我们使用成员函数calc_free_days()来计算信用卡的免息期,它依据传入的消费日得到“下一个”记账日,并算出免息期:
为了支持比较操作,我们还需要为credit_card增加小于比较操作符重载:
最后,我们在main()函数中创建了两个信用卡对象,并使用标准库的算法std::max来比较这两个信用卡,从而决定免息期最长的那张信用卡:
通过这个程序,我们可以知道:如果今天是5号,那么应该使用A银行的信用卡,它的免息期是40天;如果今天是15号,那么应该使用B银行的信用卡,它的免息期是48天。
credit_card的calc_free_days()函数还可以直接传入指定的日期,以计算任意时间点的免息期。例如:
cout<<a.calc_free_days(date(2010,5,26));
读者可以进一步改进这个程序,把credit_card放入标准容器,使用std::sort算法来管理更多的信用卡(注意要使用函数对象或lambda表达式保存消费日期,作为比较谓词传递给算法)。