date_time
date_time库勇敢地面对了这个挑战,并成功地解决了大部分问题。它是一个非常全面且灵活的日期时间库,基于我们日常使用的公历(即格里高利历),可以提供时间相关的各种所需功能,如精确定义的时间点、时间段和时间长度、加减若干天/月/年、日期迭代器等等。date_time库还支持无限时间和无效时间这种实际生活中有用的概念,而且可以与c的传统时间结构tm相互转换,提供向下支持。
编译与使用
date_time库需要编译才能使用,在jamfile里指定lib的语句是:
lib boost_data_time;
date_time 库包含两个部分,分别是处理日期的 gregorian和处理时间的posix_time,需要包含如下的头文件:
//处理日期的组件
#inlcude <boost/date_time/gregorian/gregorian.hpp>
using namespace boost::gregorian;
//处理时间的组件
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
基本概念
时间的处理很复杂,因此在使用date_time库之前,我们需要明确一些基本概念。
如果把时间想象成一个向前和向后都无限延伸的实数轴,那么时间点就是数轴上的一个点,时间段就是两个时间点之间确定的一个区间,时长(时间长度)则是一个有正负号的标量,它是两个时间点之差,不属于数轴。
时间点、时间段和时长三者之间可以进行运算,例如时间点+时长=时间点,时长+时长=时长,时间段∩时间段=时间段、时间点∈时间段等等,但有的运算也是无意义的,如时间点+时间点、时长+时间段等等。这些运算都基于生活常识,很容易理解,但在编写时间处理程序时必须注意。
date_time库支持无限时间和无效时间(NADT,Not Available Date Time)这样特殊的时间概念,类似于数学中极限的含义。时间点和时长都有无限的值,它们的运算规则比较特别,例如+ oo时间点+时长=+∞时间点,时间点+∞时长=+∞时间点。如果正无限值与负无限值一起运算将有可能是无效时间,如+∞时长-时长=NADT。
date_time库中用枚举special_values定义了这些特殊的时间概念,它位于名字空间boost::date_time,并被using语句引入其他子名字空间。
pos_infin 表示正无限
neg_infin 表示负无限
not_a_date_time 无效时间
min_date_time 可表示的最小日期或时间
max_date_time 可表示的最大日期或时间
处理日期
我们首先研究date_time库的日期处理部分,因为日期只涉及年月日,较时间少处理时分秒三个量,相当于数轴上的整数,要容易学习一些。
date_time库的日期基于格里高利历,支持从1400-01-01到9999-12-31之间的日期计算(很遗憾,它不能处理公元前的日期,不能用它来研究古老的历史)。它位于名字空间boost:: gregorian,为了使用date_time库的日期功能,需要包含头文件<boost/date_time/gregorian/gregorian.hpp>,即:
#include <boost/date_time/gregorian/gregorian.hpp>
using namespace boost::gregorian;
日期
date是 date_time 库处理日期的核心类,使用一个32位的整数作为内部存储,以天为单位表示时间点概念。它的类摘要如下:
template<typename T, typename calendar, typename duration_type_>
class date {
public:
date(year_type, month_type, day_type); //构造函数
date(const ymd_type &);
year_type year() const; //基本操作函数
month_type month() const;
day_type day() const;
day_of_week_type day_of_week() const;
ymd_type year_month_day() const;
bool operator<(const date_type &) const; //比较操作函数
bool operator==(const date_type &) const;
bool is_special() const; //有效性验证
bool is_not_a_date() const;
bool is_infinity() const;
bool is_pos_infinity() const;
bool is_neg_infinity() const;
special_values as_special() const;
duration_type operator-(const date_type &) const;
... //其他日期运算
};
date是一个轻量级的对象,很小,处理效率很高,可以被拷贝传值。date也全面支持比较操作和流输入输出,因此我们完全可以把它当成一个像int、string那样的基本类型来使用。
创建日期对象
有很多种方式可以创建一个日期对象。
空的构造函数会创建一个值为not_a_date_time的无效日期;顺序传入年月日值则创建一个对应日期的date对象,例如:
date d1; //一个无效的日期
date d2(2010, 1, 1); //使用数字构造日期
date d3(2000, Jan, 1); //也可以使用英文指定月份
date d4(d2); //date支持拷贝构造
assert(d1 == date(not_a_date_time)); //比较一个临时对象
assert(d2 == d4); //date支持比较操作
assert(d3 < d4);
date 也可以从一个字符串产生,这需要使用工厂函数from_string()或fromundelimited_string()。前者使用分隔符(斜杠或者连字符)分隔年月日格式的字符串,后者则是无分隔符的纯字符串格式。例如:
date d1 = from_string("1999-12-31");
date d2 (from_string("2015/1/1"));
date d3 = from_undelimited_string("20011118");
day_clock 是一个天级别的时钟,它也是一个工厂类,调用它的静态成员函数local_day()
或universal_day()
会返回一个当天的日期对象,分别是本地日期和UTC日期。day_clock 内部使用了c标准库的 localtime()
和gmtime()
函数,因此local_day()
的行为依赖于操作系统的时区设置。例如:
cout << day_clock::local_day() << endl;
cout << day_clock::universal_day() << endl;
我们也可以使用特殊时间概念枚举 special_values 创建一些特殊的日期,在处理如无限期的某些情形下会很有用:
date d1(neg_infin); //负无限日期
date d2(pos_infin); //正无限日期
date d3(not_a_date_time): //无效日期
date d4(max_date_time); //最大可能日期9999-12-31
date d5(min_date_time); //最小可能日期1400-01-01
使用cout将它们输出,显示将会是:
-infinity
+infinity
not-a-date-time
9999-Dec-31
1400-Jan-01
如果创建日期对象时使用了非法的值,例如日期超过了1400-01-01到9999-12-31的范围,或者使用不存在的月份或日期,那么date_time库会抛出异常(而不是转换为一个无效日期),可以使用what ()获得具体的错误信息。
下面的date对象构造均会抛出异常:
date d1(1399, 12, 1); //超过日期下限
date d2(10000, 1, 1); //超过日期上限
date d3(2010, 2, 29); //不存在的日期
访问日期
date类的对外接口很像c语言中的tm结构,也可以获取它保存的年、月、日、星期等成分,但 date提供了更多的操作。
成员函数year() 、 month()和 day()分别返回日期的年、月、日:
date d(2014, 4, 1);
assert(d.year() == 2014);
assert(d.month() == 4);
assert(d.day() == 1);
成员函数year_month_day()返回一个date::ymd_type结构,可以一次性地获取年月日数据:
date::ymd_type ymd = d.year_month_day();
assert(ymd.year == 2014);
assert(ymd.month == 4);
assert(ymd.day == 1);
成员函数 day_of_week ()
返回date 的星期数,0表示星期天。day_of_year()
返回date是当年的第几天(最多是366)。end_of_month()
返回当月的最后一天的date对象:
cout << d.day_of_week() << endl; //输出Tue
cout << d.day_of_year() << endl; //输出91
assert(d.end_of_month() == date(2014, 4, 30));
成员函数week_number()
返回date所在的周是当年的第几周,范围是0至53:
cout << date(2014, 1, 10).week_number() << endl; //输出2
cout << date(2014, 1, 1).week_number() << endl; //输出1
cout << date(2015, 1, 1).week_number() << endl; //输出1
date还有五个is_xxx()函数,用于检验日期是否是一个特殊日期,它们是:
is_infinity() 是否是一个无限日期
is_neg_infinity() 是否是一个负无限日期
is_pos_infinity() 是否是一个正无限日期
is_not_a_date() 是否是一个无效日期
is_special() 是否是任意一个特殊日期
他们用法如下:
assert(date(pos_infin).is_infinity());
assert(date(pos_infin).is_pos_infinity());
assert(date(neg_infin).is_neg_infinity());
assert(date(not_a_date_time).is_not_a_date());
assert(date(not_a_date_time).is_special());
assert(!date(2014,11,1).is_special());
日期的输出
date对象可以很方便地转换成字符串,它提供了三个自由函数。
to_simple_string(date d): 转换为YYYY-mmm-DD格式的字符串,其中的mmm为3字符的英文月份名
to_iso_string(date d): 转换为YYYYMMDD格式的数字字符串
to_iso_extended_string(date d): 转换为YYYY-MM-DD格式的数字字符串
date也支持流输入输出,默认使用YYYY-mmm-DD格式。例如:
date d(2008, 11, 20);
cout << to_simple_string(d) << endl;
cout << to_iso_string(d) << endl;
cout << to_iso_extended_string(d) << endl;
cout << d << endl;
cin >> d;
cout << d;
程序的运行结果如下:
2008-Nov-20
20081120
2008-11-20
2008-Nov-20
2010-Jan-02(用户的输入)
2010-Jan-02
转换tm结构
date支持与c标准库中的tm结构相互转换,转换的规则和函数如下:
to_tm(date) date转换到tm。tm的时分秒成员(tm_hour, tm_min, tm_sec)均置为0,夏令时标志tm_isdst置为-1(表示未知)
date_from_tm(tm datetm) tm转换到date。只使用年、月、日三个成员(tm_year, tm_mon, tm_mday), 其他成员均被忽略。
下面的代码示范了date 与tm的相互转换:
date d(2014, 2, 1);
tm t = to_tm(d);
assert(t.tm_hour == 0 && t.tm_min == 0);
assert(t.tm_year == 114 && t.tm_mday == 1);
date d2 = date_from_tm(t);
assert(d == d2);
日期长度
日期长度是以天为单位的时长,是度量时间长度的一个标量。它与日期不同,值可以是任意的整数,可正可负。基本的日期长度类是date_duration,它的类摘要如下:
class date_duration
{
public:
date_duration(long); //构造函数
date_duration(special_values);
long days() const; //成员访问函数
bool is_special() const;
bool is_negative() const;
bool operator==(const date_duration &) const;
... //其他操作符定义
static date_duration unit(); //时长单位
};
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)用法的代码如下:
days dd1(10), dd2(-100), dd3(255);
assert(dd1 > dd2 && dd1 < dd3);
assert(dd1 + dd2 == days(-90));
assert((dd1 + dd3).days() == 265);
assert(dd3 / 5 == days(51));
为了方便计算时间长度,date_time库还提供了months、years、weeks等另外三个时长类,分别用来表示月、年和星期,它们的含义与days类似,但行为不太相同。
months和 years 全面支持加减乘除运算,使用成员函数number_of_months()
和number_of_years()
可获得表示的月数和年数。weeks是date_duration的子类,除了构造函数以7为单位外其他的行为与days完全相同,可以说是一个days的近义词。
示范这三个时长类基本用法的代码如下:
weeks w(3); //3个星期
assert(w.days() == 21);
months m(5); //5个月
years y(2); //2年
months m2 = y + m;
assert(m2.number_of_months() == 29); //2年零5个月
assert((y * 2).number_of_years() == 4);
日期运算
date支持加减运算,两个date对象的加操作是无意义的(date_time库会以编译错误的方式通知我们),date主要是与时长概念配合运算。
例如,下面的代码计算了从2000年1月1日到2014年11月18日的天数,并执行其他的日期运算:
#include <boost/date_time/gregorian/gregorian.hpp>
using namespace boost::gregorian;
int main()
{
date d1(2000, 1, 1), d2(2017, 11, 18);
cout << d2 - d1 << endl; //5435天
assert(d1 + (d2 - d1) == d2);
d1 += days(10); //2000-1-11
assert(d1.day() == 11);
d1 += months(2); //2000-3-11
assert(d1.month() == 3 && d1.day() == 11);
d1 -= weeks(1); //2000-3-04
assert(d1.day() == 4);
d2 -= years(10); //2000-11-18
assert(d2.year() == d1.year() + 7);
{
date d1(2017, 1, 1);
date d2 = d1 + days(pos_infin);
assert(d2.is_pos_infinity());
d2 = d1 + days(not_a_date_time);
assert(d2.is_not_a_date());
d2 = date(neg_infin);
days dd = d1 - d2;
assert(dd.is_special() && !dd.is_negative());
}
{
date d(2017, 3, 30);
d -= months(1);
d -= months(1);
d += months(2);
assert(d.day() == 31);
}
}
日期与特殊日期长度、特殊日期与日期长度进行运算的结果也会是特殊日期:
date d1(2017, 1, 1);
date d2 = d1 + days(pos_infin);
assert(d2.is_pos_infinity());
d2 = d1 + days(not_a_date_time);
assert(d2.is_not_a_date());
d2 = date(neg_infin);
days dd = d1 - d2;
assert(dd.is_special() && !dd.is_negative());
在与months、years这两个时长类进行计算时要注意:如果日期是月末的最后一天,那么加减月或年会得到同样的月末时间,而不是简单的月份或者年份加1,这是合乎生活常识的。但当天数是月末的28、29时,如果加减月份到2月份,那么随后的运算就总是月末操作,原来的天数信息就会丢失。例如:
date d(2014, 3, 30);
d -= months(1); //2014-2-28,变为月末,原30的日期信息丢失
d -= months(1); //2014-1-31
d += months(2); //2014-3-31
assert(d.day() == 31); //与原来日期不相等
使用days则不会出现这样的问题,如果担心weeks、months、years这些时长类被无意使用进而扰乱了代码,可以undef宏BOOST_DATE_TIME_OPTIONAL_GREGORIAN_TYPES
,这将使 date_time库不包含它们的定义头文件<boost/date_time/gregorian/greg_duration_types.hpp>
。
日期区间
date_time库使用date_period类来表示日期区间的概念,它是时间轴上的一个左闭右开区间,端点是两个date对象。区间的左边界必须小于右边界,否则date_period将表示一个无效的日期区间。
date_period的类摘要如下:
class date_period
{
public:
period(date, date);
period(date, days);
date begin() const;
date end() const;
date last() const;
days length() const;
bool is_null() const;
bool operator==(const period &) const;
bool operator<(const period &) const;
void shift(const days &);
void expend(const days &);
bool contains(const date &) const;
bool contains(const period &) const;
bool intersects(const period &) const;
bool is_adjacent(const period &) const;
bool is_before(const date &) const;
bool is_after(const date &) const;
period intersection(const period &) const;
period merge(const period &) const;
period span(const period &) const;
};
date_period可以指定区间的两个端点构造,也可以指定左端点再加上时长构造,通常后一种方法比较常用,相当于生活中从某天开始的一个周期。例如:
date_period dp1(date(2014,1,1), days(20));
date_period dp2(date(2014,1,1), date(2013,1,1)); //无效
date_period dp3(date(2014,3,1), days(-20)); //无效
成员函数begin()和 last()返回日期区间的两个端点,而end()返回 last()后的第一天,与标准容器中的end()含义相同,是一个“逾尾的位置”。length()返回日期区间的长度,以天为单位。如果日期区间在构造时使用了左大右小的端点或者日期长度是0,那么is_nul1()函数将返回true。例如:
date_period dp(date(2014,1,1), days(20));
assert(!dp.is_null());
assert(dp.begin().day() == 1);
assert(dp.last().day() == 20);
assert(dp.end().day() == 21);
assert(dp.length().days() == 20);
date_period可以进行全序比较运算,但比较不是依据日期区间的长度,而是依据区间的端点,即第一个区间的end()和第二个区间的begin(),判断两个区间在时间轴上的位置大小。如果两个日期区间相交或者包含,那么比较操作无意义。
date_period也支持输入输出操作符,默认的输入输出格式是一个[YYYY-mmm-DD/YYYY-mmm-DD]形式的字符串。例如:
date_period dp1(date(2014,1,1), days(20));
date_period dp2(date(2014,2,19), days(10));
cout << dp1; //[2014-Jan-01/2014-Jan-20]
assert(dp1 < dp2);
代码实例
#include <iostream>
using namespace std;
//#define DATE_TIME_NO_DEFAULT_CONSTRUCTOR
#include <boost/date_time/gregorian/gregorian.hpp>
using namespace boost::gregorian;
//
void case1()
{
date d1;
date d2(2010, 1, 1);
date d3(2000, Jan, 1);
date d4(d2);
assert(d1 == date(not_a_date_time));
assert(d2 == d4);
assert(d3 < d4);
}
//
void case2()
{
date d1 = from_string("1999-12-31");
date d2(from_string("2015/1/1"));
date d3 = from_undelimited_string("20011118");
cout << d1 << d2 << d3 << endl;
cout << day_clock::local_day() << endl;
cout << day_clock::universal_day() << endl;
}
//
void case3()
{
date d1(neg_infin);
date d2(pos_infin);
date d3(not_a_date_time);
date d4(max_date_time);
date d5(min_date_time);
cout << d1 << d2 << d3 << d4 << d5 << endl;
try
{
//date d1(1399,12,1);
//date d2(10000,1,1);
date d3(2017, 2, 29);
}
catch (std::exception& e)
{
cout << e.what() << endl;
}
}
//
void case4()
{
date d(2017, 6, 1);
assert(d.year() == 2017);
assert(d.month() == 6);
assert(d.day() == 1);
date::ymd_type ymd = d.year_month_day();
assert(ymd.year == 2017);
assert(ymd.month == 6);
assert(ymd.day == 1);
cout << d.day_of_week() << endl;
cout << d.day_of_year() << endl;
assert(d.end_of_month() == date(2017, 6, 30));
cout << date(2015, 1, 10).week_number() << endl;
cout << date(2016, 1, 10).week_number() << endl;
cout << date(2017, 1, 10).week_number() << endl;
assert(date(pos_infin).is_infinity());
assert(date(pos_infin).is_pos_infinity());
assert(date(neg_infin).is_neg_infinity());
assert(date(not_a_date_time).is_not_a_date());
assert(date(not_a_date_time).is_special());
assert(!date(2017, 5, 31).is_special());
}
//
void case5()
{
date d(2017, 1, 23);
cout << to_simple_string(d) << endl;
cout << to_iso_string(d) << endl;
cout << to_iso_extended_string(d) << endl;
cout << d << endl;
//cout << "input date:";
//cin >>d;
//cout << d;
}
//
void case6()
{
date d(2017, 5, 20);
tm t = to_tm(d);
assert(t.tm_hour == 0 && t.tm_min == 0);
assert(t.tm_year == 117 && t.tm_mday == 20);
date d2 = date_from_tm(t);
assert(d == d2);
}
//
void case7()
{
days dd1(10), dd2(-100), dd3(255);
assert(dd1 > dd2 && dd1 < dd3);
assert(dd1 + dd2 == days(-90));
assert((dd1 + dd3).days() == 265);
assert(dd3 / 5 == days(51));
weeks w(3);
assert(w.days() == 21);
months m(5);
years y(2);
months m2 = y + m;
assert(m2.number_of_months() == 29);
assert((y * 2).number_of_years() == 4);
}
//
void case8()
{
date d1(2000, 1, 1), d2(2017, 11, 18);
cout << d2 - d1 << endl;
assert(d1 + (d2 - d1) == d2);
d1 += days(10);
assert(d1.day() == 11);
d1 += months(2);
assert(d1.month() == 3 && d1.day() == 11);
d1 -= weeks(1);
assert(d1.day() == 4);
d2 -= years(10);
assert(d2.year() == d1.year() + 7);
{
date d1(2017, 1, 1);
date d2 = d1 + days(pos_infin);
assert(d2.is_pos_infinity());
d2 = d1 + days(not_a_date_time);
assert(d2.is_not_a_date());
d2 = date(neg_infin);
days dd = d1 - d2;
assert(dd.is_special() && !dd.is_negative());
}
{
date d(2017, 3, 30);
d -= months(1);
d -= months(1);
d += months(2);
assert(d.day() == 31);
}
}
//
void case9()
{
date_period dp1(date(2017, 1, 1), days(20));
date_period dp2(date(2017, 1, 1), date(2016, 1, 1));
date_period dp3(date(2017, 3, 1), days(-20));
date_period dp(date(2017, 1, 1), days(20));
assert(!dp.is_null());
assert(dp.begin().day() == 1);
assert(dp.last().day() == 20);
assert(dp.end().day() == 21);
assert(dp.length().days() == 20);
{
date_period dp1(date(2017, 1, 1), days(20));
date_period dp2(date(2017, 2, 19), days(10));
cout << dp1; //[2010-Jan-01/2010-Jan-20]
assert(dp1 < dp2);
}
}
//
int main()
{
case1();
case2();
case3();
case4();
case5();
case6();
case7();
case8();
case9();
}