Channels ▼
RSS

C/C++

A Simple Function for Formatting Currency


Programmers are often called upon to present numeric quantities formatted as money. Conceptually, this is not a tough job. It's a simple matter of generating a formatted string that corresponds to a value stored in a numeric variable.

There are complexities, of course. For example, sometimes the fractional part is truncated and sometimes it is rounded according to specific rules. There are many international currency types and many different symbols to represent them. You can't even be sure how many decimal places to use. European currencies are customarily formatted with two decimal places, but many Arab currencies require three decimals for fractional parts.

In America, commas group digits in threes and the dot is used to separate whole dollars from cents. In much of Europe, the punctuation symbols are reversed. But that's just the beginning. In India, digits are grouped in pairs, and sometimes in trios. The user who sees Rs3,25,84,729.25 on an e-banking display screen has no trouble parsing the amount as 3 crores, 25 lakhs, 84,729 rupees, and 25 paise.

Programmers must also contend with different ways of representing negative amounts and even the placement of currency symbols before or after numeric digits — with or without a whitespace buffer.

Clearly, programmers who deal with multiple currencies must be prepared to contend with substantial complexity. In the C and C++ world, they logically start with locales. Locales are not to my taste, however. My experience is that they lead to low-level solutions that can be hard to write, read, debug, and maintain. That's what inspired me to search for an alternate solution to the money-formatting problem.

Learning from the Past

The first programming languages I learned were Fortran and Cobol. I frequently used picture clauses in my programs. They are simple to define and use, and they provide many formatting options. Picture clauses have worked well for decades, but for some reason, they are not part of the C++ or C library. I decided to base my C/C++ currency-formatting function on Cobol's picture clauses.

The first challenge I faced was to choose a name for my function. I settled on mnyfmt, which is a sort of tribute to strlen. Like strlen, mnyfmt is a six-letter name that starts by specifying the object the function acts upon, then describes the action it performs.

Next, I had to figure out how to round numbers. I discovered that truncation is one of more than a dozen options. I learned that bankers use a special rounding scheme called "round half to even," in which rounding goes to the closer integer number, but halves always round to an even number. For example, -12.63 rounds to -13 and -12.50 rounds to -12, but +13.50 rounds to 14.

Handling so many rounding schemes is overwhelming…so I decided to decide to set the problem aside. My function does not accept a floating-point value as an argument, but instead accepts two integer parameters. One is the integer part of the number to format, and the other is the fractional part. Programmers must do their own rounding before calling my function. The invocation reads mnyfmt(12,50) instead of mnyfmt(12.50) — note the comma that separates the two numbers. That's not a decimal point.

My first implementation was a C++ template function that received a std::string argument. However, I discovered that I was not using any templates, nor any std::string functionality, so I replaced the C++ std::string with a regular zero-terminated character array. I also added a parameter to mark the fractional separator. In my test programs, I used commas and dots, but there are situations where other separators could be useful.

At this point, the function prototype looked like this:

char* mnyfmt(char *fmtstr, char dec, long intpart, int fractpart);
  {
    { // test.overwrite
      typedef struct struct_overwrite
        {
          char bytes_8[8]; // 64 bits: probably aligned
          int int_neg; // -1 usually has all its bits equal to 1
        } overwrite;
      overwrite o = {'1','2','3','4','5','6','7','\0',-1};
      assertTrue(8-1==strlen(o.bytes_8) && o.int_neg == -1);
      strcpy(o.bytes_8, "1234567.."); // 2 more bytes...
      assertTrue(9==strlen(o.bytes_8));
      assertFalse(o.int_neg == -1 && "Adjacent memory overwritten ");
      assertTrue(o.int_neg != -1);
      assertTrue(CHAR_BIT == 8 && "8 bits bytes required");
    }
  }

I decided to use only one char* argument because I know that C strings are problematic when dealing with limited memory. For example, field bytes_8 can hold only eight characters. When 10 are copied into it, the last two overwrite whatever values are stored after bytes_8 — in this case, changing the value of the field int_neg. This type of error is particularly difficult to catch.

Refining the Function

When the string that contains the picture clause is also the place where the formatted value will reside, the programmer needs to ensure that this variable is large enough to hold the resulting value. For most applications, a string of 96 or 128 bytes will be big enough. For what it's worth, you can represent the U.S. national debt of $14 trillion with just 16 digits, including two for the cents.

Best practices call for the construction of tests along with code. That's why my code is peppered with assertTrue and assertFalse statements. They mimic the assertions used in JUnit, the test framework for Java. In the code shown here, they simply output the condition tested when the assertion fails.

Earlier versions of mnyfmt returned a pointer to the formatted string, mimicking the behavior of strcpy. But it turns out that it is more useful if a pointer to the first significant digit is returned, so the same picture clause can be used to format a small value like 1235.87 or a huge one like 123456789.88. mnyfmt always uses the hyphen as the negative sign, and it replaces a 9 in the format string. The only character in the format string that ever gets changed is a 9.

As mnyfmt replaces each 9 in the format string with the corresponding digit, sometimes the result begins with a decimal separator. This special case should be handled by the programmer as there is no generalized solution that can be applied by mnyfmt.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video