根据python datetime实现的C++Date类
文章目录
一、需求
(来自The C++ Programming Language第十章)要求设计一个以1970年1月1日以后的天数来表示日期的日期类,如下图:
这里借鉴了python标准库datetime的实现方法。datetime的日期是以日期在公历(Gregorian Calendar)中的序号来表示的,也就是“第几天”的意思。例如,1-1-1在公历中是第一天,它的序号就是1。
见Lib/datetime.py:20
的注释:The code here calls January 1 of year 1 day number 1.
这里主要学习datetime.py的实现中年月日格式和序号格式互转的方法。
二、年月日格式和序号格式互转方法
2.1 年月日转序号(ymd2ord)
年月日转序号的思想是这样的:日期(year-month-day)的序号是
ordinal=_days_before_year(year)+_days_before_month(month)+day
也就是(1)公元1年起到当前年份的所有整年的天数、(2)本年内本月前所有整月的天数、(3)本月天数,这三者相加。
(2)和(3)的计算很简单。注意闰年的判断条件是所有能被4整除而不能被100整除,或者能被400整除的年份。每4年一个闰年、每100要减去一个闰年、每400年要再增加一个闰年(我们知道这是地球公转周期决定的)。
因此_days_before_year()的实现是这样的:
def _days_before_year(y):
y = y -1
return 365*y + y//4 - y//100 + y//400
2.2 序号转年月日(ord2ymd)
2.2.1 年份的确定
给定日期的序号形式ord来确定年份y。
定义三个常量DI400Y, DI100Y, DI4Y,把ord-1(ord-1是序号,ord-1才是距离公历1-1-1的天数)依次对这三个常量取模后当前年份之前的闰年已经全部包含在内,再对365取模,商分别是n400, n100, n4, n1,这时就能基本确定年份y了:
n = ord - 1
n400, n = divmod(n, DI400Y)
n100, n = divmod(n, DI100Y)
n4, n = divmod(n, DI4Y)
n1, n = divmod(n, 365)
y = 400*n400 + 100*n100 + 4*n4 + n1 + 1
但是要注意到DI400Y, DI100Y, DI4Y的关系,这里比较复杂:
DI4Y = 365*4 + 1
DI100Y = 25*DI4Y -1
DI400Y = 4*DI100Y + 1
// 所以 n % DI4Y, n % DI400Y 最大可以取到 4*365 和 4*DI100Y
// 在这种情况下 (n1 == 4 || n100 == 4), n 必然为 0, 日期则是 12月31日
// 由于 n1 **或** n100 多算了一年(显然 n1 和 n100 是不会同时取4的), 年份应该是 y - 1
也就是说这里n1, n100为4其实是不正常的值,毕竟我们已经抛去了4年(或100年)相应的天数。
仔细思考一下,公历日期5-1-1的序号是365*4+2,距离1-1-1的天数是365*4+1。而4-12-31才是序号是365*4+1,距离1-1-1的天数是4*365。我们发现这时候年份确实是要减一的。
因此datetime.py中有这样几句断言:
assert DI400Y == 4 * DI100Y + 1
assert DI100Y == 25 * DI4Y -1
assert DI4Y == 4 * 365 + 1
if n1 == 4 or n100 == 4:
assert n == 0
// 而且根据n1, n100, n4 的值我们可以推导出闰年条件:
leapyear = n1 == 3 and (n4 != 24 || n100 == 3)
assert leapyear == _is_leap(y)
2.2.2 月份和日期的确定
确定月份利用位运算构造了一个线性函数的表达式,使得取值总是正确的月份值或者正好比真正的月份值大一。也是比较精妙的,上官方代码:
# Now the year is correct, and n is the offset from January 1. We find
# the month via an estimate that's either exact or one too large.
leapyear = n1 == 3 and (n4 != 24 or n100 == 3)
assert leapyear == _is_leap(year)
month = (n + 50) >> 5
// 这里preceding 的意思是所谓猜测的月份值所对应的 “days before month”
preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear)
if preceding > n: # estimate is too large
month -= 1
preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear)
n -= preceding
assert 0 <= n < _days_in_month(year, month)
# Now the year and month are correct, and n is the offset from the
# start of that month: we're done!
return year, month, n+1
到这里序号转年月日的核心代码就都出现过了,个人感觉最难懂的地方还是在确定年份的地方,有很多细节部分需要仔细思考才能想明白。
最后搬运一下python内置模块datetime的相关代码。
2.3 datetime.py 1~142行 官方代码
datetime.py:
"""Concrete date/time and related types.
See http://www.iana.org/time-zones/repository/tz-link.html for
time zone and DST data sources.
"""
import time as _time
import math as _math
def _cmp(x, y):
return 0 if x == y else 1 if x > y else -1
MINYEAR = 1
MAXYEAR = 9999
_MAXORDINAL = 3652059 # date.max.toordinal()
# Utility functions, adapted from Python's Demo/classes/Dates.py, which
# also assumes the current Gregorian calendar indefinitely extended in
# both directions. Difference: Dates.py calls January 1 of year 0 day
# number 1. The code here calls January 1 of year 1 day number 1. This is
# to match the definition of the "proleptic Gregorian" calendar in Dershowitz
# and Reingold's "Calendrical Calculations", where it's the base calendar
# for all computations. See the book for algorithms for converting between
# proleptic Gregorian ordinals and many other calendar systems.
# -1 is a placeholder for indexing purposes.
_DAYS_IN_MONTH = [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
_DAYS_BEFORE_MONTH = [-1] # -1 is a placeholder for indexing purposes.
dbm = 0
for dim in _DAYS_IN_MONTH[1:]:
_DAYS_BEFORE_MONTH.append(dbm)
dbm += dim
del dbm, dim
def _is_leap(year):
"year -> 1 if leap year, else 0."
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
def _days_before_year(year):
"year -> number of days before January 1st of year."
y = year - 1
return y*365 + y//4 - y//100 + y//400
def _days_in_month(year, month):
"year, month -> number of days in that month in that year."
assert 1 <= month <= 12, month
if month == 2 and _is_leap(year):
return 29
return _DAYS_IN_MONTH[month]
def _days_before_month(year, month):
"year, month -> number of days in year preceding first day of month."
assert 1 <= month <= 12, 'month must be in 1..12'
return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year))
def _ymd2ord(year, month, day):
"year, month, day -> ordinal, considering 01-Jan-0001 as day 1."
assert 1 <= month <= 12, 'month must be in 1..12'
dim = _days_in_month(year, month)
assert 1 <= day <= dim, ('day must be in 1..%d' % dim)
return (_days_before_year(year) +
_days_before_month(year, month) +
day)
_DI400Y = _days_before_year(401) # number of days in 400 years
_DI100Y = _days_before_year(101) # " " " " 100 "
_DI4Y = _days_before_year(5) # " " " " 4 "
# A 4-year cycle has an extra leap day over what we'd get from pasting
# together 4 single years.
assert _DI4Y == 4 * 365 + 1
# Similarly, a 400-year cycle has an extra leap day over what we'd get from
# pasting together 4 100-year cycles.
assert _DI400Y == 4 * _DI100Y + 1
# OTOH, a 100-year cycle has one fewer leap day than we'd get from
# pasting together 25 4-year cycles.
assert _DI100Y == 25 * _DI4Y - 1
def _ord2ymd(n):
"ordinal -> (year, month, day), considering 01-Jan-0001 as day 1."
# n is a 1-based index, starting at 1-Jan-1. The pattern of leap years
# repeats exactly every 400 years. The basic strategy is to find the
# closest 400-year boundary at or before n, then work with the offset
# from that boundary to n. Life is much clearer if we subtract 1 from
# n first -- then the values of n at 400-year boundaries are exactly
# those divisible by _DI400Y:
#
# D M Y n n-1
# -- --- ---- ---------- ----------------
# 31 Dec -400 -_DI400Y -_DI400Y -1
# 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary
# ...
# 30 Dec 000 -1 -2
# 31 Dec 000 0 -1
# 1 Jan 001 1 0 400-year boundary
# 2 Jan 001 2 1
# 3 Jan 001 3 2
# ...
# 31 Dec 400 _DI400Y _DI400Y -1
# 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary
n -= 1
n400, n = divmod(n, _DI400Y)
year = n400 * 400 + 1 # ..., -399, 1, 401, ...
# Now n is the (non-negative) offset, in days, from January 1 of year, to
# the desired date. Now compute how many 100-year cycles precede n.
# Note that it's possible for n100 to equal 4! In that case 4 full
# 100-year cycles precede the desired day, which implies the desired
# day is December 31 at the end of a 400-year cycle.
n100, n = divmod(n, _DI100Y)
# Now compute how many 4-year cycles precede it.
n4, n = divmod(n, _DI4Y)
# And now how many single years. Again n1 can be 4, and again meaning
# that the desired day is December 31 at the end of the 4-year cycle.
n1, n = divmod(n, 365)
year += n100 * 100 + n4 * 4 + n1
if n1 == 4 or n100 == 4:
assert n == 0
return year-1, 12, 31
# Now the year is correct, and n is the offset from January 1. We find
# the month via an estimate that's either exact or one too large.
leapyear = n1 == 3 and (n4 != 24 or n100 == 3)
assert leapyear == _is_leap(year)
month = (n + 50) >> 5
preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear)
if preceding > n: # estimate is too large
month -= 1
preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear)
n -= preceding
assert 0 <= n < _days_in_month(year, month)
# Now the year and month are correct, and n is the offset from the
# start of that month: we're done!
return year, month, n+1
三、我的C++ Date class
借鉴python datetime.py中的思想,要求Date class只存储一个日期距离公历1970-1-1(默认日期,default date)的天数,只要设置一个静态成员变量offset来存default date距离公元1-1-1的天数即可,而普通成员变量ordinal存一个日期距离公历1970-1-1的天数。
实现原理和datetime.py几乎一模一样,也作为对所学方法的巩固,这里就直接上代码了:
Date.h:
#ifndef DATE_H_
#define DATE_H_
# include <iostream>
using std::string;
class Date
{
int ord;
// default date
static int offset, Y, M, D;
friend int daysBeforeYear(int);
friend int daysBeforeMonth(int, int);
friend void ord2ymd(int, int&, int&, int&);
public:
static int DI400Y, DI100Y, DI4Y;
static int Days_in_month[13], Days_before_month[13];
enum Month {Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};
Date(int, int, int);
void set_default(int, int, int);
string string_rep() const;
void char_rep(char s[]) const;
int day() const;
int month() const;
int year() const;
int ordinal() const;
Date& add_day(int);
Date& add_month(int);
Date& add_year(int);
};
#endif /* DATE_H_ */
Date.cpp:
/*
* Date.cpp
*
* Created on: 2020年6月11日
* Author: 18488
*/
# include <iostream>
# include <sstream>
# include "Date.h"
using std::string;
using std::ostringstream;
using std::cout;
using std::endl;
int Date::DI4Y = 365*4 +1;
int Date::DI100Y = 25*DI4Y -1;
int Date::DI400Y = 4*DI100Y +1;
int Date::Days_in_month[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int Date::Days_before_month[] = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int Date::Y = 1970, Date::M = 1, Date::D = 1;
bool isLeapYear(int year) {
return (year %4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int daysBeforeYear(int year) {
-- year;
return 365*year + year/4 + year/400 - year/100;
}
int daysBeforeMonth(int year, int month) {
return Date::Days_before_month[month] + (int)(isLeapYear(year) && month > 2);
}
int ymd2ord(int y, int m, int d) {
return daysBeforeYear(y) + daysBeforeMonth(y, m) + d;
}
int Date::offset = ymd2ord(1970,1,1) -1;
void ord2ymd(int ord, int& y, int& m, int& d) {
int n = ord + Date::offset-1;
// cout <<"n = " <<n <<endl;
int n400 = n/Date::DI400Y; n = n%Date::DI400Y;
int n100 = n/Date::DI100Y; n = n%Date::DI100Y;
int n4 = n/Date::DI4Y; n = n%Date::DI4Y;
int n1 = n/365; n = n%365;
y = 400*n400 + 100*n100 + 4*n4 + n1 + 1;
if ((n1 == 4 || n100 == 4) && n == 0) {
-- y; m = 12; d = 31; return;
}
m = (n+50) >> 5;
int preceding = daysBeforeMonth(y,m);
if (preceding > n) {
-- m;
preceding -= Date::Days_in_month[m] + (int)(isLeapYear(y) && m == 2);
}
// cout <<"preceding = " <<preceding <<" n = " <<n <<endl;
// cout <<"year: " <<y <<"Leapyear: " <<isLeapYear(y) <<"offset :" <<Date::offset <<endl;
d = n - preceding +1;
}
void Date::set_default(int y, int m, int d) {
Y = y; M = m; D = d;
offset = ymd2ord(y,m,d) -1;
}
Date::Date(int y, int m, int d) {
ord = ymd2ord(y,m,d) - offset;
}
string Date::string_rep() const{
int y, m, d;
ord2ymd(ord, y, m, d);
ostringstream out;
out <<y <<"-" <<m <<"-" <<d;
return out.str();
}
void Date::char_rep(char s[]) const {
int y, m, d;
ord2ymd(ord, y, m, d);
sprintf(s, "%d-%d-%d", y, m, d);
}
int Date::day() const {
int y,m,d;
ord2ymd(ord, y,m,d);
return d;
}
int Date::month() const {
int y,m,d;
ord2ymd(ord, y,m,d);
return m;
}
int Date::year() const {
int y,m,d;
ord2ymd(ord, y,m,d);
return y;
}
int Date::ordinal() const {
// return ord;
return ord + offset;
}
Date& Date::add_day(int n) {
ord += n;
return *this;
}
Date& Date::add_month(int n) {
int y, m, d;
ord2ymd(ord, y,m,d);
y += n/12; m += n;
ord = ymd2ord(y, m, d) - offset;
return *this;
}
Date& Date::add_year(int n) {
int y,m,d;
ord2ymd(ord, y,m,d);
y += n;
ord = ymd2ord(y, m, d) - offset;
return *this;
}