#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <cassert>
class Date {
public:
Date(int year, int month, int day) : year_(year), month_(month), day_(day) {}
static long getDaysSince1970(int year, int month) {
int a = (14 - month) / 12;
int y = year + 4800 - a;
int m = month + 12 * a - 3;
return (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045;
}
private:
int year_, month_, day_;
};
class Month {
public:
Month(int year, int month) : year_(year), month_(month) {
assert(month >= 1 && month <= 12);
assert(year >= 1970 && year <= 65535);
long this_month_beg = Date::getDaysSince1970(year, month);
long next_month_beg = Date::getDaysSince1970(year + (month == 12), month % 12 + 1);
days_in_month_ = next_month_beg - this_month_beg;
first_day_of_week_ = (this_month_beg + 1) % 7;
}
int getDaysInMonth() const { return days_in_month_; }
int getFirstDayOfWeek() const { return first_day_of_week_; }
int getYear() const { return year_; }
int getMonth() const { return month_; }
private:
int year_, month_, days_in_month_, first_day_of_week_;
};
class Calendar {
public:
Calendar(int year) : year_(year) {
for (int i = 1; i <= 12; ++i) {
months_.emplace_back(year, i);
}
}
void print() const {
static const std::vector<std::string> month_names = {
"一月", "二月", "三月", "四月", "五月", "六月",
"七月", "八月", "九月", "十月", "十一月", "十二月"
};
static const std::vector<std::string> weekdays = {
"日", "一", "二", "三", "四", "五", "六"
};
for (int i = 0; i < 12; i += 3) {
// 打印月份名称
for (int j = 0; j < 3; ++j) {
std::cout << std::setw(20) << std::left << month_names[i + j];
if (j < 2) std::cout << " ";
}
std::cout << "\n";
// 打印星期
for (int j = 0; j < 3; ++j) {
for (const auto& day : weekdays) {
std::cout << std::setw(3) << day;
}
if (j < 2) std::cout << " ";
}
std::cout << "\n";
// 打印日期
std::vector<std::vector<std::string>> month_grids(3, std::vector<std::string>(6, ""));
for (int j = 0; j < 3; ++j) {
const Month& month = months_[i + j];
int day = 1;
for (int week = 0; week < 6; ++week) {
for (int weekday = 0; weekday < 7; ++weekday) {
if (week == 0 && weekday < month.getFirstDayOfWeek()) {
month_grids[j][week] += " ";
} else if (day <= month.getDaysInMonth()) {
month_grids[j][week] += std::to_string(day).length() < 2 ? " " + std::to_string(day) : " " + std::to_string(day);
++day;
} else {
month_grids[j][week] += " ";
}
}
}
}
for (int week = 0; week < 6; ++week) {
for (int j = 0; j < 3; ++j) {
std::cout << month_grids[j][week];
if (j < 2) std::cout << " ";
}
std::cout << "\n";
}
std::cout << "\n";
}
}
private:
int year_;
std::vector<Month> months_;
};
int main() {
Calendar calendar(2024);
calendar.print();
return 0;
}
Date::getDaysSince1970
方法是算法的核心,它使用了一个改进的 Zeller's congruence 算法来计算从 1970 年 1 月 1 日到指定日期的天数。这个算法允许我们:- 确定每个月的第一天是星期几
- 计算每个月有多少天
- 月份表示:
Month
类封装了每个月的关键信息:- 年份和月份
- 该月的天数
- 该月第一天是星期几
- 日历生成:
Calendar
类的print
方法实现了日历的生成和打印:- 按季度(每三个月一组)生成日历
- 使用二维网格表示每个月的日期
- 输出格式化: 算法通过以下步骤格式化输出:
- 打印月份名称
- 打印星期头
- 使用嵌套循环填充日期网格
- 对齐和格式化输出以创建美观的布局
- 闰年处理: 算法自动处理闰年,因为
getDaysSince1970
方法在计算天数时考虑了闰年。 - 模块化设计: 通过将功能分散到不同的类中,算法实现了关注点分离,使得代码更易于理解和维护。
- 使用数学公式精确计算日期,避免硬编码或查表。
- 将日历看作是由多个月组成的二维网格。
- 先生成每个月的数据,然后按照预定的布局格式化输出。
上面的代码中最重要的是Zeller's congruence 算法
Zeller's congruence 是由德国数学家 Christian Zeller 在 19 世纪发明的一种计算任何公历日期对应星期几的算法。这个算法非常有用,因为它可以快速计算出任何日期是星期几,而不需要查表或进行复杂的迭代。
算法的基本公式如下:
h = (q + 13(m+1)/5 + K + K/4 + J/4 + 5J) mod 7
其中:
- h 是星期几(0 = 星期六,1 = 星期日,2 = 星期一,...,6 = 星期五)
- q 是日期
- m 是月份(3 = 三月,4 = 四月,...,14 = 二月)
- K 是年份的后两位数字
- J 是年份的前两位数字
注意:在这个公式中,一月和二月被当作前一年的 13 月和 14 月。
让我们详细解释一下这个算法的思想和步骤:
- 月份调整:
- 将一月和二月视为上一年的 13 月和 14 月。这是为了处理跨年的情况,特别是在处理闰年时。
- 年份分割:
- 将年份分为世纪(J)和年(K)两部分。例如,1995 年中 J = 19,K = 95。
- 数学计算:
- 13(m+1)/5:这部分与月份有关,用于调整不同月份的起始日偏移。
- K + K/4:考虑了普通闰年(每 4 年一次)。
- J/4:考虑了世纪闰年(每 400 年一次,而不是每 100 年)。
- 5J:这是一个调整因子,用于使公式在不同世纪都能正确工作。
- 模运算:
- 最后对 7 取模,得到 0-6 的结果,对应星期六到星期五。