Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Roman Numerals be printed instead of an int?

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?

like image 807
Dietmar Kühl Avatar asked Dec 11 '22 08:12

Dietmar Kühl


1 Answers

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).

like image 159
Dietmar Kühl Avatar answered Dec 28 '22 22:12

Dietmar Kühl