1. 项目背景与需求解析
这个OJ题目属于面向对象编程中的经典日期类设计问题。我在大二数据结构课程第一次接触这类题目时,花了整整一个周末才完全理解所有边界条件。日期计算看似简单,实则暗藏玄机——闰年判断、月份天数差异、日期合法性校验,每个环节都能让新手栽跟头。
题目核心是构建一个健壮的日期类(Date),需要实现以下基本功能:
- 日期合法性验证(如2023-02-29应判定无效)
- 日期比较运算(>, <, ==)
- 日期增减(n天后的日期)
- 日期差计算(两个日期相隔天数)
- 星期计算(Zeller公式或基姆拉尔森公式)
2. 类设计与接口规划
2.1 成员变量设计
cpp复制class Date {
private:
int year;
int month;
int day;
// 辅助方法
bool isLeapYear() const;
int getMonthDays() const;
bool isValid() const;
public:
// 构造函数与接口
Date(int y, int m, int d);
void setDate(int y, int m, int d);
void print() const;
// 功能方法
Date operator+(int days) const;
Date operator-(int days) const;
int operator-(const Date& other) const;
bool operator>(const Date& other) const;
// ...其他重载运算符
};
关键点:将年份、月份、日设为private成员,避免外部直接修改导致数据不一致。所有修改操作都应通过setDate方法,该方法内部需调用isValid()进行校验。
2.2 日期验证逻辑
验证日期的三个层次:
- 基础范围检查:年>0,月∈[1,12],日∈[1,31]
- 月份天数检查:2月要考虑闰年(能被4整除但不能被100整除,或能被400整除)
- 特殊日期检查:如1582年10月5日-14日不存在(儒略历转格里历的过渡期,OJ题通常不要求)
cpp复制bool Date::isValid() const {
if(year <= 0 || month <= 0 || month > 12 || day <= 0)
return false;
int maxDay = 31;
if(month == 4 || month == 6 || month == 9 || month == 11) {
maxDay = 30;
} else if(month == 2) {
maxDay = isLeapYear() ? 29 : 28;
}
return day <= maxDay;
}
3. 核心算法实现
3.1 日期加减运算
处理日期增减的关键是将日期转换为"天数"这个统一维度:
- 将当前日期转换为从基准日(如0001-01-01)开始的总天数
- 对总天数进行加减操作
- 将新的总天数转换回年月日格式
cpp复制// 计算该日期是该年的第几天
int Date::getYearDay() const {
int days = day;
for(int m = 1; m < month; ++m) {
days += getMonthDays(m);
}
return days;
}
// 天数转日期
Date fromTotalDays(int totalDays) {
int y = 1, m = 1, d = 1;
// 逐年扣除
while(totalDays > (isLeapYear(y) ? 366 : 365)) {
totalDays -= isLeapYear(y) ? 366 : 365;
y++;
}
// 逐月扣除
while(totalDays > getMonthDays(y, m)) {
totalDays -= getMonthDays(y, m);
m++;
}
d = totalDays;
return Date(y, m, d);
}
3.2 日期差计算
采用同样的思路,将两个日期都转换为总天数后相减:
cpp复制int Date::operator-(const Date& other) const {
return this->toDays() - other.toDays();
}
3.3 星期计算
基姆拉尔森公式(Zeller公式的优化版):
cpp复制int Date::getWeekday() const {
if(month < 3) {
int tempM = month + 12;
int tempY = year - 1;
return (day + 2*tempM + 3*(tempM+1)/5 + tempY + tempY/4 - tempY/100 + tempY/400) % 7;
}
return (day + 2*month + 3*(month+1)/5 + year + year/4 - year/100 + year/400) % 7;
}
4. 边界情况处理
4.1 闰年测试用例
cpp复制void testLeapYear() {
assert(Date(2000,2,29).isValid()); // 能被400整除是闰年
assert(!Date(1900,2,29).isValid()); // 能被100整除不是闰年
assert(Date(2020,2,29).isValid()); // 能被4整除不是100倍数是闰年
assert(!Date(2021,2,29).isValid()); // 普通非闰年
}
4.2 跨年月日计算
cpp复制void testDateCalculation() {
Date d1(2023,12,31);
Date d2 = d1 + 1; // 应变为2024-01-01
assert(d2.getYear() == 2024 && d2.getMonth() == 1 && d2.getDay() == 1);
Date d3(2023,3,1);
Date d4 = d3 - 1; // 应变为2023-02-28
assert(d4.getMonth() == 2 && d4.getDay() == 28);
}
5. 性能优化技巧
- 预计算月份天数表:避免每次调用getMonthDays都计算闰年
cpp复制static const int monthDays[2][13] = {
{0,31,28,31,30,31,30,31,31,30,31,30,31}, // 平年
{0,31,29,31,30,31,30,31,31,30,31,30,31} // 闰年
};
- 缓存计算结果:对于频繁调用的toDays()结果可以缓存
cpp复制class Date {
private:
mutable int cachedTotalDays; // mutable允许const方法修改
mutable bool cacheValid;
void updateCache() const {
cachedTotalDays = calculateTotalDays();
cacheValid = true;
}
public:
int toDays() const {
if(!cacheValid) updateCache();
return cachedTotalDays;
}
};
6. 常见错误排查
- 日期增减时未考虑跨年:
cpp复制// 错误示例:仅增加天数不考虑月份进位
Date operator+(int days) {
day += days; // 当days很大时会出错
return *this;
}
- 比较运算符未全面比较:
cpp复制// 错误示例:只比较年或月
bool operator<(const Date& other) {
return year < other.year; // 应继续比较month和day
}
- 未处理负天数情况:
cpp复制Date operator-(int days) {
if(days < 0) return *this + (-days); // 处理负数情况
// ...正常减法逻辑
}
7. 扩展功能建议
- 国际化支持:
cpp复制enum DateFormat {ISO, USA, EURO};
void print(DateFormat fmt) const {
switch(fmt) {
case ISO: cout<<year<<"-"<<month<<"-"<<day; break;
case USA: cout<<month<<"/"<<day<<"/"<<year; break;
case EURO: cout<<day<<"."<<month<<"."<<year; break;
}
}
- 节假日计算:
cpp复制bool isHoliday() const {
// 计算农历节日或固定公历节日
if(month == 1 && day == 1) return true; // 元旦
// ...其他节日判断
}
- 日期解析:
cpp复制static Date fromString(const string& str) {
// 处理"2023-08-15"或"08/15/2023"等格式
char sep;
int y,m,d;
istringstream iss(str);
if(str.find('/') != string::npos) {
iss >> m >> sep >> d >> sep >> y; // 美国格式
} else {
iss >> y >> sep >> m >> sep >> d; // ISO格式
}
return Date(y,m,d);
}
实现日期类时最容易被忽视的是2月29日的处理。我曾在一个项目中遇到缓存过期问题,就是因为没有正确处理2000年(闰年)到2001年(平年)的转换。建议所有日期类实现完成后,至少测试以下边界案例:
- 闰年2月29日
- 一年的最后一天(12月31日)+1天
- 一月第一天(1月1日)-1天
- 日期差计算跨越闰年
- 1582年10月(儒略历转格里历的过渡期,视题目要求决定是否处理)