There are two separate parts to the question:
- The boring part of the question is how to transform an
int
into a sequence of characters with the Roman representation of the value. - 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 int
s 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:
- The value
v
is converted into a string for the Roman numeral and stored inbuffer
. - The length of the result string and the number of characters to be produced are determined (and the stream's
width()
is reset to0
). - 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
).