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 int
s as Roman numerals?
There are two separate parts to the question:
int
into a sequence of characters with the Roman representation of the value.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:
v
is converted into a string for the Roman numeral and stored in buffer
.width()
is reset to 0
).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
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With