使用 C++20 Ranges 标准库实现日历程序
笔者在多年前看到过一篇文章,如今已经2025年了,C++的range库也更新了很多其他的东西。如今STL提供的组件已经足够用来输出日历了。
日历程序问题分解
首先来看效果图
January February March
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 6
3 4 5 6 7 8 9 7 8 9 10 11 12 13 7 8 9 10 11 12 13
10 11 12 13 14 15 16 14 15 16 17 18 19 20 14 15 16 17 18 19 20
17 18 19 20 21 22 23 21 22 23 24 25 26 27 21 22 23 24 25 26 27
24 25 26 27 28 29 30 28 28 29 30 31
31
April May June
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
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
July August September
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
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
October November December
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
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
不难发现,每个月的日历都包含了8行,其中月份和星期独占一行,而日期部分占了后6行,不足6行则使用空行补齐。
我们首先来看一个月:
January
Su Mo Tu We Th Fr Sa
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
当每个月的第一天不是周日的时候,我们需要在前面补空格,当每个月最后一天不是周六的时候,我们需要在后面补空格。
以一月份为例,我们前面有5天需要补空格,后面有6天需要补空格。我们可以定义如下序列:
[0,0,0,0,0,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,0,0,0,0,0,0]
我们使用0来代表空格,剩余的数字代表日期。
Sequence = concat(
prefix,
days,
suffix
)
于是有
auto RangeAsString = [](auto&& rg) static
{
auto ToString = [](int x) static
{
return x == 0 ? DoubleBlank : std::format("{:2} ", x);
};
return rg | transform(ToString) | join | std::ranges::to<std::string>();
};
auto CollectDays = [](std::chrono::year_month ym) static
{
auto total_days = DayofMonth(ym);
auto first_day = WeekdayOfDay(ym / std::chrono::day(1));
auto leading_zero = first_day.c_encoding();
auto prefix = repeat(0, leading_zero);
auto days = iota(1, total_days + 1);
auto suffix = repeat(0, 7 - (total_days + leading_zero) % 7);
auto title = Titles[ym.month().operator unsigned int() - 1];
auto row_number = leading_zero + total_days;
auto day_of_month = concat(prefix, days, suffix) | chunk(7) | transform(RangeAsString);
return concat(
single(title), // January
single(WeekName), // Su Mo Tu We Th Fr Sa
day_of_month, // 1, 2, 3, ...
repeat(EmptyLine, 6 - row_number / 7) // Empty line
);
};
这样我们每次调用CollectDays可以获得一个长度未8的序列,其中每个序列都是一个字符串。
CollectDays(std::chrono::year(2024) / std::chrono::January) | std::views::join_with('\n') | std::ranges::to<std::string>();
/*
January
Su Mo Tu We Th Fr Sa
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
*/
单个月的日期我们已经处理好了,接下来就是一整年的日期。
std::string Calendar(std::chrono::year y, int amount)
{
auto unpack = []<size_t... I>(std::index_sequence<I...>, std::chrono::year y) static
{
auto impl = [=](size_t index) {
return CollectDays(std::chrono::year_month(y, std::chrono::month(index + 1)));
};
return concat(single(impl(I))...);
};
return unpack(std::make_index_sequence<12>(), y)
| chunk(amount)
| transform(MergeChunk)
| join
| std::ranges::to<std::string>();
}
一年有12个月,所以我们需要产生一个长度为12的序列,然后我们每次展示三个。我们利用unpack产生12个序列,然后对于每个序列,我们使用single让其成为一个单独的元素,最后再使用concat就可以将这些序列合并了。unpack代码就等价于
concat(
single(CollectDays(year / 1))
single(CollectDays(year / 2))
single(CollectDays(year / 3))
...
single(CollectDays(year / 11))
single(CollectDays(year / 12))
);
接下来面临的问题是如何合并三个月的日期。
January February March
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 6
3 4 5 6 7 8 9 7 8 9 10 11 12 13 7 8 9 10 11 12 13
10 11 12 13 14 15 16 14 15 16 17 18 19 20 14 15 16 17 18 19 20
17 18 19 20 21 22 23 21 22 23 24 25 26 27 21 22 23 24 25 26 27
24 25 26 27 28 29 30 28 28 29 30 31
31
其实我们只需要把每一行的string合并一下即可,因为每个月的日期都固定占8行,我们只需要固定长度的循环就可以解决问题。
auto DereferenceAndAdvance = [](auto&& it) static { return *it++; };
std::generator<std::string> DereferenceAndAdvanceMultiIterator(auto&& blocks)
{
auto iterators = blocks | transform(std::ranges::begin) | std::ranges::to<std::vector>();
for (int i = 0; i < 8; ++i)
{
co_yield iterators
| transform(DereferenceAndAdvance)
| join_with(' ')
| std::ranges::to<std::string>();
}
}
auto MergeChunk = [](auto&& blocks) static
{
auto s = DereferenceAndAdvanceMultiIterator(blocks)
| join_with('\n')
| std::ranges::to<std::string>();
return s += '\n';
};
由于GCC14.2目前还没有实现concat,不过concat已经进入C++26标准了,笔者在这里简单实现了concat。
完整的代码。
浙公网安备 33010602011771号