旧问题的新答案。新答案的理由:该领域有更好的工具和技术。
这个答案大量使用了这个free, open-source header-only library。我将从最高级别开始提出这个答案,并深入到较低级别的细节。但在任何时候,我们都不必进行详细的历法计算。 "date.h" 为我们处理。
这里是main:
#include "date.h"
#include <iomanip>
#include <ostream>
#include <string>
#include <iostream>
int
main()
{
print_calendar_year(std::cout);
}
这只是为我输出:
January 2016
S M T W T F S
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
February 2016
S M T W T F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29
March 2016
S M T W T F S
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
April 2016
S M T W T F S
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
May 2016
S M T W T F S
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
June 2016
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
July 2016
S M T W T F S
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
August 2016
S M T W T F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
September 2016
S M T W T F S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30
October 2016
S M T W T F S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
November 2016
S M T W T F S
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
December 2016
S M T W T F S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
可以打印明年的日历:
using namespace date::literals;
print_calendar_year(std::cout, 2017_y);
我将从声明开始:
这是一个类型安全系统。
文字2017_y 是date::year 类型的对象,而不是简单的整数。具有表示year 和month 的类型意味着混淆这些概念的可能性要小得多。错误往往会在编译时被发现。
print_calendar_year 很简单:
void
print_calendar_year(std::ostream& os, date::year y = current_year())
{
using namespace date;
for (auto ym = y/jan; ym < y/jan + years{1}; ym += months{1})
{
print_calendar_month(os, ym);
os << '\n';
}
}
表达式year/month 创建了一个名为date::year_month 的类型,它只不过是一个简单的结构{year, month}。所以这个函数简单地设置了一个循环,从 y 年的 1 月迭代到下一个 1 月,不包括下一个 1 月。这一切都非常可读。请注意,不允许使用“bare ints”。一切都有一个非整数类型。
print_calendar_month 是橡胶与道路的交汇处:
void
print_calendar_month(std::ostream& os, date::year_month ym = current_year_month())
{
using namespace std;
using namespace date;
os << format("%B %Y\n", sys_days{ym/1});
os << " S M T W T F S\n";
auto wd = unsigned{weekday{ym/1}};
os << string(wd*3, ' ');
auto const e = (ym/last).day();
for (day d = 1_d; d <= e; wd = 0)
{
for (; wd < 7 && d <= e; ++wd, ++d)
os << setw(3) << unsigned{d};
os << '\n';
}
}
os << format("%B %Y\n", sys_days{ym/1}); 是打印每个月的标题(例如January 2016)。这些是类似 strftime 的格式化标志,将尊重当前全局 std::locale 的本地化设置(操作系统支持的程度)。
子表达式ym/1 创建一个类型date::year_month_day,它代表指定月份和年份的第一天。 date::year_month_day 是一个持有{year, month, day} 的简单类。
sys_days 是基于system_clock 的chrono::time_point,精度为days。 date::format 可以采用任何精度 system_clock time_point 并使用类似 strftime 的格式化标志对其进行格式化。如图所示,year_month_day 可以转换为sys_days。这是从{year, month, day} 字段类型到串行{count of days} 类型的转换。
os << " S M T W T F S\n"; 显然会打印出日历的星期几标题。
auto wd = unsigned{weekday{ym/1}}; 查找该月第一天的星期几,并使用编码[Sun == 0, Sat == 6] 将weekday 转换为unsigned。 [注意: gcc 需要语法unsigned(weekday{ym/1})。它不喜欢{} for unsigned。 — 尾注]
os << string(wd*3, ' '); 只是在每月第一天之前的每一天打印出 3 个空格来填充第一行。
auto const e = (ym/last).day(); 是 date::day 类型的常量,它等于今年和月份组合的月份的最后一天。
for (day d = 1_d; d <= e; wd = 0)
从第 1 天循环开始,直到该月的最后一天(含),并在每次迭代时将 unsigned wd 设置回星期日的编码。
for (; wd < 7 && d <= e; ++wd, ++d):直到你到达周末或月末,增加一周中的某一天和一个月中的某一天。
os << setw(3) << unsigned{d};:将月份中的日期转换为unsigned,并以 3 个空格的宽度右对齐打印出来。
os << '\n';打印一周后返回。
这就是程序的大部分内容!几乎所有棘手的日历逻辑都封装在这两行代码中:
auto wd = unsigned{weekday{ym/1}};
auto const e = (ym/last).day();
为了完整起见,这里是获取当前date::year 和当前date::year_month 的函数:
date::year_month
current_year_month()
{
using namespace std::chrono;
using namespace date;
year_month_day ymd = floor<days>(system_clock::now());
return ymd.year()/ymd.month();
}
date::year
current_year()
{
using namespace std::chrono;
using namespace date;
year_month_day ymd = floor<days>(system_clock::now());
return ymd.year();
}
这两个都只是使用floor将从system_clock::now()返回的system_clock::time_point截断为days的精度,然后将天数精度time_point转换为date::year_month_day类型。然后这种类型有 year 和 month 的 getter 来挑选所需的部分日历类型。
更新
好吧,TemplateRex 在下面问了一个我一开始不想回答的问题,然后我忍不住了,因为答案突出了"date.h" 的强大功能。 ;-)
问题是:
你能像这样把日历打印成 3x4 格式吗?
January 2016 February 2016 March 2016
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 1 2 3 4 5 6 1 2 3 4 5
3 4 5 6 7 8 9 7 8 9 10 11 12 13 6 7 8 9 10 11 12
10 11 12 13 14 15 16 14 15 16 17 18 19 20 13 14 15 16 17 18 19
17 18 19 20 21 22 23 21 22 23 24 25 26 27 20 21 22 23 24 25 26
24 25 26 27 28 29 30 28 29 27 28 29 30 31
31
April 2016 May 2016 June 2016
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 1 2 3 4 5 6 7 1 2 3 4
3 4 5 6 7 8 9 8 9 10 11 12 13 14 5 6 7 8 9 10 11
10 11 12 13 14 15 16 15 16 17 18 19 20 21 12 13 14 15 16 17 18
17 18 19 20 21 22 23 22 23 24 25 26 27 28 19 20 21 22 23 24 25
24 25 26 27 28 29 30 29 30 31 26 27 28 29 30
July 2016 August 2016 September 2016
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 1 2 3 4 5 6 1 2 3
3 4 5 6 7 8 9 7 8 9 10 11 12 13 4 5 6 7 8 9 10
10 11 12 13 14 15 16 14 15 16 17 18 19 20 11 12 13 14 15 16 17
17 18 19 20 21 22 23 21 22 23 24 25 26 27 18 19 20 21 22 23 24
24 25 26 27 28 29 30 28 29 30 31 25 26 27 28 29 30
31
October 2016 November 2016 December 2016
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 1 2 3 4 5 1 2 3
2 3 4 5 6 7 8 6 7 8 9 10 11 12 4 5 6 7 8 9 10
9 10 11 12 13 14 15 13 14 15 16 17 18 19 11 12 13 14 15 16 17
16 17 18 19 20 21 22 20 21 22 23 24 25 26 18 19 20 21 22 23 24
23 24 25 26 27 28 29 27 28 29 30 25 26 27 28 29 30 31
30 31
显然如此,因为我不打算手动输入以上所有内容! ;-)
它需要重写 print_calendar_year 并引入一些新功能,最值得注意的是:
void
print_line_of_calendar_month(std::ostream& os, date::year_month ym, unsigned line,
date::weekday firstdow);
此函数仅打印与year_month ym 关联的日历的一行,并且是这种 3x4 格式的核心。
我还认为让这个程序可本地化会很有趣,这样可以打印出所需的星期几,以及月份和星期几的本地化名称(与std::locale 在您的平台上允许)。
这些行编号为 [0, infinity]。第 0 行打印出月份年份,例如 January 2016。第 1 行打印出星期标题:Su Mo Tu We Th Fr Sa。然后第 [2, infinity] 行打印出月份中的日期。
为什么是无穷大?
因为不同的月份需要不同的行数,所以我希望能够告诉year/month 打印下一行,即使它不需要(因为本季度的另一个月需要它)。因此,当您要求日历打印出不需要的行时,它只会输出正确数量的 ' ' 以用于填充目的。
足够的介绍,这是功能:
void
print_line_of_calendar_month(std::ostream& os, date::year_month ym, unsigned line,
date::weekday firstdow)
{
using namespace std;
using namespace date;
switch (line)
{
case 0:
os << left << setw(21) << format(os.getloc(), " %B %Y", sys_days{ym/1}) << right;
break;
case 1:
{
auto sd = sys_days{ym/firstdow[1]};
for (auto const esd = sd + weeks{1}; sd < esd; sd += days{1})
{
auto d = format(os.getloc(), "%a", sd);
d.resize(2);
os << ' ' << d;
}
break;
}
case 2:
{
auto wd = weekday{ym/1}; // This line and the next are the "offset"
os << string((wd-firstdow).count()*3, ' '); // referred to in the question.
auto d = 1_d;
do
{
os << setw(3) << unsigned(d);
++d;
} while (++wd != firstdow);
break;
}
default:
{
unsigned index = line - 2;
auto sd = sys_days{ym/1};
if (weekday{sd} == firstdow)
++index;
auto ymdw = ym/firstdow[index];
if (ymdw.ok())
{
auto d = year_month_day{ymdw}.day();
auto const e = (ym/last).day();
auto wd = firstdow;
do
{
os << setw(3) << unsigned(d);
} while (++wd != firstdow && ++d <= e);
os << string((firstdow-wd).count()*3, ' ');
}
else
os << string(21, ' ');
break;
}
}
}
所以打开行号[0, infinity],并且对于每个行号,做正确的事:
0. 打印出月份年份标题。
这将传递给format locale 的os 以获取本地化月份名称。
1. 打印出星期几标题。
这将传递给format 和os 的locale 以获取本地化的工作日名称,并打印前2 个字符。这(不幸的是)仅在这些是多字节字符时才大致正确,但这篇文章主要是关于日历,而不是 Unicode。
2. 打印出第一周,可能以空格为前缀。前缀的空格数是 3*(当月的第一天超过一周的第一天的天数)。然后追加天数,直到到达一周的最后一天。请注意,工作日减法始终以 7 为模,因此您不必担心星期几的底层编码。工作日形成一个圆形范围。这确实需要类似于 do-while 的内容,而不是传统的 for 在一周内循环所有天时。
3 - infinity。啊,这是有趣的部分。
"date.h" 中有一个类型叫做year_month_weekday,它是一个存储{year, month, weekday, unsigned} 的类型。您可以通过以下方式指定母亲节:5 月的第二个星期日:sun[2]/may/2016。这个表达式创建了一个结构{2016, 5, 0, 2}(粗略地说)。因此,如果switch 出现在此处,那么我们正在寻找本月和本年的[第一个,最后一个] 星期日,其中确切的索引取决于line,以及我们是否在线打印了星期日2.
另外,这个库允许使用 any 索引:
auto ymdw = ym/firstdow[index];
index 可以是 1,也可以是 57。以上行编译,不是运行时错误。
但几个月不能有 57 个星期日(或星期一或其他)!
没问题。您可以询问ym/firstdow[index] 是否为有效日期。这就是下一行的作用:
if (ymdw.ok())
如果日期有效,那么您有工作要做。否则,您只需打印出一个空白行。
如果我们有工作要做,请将 year_month_weekday 转换为 year_month_day,以便您可以从中获取月份中的日期 (d)。并找到该月的最后一天:
auto const e = (ym/last).day();
然后从一周的第一天迭代到先到者:月末或一周的最后一天。打印出每个地点的月份日期。然后,如果您没有在一周的最后一天结束,则打印空格以填充到一周的最后一天。
我们已经完成了print_line_of_calendar_month!请注意,在此级别上从未输出换行符。在这个级别上甚至没有输出月间填充。每个日历的宽度正好是 21char,并且可以打印到任意行数。
现在我们需要另一个小工具:一个日历月在开始填充空白行之前需要多少行?
unsigned
number_of_lines_calendar(date::year_month ym, date::weekday firstdow)
{
using namespace date;
return ceil<weeks>((weekday{ym/1} - firstdow) +
((ym/last).day() - day{0})).count() + 2;
}
这是一个月的天数,加上从一周的第一天到本月的第一天的天数,再加上 2 行用于星期几标题和年份 -月标题。最后的小数周被四舍五入!
注意事项:
从一周的第一天到每月的第一天的天数就是:(weekday{ym/1} - firstdow)。
每月的天数在这里编码为((ym/last).day() - day{0})。请注意,day{0} 不是有效的day,但在减法中仍然有用:day - day 给出了chrono::duration days 的结果。另一种说法是((ym/last).day() - day{1} + days{1})。
请注意,此处使用ceil<weeks> 将days 的数量转换为weeks 的数量,如果转换不准确,则向上舍入到下一个weeks。 1 周 == 1 行。此汇总说明在一周的最后一天之前结束的最后一周。
现在print_calendar_year 可以用这些原语重写:
void
print_calendar_year(std::ostream& os, unsigned const cols = 3,
date::year const y = current_year(),
date::weekday const firstdow = date::sun)
{
using namespace date;
if (cols == 0 || 12 % cols != 0)
throw std::runtime_error("The number of columns " + std::to_string(cols)
+ " must be one of [1, 2, 3, 4, 6, 12]");
// Compute number of lines needed for each calendar month
unsigned ml[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
for (auto& m : ml)
m = number_of_lines_calendar(y/month{m}, firstdow);
for (auto r = 0u; r < 12/cols; ++r) // for each row
{
const auto lines = *std::max_element(std::begin(ml) + (r*cols),
std::begin(ml) + ((r+1)*cols));
for (auto l = 0u; l < lines; ++l) // for each line
{
for (auto c = 0u; c < cols; ++c) // for each column
{
if (c != 0)
os << " ";
print_line_of_calendar_month(os, y/month{r*cols + c+1}, l, firstdow);
}
os << '\n';
}
os << '\n';
}
}
首先计算每个月需要多少行:
// Compute number of lines needed for each calendar month
unsigned ml[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
for (auto& m : ml)
m = number_of_lines_calendar(y/month{m}, firstdow);
然后对于每个“日历行”,通过搜索 ml 的真子集来找到该行所需的行数。
然后对于每一行,对于每个“日历列”,打印出该列对应日历月的行。
在每一行之后打印一个'\n'。
在每个日历行之后,打印一个'\n'。
请注意,我们仍然不需要深入研究历法算术。在这个级别,我们需要知道“每周 7 天”、“每天 3 个空格”和“每个日历行 12/cols 月”。
在 macOS 上此驱动程序:
using namespace date::literals;
std::cout.imbue(std::locale("de_DE"));
print_calendar_year(std::cout, 3, 2016_y, mon);
输出:
Januar 2016 Februar 2016 März 2016
Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So
1 2 3 1 2 3 4 5 6 7 1 2 3 4 5 6
4 5 6 7 8 9 10 8 9 10 11 12 13 14 7 8 9 10 11 12 13
11 12 13 14 15 16 17 15 16 17 18 19 20 21 14 15 16 17 18 19 20
18 19 20 21 22 23 24 22 23 24 25 26 27 28 21 22 23 24 25 26 27
25 26 27 28 29 30 31 29 28 29 30 31
April 2016 Mai 2016 Juni 2016
Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So
1 2 3 1 1 2 3 4 5
4 5 6 7 8 9 10 2 3 4 5 6 7 8 6 7 8 9 10 11 12
11 12 13 14 15 16 17 9 10 11 12 13 14 15 13 14 15 16 17 18 19
18 19 20 21 22 23 24 16 17 18 19 20 21 22 20 21 22 23 24 25 26
25 26 27 28 29 30 23 24 25 26 27 28 29 27 28 29 30
30 31
Juli 2016 August 2016 September 2016
Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So
1 2 3 1 2 3 4 5 6 7 1 2 3 4
4 5 6 7 8 9 10 8 9 10 11 12 13 14 5 6 7 8 9 10 11
11 12 13 14 15 16 17 15 16 17 18 19 20 21 12 13 14 15 16 17 18
18 19 20 21 22 23 24 22 23 24 25 26 27 28 19 20 21 22 23 24 25
25 26 27 28 29 30 31 29 30 31 26 27 28 29 30
Oktober 2016 November 2016 Dezember 2016
Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So Mo Di Mi Do Fr Sa So
1 2 1 2 3 4 5 6 1 2 3 4
3 4 5 6 7 8 9 7 8 9 10 11 12 13 5 6 7 8 9 10 11
10 11 12 13 14 15 16 14 15 16 17 18 19 20 12 13 14 15 16 17 18
17 18 19 20 21 22 23 21 22 23 24 25 26 27 19 20 21 22 23 24 25
24 25 26 27 28 29 30 28 29 30 26 27 28 29 30 31
31
您的标准可能会因您的 std::lib/OS 支持本地化的程度而异。但是现在您可以使用任何年份,使用任何年份,使用一周中的任何一天作为第一天,以月份列打印日历周,并使用任何语言环境(对语言环境的模操作系统支持)。
这是print_calendar_year(std::cout, 4, 2017_y); 的输出
January 2017 February 2017 March 2017 April 2017
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 7 1 2 3 4 1 2 3 4 1
8 9 10 11 12 13 14 5 6 7 8 9 10 11 5 6 7 8 9 10 11 2 3 4 5 6 7 8
15 16 17 18 19 20 21 12 13 14 15 16 17 18 12 13 14 15 16 17 18 9 10 11 12 13 14 15
22 23 24 25 26 27 28 19 20 21 22 23 24 25 19 20 21 22 23 24 25 16 17 18 19 20 21 22
29 30 31 26 27 28 26 27 28 29 30 31 23 24 25 26 27 28 29
30
May 2017 June 2017 July 2017 August 2017
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 1 2 3 1 1 2 3 4 5
7 8 9 10 11 12 13 4 5 6 7 8 9 10 2 3 4 5 6 7 8 6 7 8 9 10 11 12
14 15 16 17 18 19 20 11 12 13 14 15 16 17 9 10 11 12 13 14 15 13 14 15 16 17 18 19
21 22 23 24 25 26 27 18 19 20 21 22 23 24 16 17 18 19 20 21 22 20 21 22 23 24 25 26
28 29 30 31 25 26 27 28 29 30 23 24 25 26 27 28 29 27 28 29 30 31
30 31
September 2017 October 2017 November 2017 December 2017
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 1 2 3 4 5 6 7 1 2 3 4 1 2
3 4 5 6 7 8 9 8 9 10 11 12 13 14 5 6 7 8 9 10 11 3 4 5 6 7 8 9
10 11 12 13 14 15 16 15 16 17 18 19 20 21 12 13 14 15 16 17 18 10 11 12 13 14 15 16
17 18 19 20 21 22 23 22 23 24 25 26 27 28 19 20 21 22 23 24 25 17 18 19 20 21 22 23
24 25 26 27 28 29 30 29 30 31 26 27 28 29 30 24 25 26 27 28 29 30
31