Quirks, Insights, and Obscure Details
Many C library functions come in multiple flavors, one for each type. For example, lround, lroundf, lroundl, llround, llroundf, and llroundl are versions of a single rounding function. The name changes because various versions return different kinds of integers and accept different types of arguments. It is not necessary to have multiple versions of mnyfmt because conversion to the wider integer type is always provided by the compiler.
The relationship between types char and wchar_t is not always clear. The C standard does not specify the exact type for wchar_t. It can be two or four bytes wide, or even one byte wide. Moreover, type char can be signed or unsigned. Because most picture clauses for mnyfmt rely on characters from the 103-character portable character subset that POSIX requires in any character set, it's easiest to use the char type for picture clauses and perform an explicit conversion into wchar_t when necessary:
// Convert a char[] into a wchar_t[]
char *src, chBuff[128];
wchar_t *dst, *wcBuff;
strcpy (chBuff, "Convert me to (wchar_t)");
wcBuff = (wchar_t*)(malloc(strlen(chBuff)*sizeof(wchar_t)));
for (dst=wcBuff,src=chBuff; (*dst=*src); ++dst,++src) {}
// ... C++ will let you use more sophisticated stuff
free(wcBuff);
Most processors use two's complement binary arithmeti, which behaves strangely when dealing with the smallest negative values. That is why mnyfmt fails to calculate the correct formatted result for this case and returns a null pointer:
// test.minus.max long long_min = -LONG_MAX-1; assertTrue(long_min<0); long_min = -long_min; assertTrue(long_min<0 && "?????"); assertTrue(long_min == -long_min);
The behavior of mnyfmt is different when formatting the integer and the fractional part of the number. In the integer part, all leading non-significant format characters get replaced by 0, but only those that immediately follow the decimal separator get changed. This is why, in the next code sample, the 1 in the format string stops the substitution, leaving the remaining characters unchanged. This code also shows a bad programming practice, as there is no check to ensure that sgn is not null. This error could cause a program failure when the null pointer returned by mnyfmt gets used to change a value in memory. Enclosing every invocation of mnyfmt in an if statement is necessary to avoid this pitfall.
// test.stop
char *sgn, fmtstr[96];
strcpy(fmtstr, "999,999.9999919,one9.");
if ((sgn = mnyfmt(fmtstr, '.', 2455,87)))
{
if ((*sgn=='-') && (','==*(sgn+1)))
{
++sgn; *sgn='-';
}
assertTrue(eqstr(fmtstr, "002,455.8700019,one9."));
assertTrue(eqstr(sgn, "2,455.8700019,one9."));
}
The following code shows that a picture clause can be used to format rupee amounts when words are also included. This code looks for the decimal point and replaces it with a blank space to achieve a more convincing result.
// test.rupee
char *sgn, fmtstr[96]; char *p;
strcpy(fmtstr, "99,99,99,99,99,99,99,999.99");
// LONG_LONG_MAX == 92,23,37,20,36,85,47,758.07
// 3,25,84,729.25
// 19 digits: 12 34 56 78 90 12 34 567 89
if ((sgn = mnyfmt(fmtstr, '.', 32584729,25)))
{
assertTrue(eqstr(sgn, "3,25,84,729.25"));
}
strcpy(fmtstr,"99,99,99,99,99 crores 99 lakhs 99,999 rupees.99 paise");
if ((sgn = mnyfmt(fmtstr, '.', 32584729,25)))
{
for (p=sgn; *p!='.'&&*p!=0; ++p)
{
// advance p to dec '.'
}
*p = ' '; // blank the dot: ".rupees" ==> " rupees"
}
assertTrue(eqstr(sgn, "3 crores 25 lakhs 84,729 rupees 25 paise"));
// Rp3,25,84,729.25 is read as three crore(s), twenty-five lakh(s),
// eighty-four thousand, seven hundred and twenty-nine rupees and
// twenty-five paise.
Picture clauses can be used to format almost any type of numeric values. The following code shows how to handle dates and hours, but many more applications are possible.
// test.times
char *sgn, fmtstr[96];
strcpy(fmtstr, "99/99/9999");
if ((sgn = mnyfmt(fmtstr, 000, 9272002,0)))
{
assertTrue(eqstr(fmtstr, "09/27/2002"));
assertTrue(eqstr(sgn, "9/27/2002"));
}
strcpy(fmtstr , "99:99:99");
if ((sgn = mnyfmt(fmtstr, '?', 21435,0)))
{
assertTrue(eqstr(fmtstr, "02:14:35"));
assertTrue(eqstr(sgn, "2:14:35"));
}
The next code sample illustrates a mistake any programmer could make: failure to copy the format string into the formatting variable before invoking mnyfmt. Many programmers will choose to place their currency-formatting logic inside functions that are tailored to each application in order to prevent mistakes like this one.
// test.no.strcpy
char *sgn, fmtstr[96];
strcpy(fmtstr, "9,999.");
if ((sgn = mnyfmt(fmtstr, '.', 2455,87)))
{
assertTrue(eqstr(sgn, "2,455."));
}
if ((sgn = mnyfmt(fmtstr, '.', 1400,87)))
{
// never executed: missing strcpy()
// no char in "2,455." is a format char
}
else
{
assertFalse(eqstr(fmtstr, "1,400."));
assertTrue(eqstr(fmtstr, "2,455.")); // ???
assertTrue("BEWARE: missing strcpy()"); // ???
{
strcpy(fmtstr, "9,999.");
sgn = mnyfmt(fmtstr, '.', 1400,87);
assertTrue(eqstr(sgn, "1,400."));
}
}
The double parentheses in the if statement that contains the invocation to mnyfmt might seem odd, but it is a good programming practice suggested by the compiler: "warning: suggest parentheses around assignment used as truth value."
A Good Enough Solution
mnyfmt in its current form does a "good enough" job of formatting international currencies. Further internationalization can be accomplished with a library like ISOMON, which provides simple access to ISO currency data. If you're programming in C++, you'll probably want to pack mnyfmt inside a wrapper that validates input and guards against other usage errors.
For further information, download the complete source code and documentation for mnyfm.
Adolfo Di Mare is a researcher at the Escuela de Ciencias de la Computacion e Informática, Universidad de Costa Rica, where he is full professor. He is a tutor at the Stvdivm Generale in the Universidad Autónoma de Centro America, where he is a Cathedraticum.


