老问题的新答案。新答案的理由:该领域有更好的工具和技术。
这个答案大量使用了这个免费的、开源的仅标头库。我将从最高级别开始提出这个答案,并深入到较低级别的细节。但在任何时候,我们都不必进行详细的历法计算。 "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
是 type 的对象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
,它只不过是一个简单的 struct {year, month}
。所以这个函数简单地设置了一个循环,从 y 年的 1 月迭代到下一个 1 月,不包括下一个 1 月。这一切都非常可读。请注意,int
不允许使用“bare s”。一切都有一个非整数类型。
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
是一个chrono::time_point
基于,system_clock
精度为days
. date::format
可以采用任何精度system_clock
time_point
并使用类似 strftime 的格式化标志对其进行格式化。如图所示, Ayear_month_day
可以转换为a sys_days
。这是从{year, month, day}
字段类型到串行{count of days}
类型的转换。
os << " S M T W T F S\n";
显然打印出日历的星期几标题。
auto wd = unsigned{weekday{ym/1}};
查找该月第一天的星期几,并使用 encoding将其转换weekday
为。[注意: gcc 需要语法. 它不喜欢for 。——尾注]unsigned
[Sun == 0, Sat == 6]
unsigned(weekday{ym/1})
{}
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};
: 将月份中的日期转换为 anunsigned
并以 3 个空格的宽度 a 右对齐打印出来。
os << '\n';
打印一周后返回。
这就是程序的大部分内容!几乎所有棘手的日历逻辑都封装在这两行代码中:
auto wd = unsigned{weekday{ym/1}};
auto const e = (ym/last).day();
为了完整起见,这里是获取 currentdate::year
和 current的函数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();
}
这两个都只是将system_clock::time_point
返回的a 截断为usingsystem_clock::now()
的精度,然后将该 days-precision 转换为类型。然后,此类型具有获取器并选择所需的部分日历类型。days
floor
time_point
date::year_month_day
year
month
更新
好吧,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] 打印出月份中的日期。
为什么是无穷大?
因为不同的月份需要不同的行数,所以我希望能够告诉 ayear/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
获取本地化的工作日名称,并打印前 2 个字符。这(不幸的是)仅在这些是多字节字符时才大致正确,但这篇文章主要是关于日历,而不是 Unicode。locale
os
2.
打印出第一周,可能以空格为前缀。前缀的空格数是 3*(当月的第一天超过一周的第一天的天数)。然后追加天数,直到到达一周的最后一天。请注意,工作日减法始终以 7 为模,因此您不必担心星期几的底层编码。工作日形成一个圆形范围。这确实需要一些类似的东西,do-while
而不是传统for
的在一周内循环所有天时。
3 - infinity
. 啊,这是有趣的部分。
有一个类型 in "date.h"
calledyear_month_weekday
是一个类型 storage {year, month, weekday, unsigned}
。这是您指定母亲节的方式: 5 月的第二个星期日: sun[2]/may/2016
。这个表达式创建了一个结构{2016, 5, 0, 2}
(粗略地说)。因此,如果在switch
这里着陆,那么我们正在寻找本月和本年的 [第一个,最后一个] 星期日,确切的索引取决于line
,以及我们是否在第 2 行打印出星期日。
同样关键的是,这个库允许使用任何索引:
auto ymdw = ym/firstdow[index];
index
可能是 1,也可能是 57。上面的行编译并且不是运行时错误。
但是几个月不能有 57 个星期日(或星期一或其他)!
没问题。您可以询问是否ym/firstdow[index]
是有效日期。这就是下一行的作用:
if (ymdw.ok())
如果日期有效,那么您有工作要做。否则,您只需打印出一个空白行。
如果我们有工作要做,则将 转换year_month_weekday
为 ayear_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>
此处用于将 number of 转换为days
number of ,如果转换不准确,则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