4

I have some code which prints some small numbers (actually years) and the request is to have the numbers printed as Roman numerals instead of using the usual Hindu-Arabic 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";
    // ...
}

What can be done to format ints as Roman numerals?

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380

1 Answers1

13

There are two separate parts to the question:

  1. The boring part of the question is how to transform an int into a sequence of characters with the Roman representation of the value.
  2. How to intercept the output of the int and turn it into the sequence just described.

The Roman numerals follow a fairly straight forward rule which seems to be handled easiest with a simple look-up table. Since the main focus of the question is on how to make it work with IOStreams, a straight forward algorithm is used:

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;
}

Each "digit" is simply produced by looking up the corresponding string and copying it to an iterator. If the integer is out of range for the value which can be represented using Roman numerals an exception is thrown. The longest string which can be produced is 15 characters long (3888).

The next step is to setup std::cout such that it formats ints using the above conversion. When an std::ostream needs to convert any of the built-in numeric types (integers, floating points), or the types bool and void const*, it obtains the std::num_put<cT> facet from the stream's std::locale and calls put() on the object, essentially using

std::use_facet<std::num_put<cT>>(s.getloc())
    .put(std::ostreambuf_iterator<char>(s), s, s.fill(), value);

By deriving from std::num_put<char> and overriding the do_put() member function for the version taking a long as argument, the formatting of the numbers can be changed:

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;
    }
};

Although the function is relatively long it is fairly straight forward:

  1. The value v is converted into a string for the Roman numeral and stored in buffer.
  2. The length of the result string and the number of characters to be produced are determined (and the stream's width() is reset to 0).
  3. Depending on where the output is aligned, either the value is copied followed by the fill characters (if any) being store or the other way around.

What is remaining is to create a std::locale using this version of the std::num_put<char> facet and to install the resulting std::locale into std::cout:

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

Here is a live example showing a couple different values with different alignments. The example also implements all four integer version of do_put() (i.e., for long, long long, unsigned long, and unsigned long long).

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • @chris: I have written different methods in the past (but not any in the recent past) but this one is entirely written from scratch based on the [Wikipedia article](http://en.wikipedia.org/wiki/Roman_numerals). – Dietmar Kühl Dec 27 '13 at 02:27
  • Do you have to write this entire thing over ad verbatim for the `unsigned long` overload? – David G Dec 27 '13 at 02:29
  • @DietmarKühl, Ah, I looked for a bit and found [this isocpp RSS feed](https://dl.dropboxusercontent.com/u/11033193/C%2B%2BTDD_roman_numerals.pdf). I'll bet I was mixing that with some completely different IO streams code I've seen recently. Sorry about that. – chris Dec 27 '13 at 02:30
  • @0x499602D2: if you want to also support the other integer types, you'd probably create an internal version with the basic logic, check the boundaries, and delegate to the internal version. – Dietmar Kühl Dec 27 '13 at 02:31
  • @chris: after you mentioned it I searched for similar things and I found [this](http://www.daniweb.com/software-development/cpp/threads/381687/c-integer-to-roman-numeral-conversion) which is somewhat similar. I couldn't be bothered to write the expressions four times, though, and I don't think the code is actually working. – Dietmar Kühl Dec 27 '13 at 02:36
  • 1
    @0x499602D2: I have updated the [example](http://ideone.com/GNQguT) to override all four versions of `do_put()` which are relevant. – Dietmar Kühl Dec 27 '13 at 02:48
  • I just thought of something: It would be even better to create a manipulator `roman_numeral` so that it would be easier for the user to enable this different formatting rule. This also necessitates the creation of a `noroman_numeral` manipulator to disable it. So you will have to create and check the `iword` value before using the custom functionality of the facet. – David G Dec 27 '13 at 14:06
  • The awful thing about replacing `num_put` is that you have to duplicate the stream's locale substituting the new `num_put`, and then `imbue` the new locale into the stream. But there isn't a way around this without a complete redesign of streams from the bottom up. – Spencer Jun 22 '23 at 14:23