实现python datetime算法的C++Date类--The C++ Programmming Language

根据python datetime实现的C++Date

一、需求

(来自The C++ Programming Language第十章)要求设计一个以197011日以后的天数来表示日期的日期类,如下图:

在这里插入图片描述
这里借鉴了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*3654*DI100Y
// 在这种情况下 (n1 == 4 || n100 == 4), n 必然为 0, 日期则是 1231// 由于 n1 **** n100 多算了一年(显然 n1 和 n100 是不会同时取4), 年份应该是 y - 1

也就是说这里n1, n1004其实是不正常的值,毕竟我们已经抛去了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;
}


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值