可以打印罗马数字而不是int吗?

Die*_*ühl 4 c++ locale iostream roman-numerals

我有一些代码打印一些小数字(实际上是几年),请求是将数字打印为罗马数字,而不是使用通常的印度 - 阿拉伯数字:

int main() {
    // do something to make all integers appear in Roman numerals
    std::cout << "In the year " << 2013 << " the following output was generated:\n";
    // ...
}
Run Code Online (Sandbox Code Playgroud)

ints 格式化为罗马数字可以做些什么?

Die*_*ühl 13

该问题有两个独立的部分:

  1. 这个问题的无聊部分是如何int使用罗马表示的值将变换为一系列字符.
  2. 如何拦截输出int并将其转换为刚刚描述的序列.

罗马数字遵循一个相当直接的规则,似乎用简单的查找表处理最简单.由于问题的主要焦点是如何使其与IOStream一起使用,因此使用了直接算法:

template <typename To>
To make_roman(int value, To to) {
    if (value < 1 || 3999 < value) {
        throw std::range_error("int out of range for a Roman numeral");
    }
    static std::string const digits[4][10] = {
        { "", "M", "MM", "MMM", "", "", "", "", "", "" },
        { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" },
        { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" },
        { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" },
    };
    for (int i(0), factor(1000); i != 4; ++i, factor /= 10) {
        std::string const& s(digits[i][(value / factor) % 10]);
        to = std::copy(s.begin(), s.end(), to);
    }
    return to;
}
Run Code Online (Sandbox Code Playgroud)

通过查找相应的字符串并将其复制到迭代器,可以简单地生成每个"数字".如果整数超出了可以使用罗马数字表示的值的范围,则抛出异常.可生产的最长字符串长度为15个字符(3888).

下一步是设置为使用上述转换std::cout格式化ints.当std::ostream需要转换任何内置数值类型(整数,浮点)或类型bool和时void const*,它std::num_put<cT>从流中获取facet std::locale并调用put()对象,主要是使用

std::use_facet<std::num_put<cT>>(s.getloc())
    .put(std::ostreambuf_iterator<char>(s), s, s.fill(), value);
Run Code Online (Sandbox Code Playgroud)

通过从带有as参数的版本派生std::num_put<char>和覆盖do_put()成员函数,long可以更改数字的格式:

class num_put
    : public std::num_put<char>
{
    iter_type do_put(iter_type to, std::ios_base& fmt, char fill, long v) const {
        char buffer[16];
        char* end(make_roman(v, buffer));

        std::streamsize len(end - buffer);
        std::streamsize width(std::max(fmt.width(0), len));
        std::streamsize fc(width - (end - buffer));

        switch (fmt.flags() & std::ios_base::adjustfield) {
        default:
        case std::ios_base::left:
            to = std::copy(buffer, end, to);
            to = std::fill_n(to, fc, fill);
            break;
        case std::ios_base::right:
        case std::ios_base::internal:
            to = std::fill_n(to, fc, fill);
            to = std::copy(buffer, end, to);
        }
        return to;
    }
};
Run Code Online (Sandbox Code Playgroud)

虽然功能相对较长,但相当直接:

  1. 该值v将转换为罗马数字的字符串并存储在其中buffer.
  2. 确定结果字符串的长度和要生成的字符数(并将流width()重置为0).
  3. 根据输出对齐的位置,复制值,然后复制填充字符(如果有),或者相反.

剩下的就是std::locale使用这个版本的std::num_put<char>facet 创建一个并将结果安装std::localestd::cout:

std::cout.imbue(std::locale(std::cout.getloc(), new num_put));
std::cout << "year " << 2013 << '\n';
Run Code Online (Sandbox Code Playgroud)

是一个实时示例,显示了具有不同对齐的几个不同值.该示例还实现的所有四个整数版本do_put()(即,对于long,long long,unsigned long,和unsigned long long).