Q. I know about GetNumberFormat, but I want 4 decimal digits, not two. And I want a GetCurrencyFormat
with my own currency.
A. You have to fill the NUMBERFMT
and CURRENCYFMT
with the right values.
2007.02.22: Fixed formats "3;2" / "3" that needed to become 320 / 30. Bug reported and explained by Shawn Steele. Thank you!
2007.02.22: Fixed LOCALE_STHOUSAND in NumberFormat (I was using LOCALE_SMONTHOUSANDSEP), thanks Jason Owen.
2007.02.22: Added test application, and all the code is now available for download here.
Now, why would you want to do that? The standard API is enough, isn’t it? Well, yes and no. For instance GetCurrencyFormat
takes a pointer to a CURRENCYFMT
or NULL
. If you pass NULL
, everything is formatted according with the LCID
passed. But if you pass a real structure, all fields have to be filled. What if I want to format something “almost” like the standard, just slightly off?
So, what should you do if you want to display a currency without having the currency symbol changed?
Let’s say a car costs 12345.67 USD. Displaying this with US locales gives you $12,345.67, but using a Japanese locale will return ¥12.345,45. Right now, one US dollar is about 100 Japanese Yen. Offering it for ¥12.345,45 would be quite a loss.
Also, the $ symbol is used for many other dollars, not only the U.S. one (Argentina, Australia, Brunei, Canada, Chile, Columbia, Ecuador, El Salvador, Mexico, New Zealand, Singapore), so even for the American dollar you might want to use “USD” once your application “goes abroad,” to say so.
Ok, so this is what I would like to get for 12345.67 without losing the USD:
en_US: “$12,345.67” or “USD 12,345.67”
ro_RO: 12.345,67 dolari americani” or “12.345,67 USD”
Recently I have found out that I am not the only one who needs this (from comments to MishKa’s blog and also from some newsgroups), and since in my answers I have promised to think about making the code public, here it is (after doing the thinking).
Ok, now that we established the need, let’s see how we solve this problem.
The available API is enough? Yes and no. There is nothing to fill the structures for us. But we do have GetLocaleInfo
to help us do it. It is easy, but I will write some classes which will help. I will explain later why C++ and classes instead of plain C, like all the Windows API.
Now, here is the code to fill a NUMBERFMT
. Quite straightforward, except for the Grouping
part. But that should be clear, if you read the comment.
You can take it and use it.
// NumberFormatStruct.h
// Copyright © January 2004, Mihai Nita
#pragma once
#include
<windows.h>#include
<tchar.h>class
CNumberFormatStruct {private:
TCHAR decimalSep[10]; TCHAR thousandSep[10];public:
NUMBERFMT struNumberFormat;virtual
~CNumberFormatStruct() {}; CNumberFormatStruct( LCID lcid = CP_ACP ) { GetDefaultFormat( lcid ); };int
GetDefaultFormat( LCID lcid ); };
// NumberFormatStruct.cpp
// Copyright © January 2004, Mihai Nita
#include
"NumberFormatStruct.h"#define
DIM(a) (sizeof
(a)/sizeof
(a[0]))int
CNumberFormatStruct::GetDefaultFormat( LCID lcid ) { TCHAR buff[100];// huge, but just in case
int
nRez = 1; memset( &struNumberFormat, 0,sizeof
(struNumberFormat) );// Not using LOCALE_RETURN_NUMBER, is not available for Win 95.
// I want to be compatible all the way back (I wonder why :-))
// And in fact the ugliest one is LOCALE_SGROUPING, which does not support LOCALE_RETURN_NUMBER
nRez &= GetLocaleInfo( lcid, LOCALE_IDIGITS, buff, DIM(buff) );// max 2
struNumberFormat.NumDigits = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_ILZERO, buff, DIM(buff) );// max 2
struNumberFormat.LeadingZero = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_SDECIMAL, decimalSep, DIM(decimalSep) );// max 4
struNumberFormat.lpDecimalSep = decimalSep; nRez &= GetLocaleInfo( lcid, LOCALE_STHOUSAND, thousandSep, DIM(thousandSep) );// max 4
struNumberFormat.lpThousandSep = thousandSep; nRez &= GetLocaleInfo( lcid, LOCALE_INEGNUMBER, buff, DIM(buff) );// max 2
struNumberFormat.NegativeOrder = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_SGROUPING, buff, DIM(buff) );//max undocumented
// Now the ugly part. Have to convert from something like string "3;2;0" to int 32
TCHAR *parseGrpS = buff;while
( *parseGrpS ) {if
( (*parseGrpS >= _T('1')) && (*parseGrpS <= _T('9')) ) struNumberFormat.Grouping = struNumberFormat.Grouping * 10 + (*parseGrpS-_T('0'));if
( (*parseGrpS != _T('0')) && !parseGrpS[1] ) struNumberFormat.Grouping *= 10; parseGrpS++; }return
nRez; }
Now, why did I choose to create a class instead of plain C?
Because of the decimal and the thousand separators, which are TCHAR *
in the original structure. (Ok, I am lying here, there is no NUMBERFMT
structure. There is a NUMBERFMTA
structure with WCHAR *
and a NUMBERFMTW
with char *
. See How SBCS-MBCS-Unicode application interact with Windows).
So, a function trying to fill a structure will have to receive the two TCHAR *
pointing to some buffers, big enough to receive the result (not too much), but the call would be something like:
NUMBERFMT nf; TCHAR decimalSep[10]; TCHAR thousandSep[10]; nf.lpDecimalSep = decimalSep; nf.lpThousandSep = thousandSep; FilleStructure( lcid, &nf ); nf.NumDigits = 4; GetNumberFormat( ..., &nf, ... );
Quite ugly, if you ask me.
Another option would be to have two static buffers in the FillStructure
function.
int
FillStructure( LCID lcid, NUMBERFMT &nf ) {static
TCHAR decimalSep[10];static
TCHAR thousandSep[10];if
( ! nf )return
ERROR;// or ASSERT( nf ); if you like
...// Fill the structure using GetLocaleInfo
nf.lpDecimalSep = decimalSep; nf.lpThousandSep = thousandSep; }void
Test(void
) { NUMBERFMT nf FilleStructure( lcid, &nf ); nf.NumDigits = 4; GetNumberFormat( ..., &nf, ... ); }
But this restricts the use of the API. If one calls FillStructure
with various LCID
s, the structure and the two buffers have to be saved somewhere, and even then we might have a problem in multithreaded use.
The third option is to have some kind of map storing a full collection of buffers, each for each LCID
ever requested. Not easy to implement, and quite cumbersome. Ok, one can use a std::map
, but this means again C++, so why not our own class.
Then it becomes quite easy to use:
CNumberFormatStruct nf(lcid); nf.struNumberFormat.NumDigits = 4; GetNumberFormat( ..., &nf.struNumberFormat, ... );
Clean and nice, I think.
For the GetCurrencyFormat
I am just going to give you the code. There is no major difference, just taking care of the lpCurrencySymbol
field and to use the currency-related constants in GetLocaleInfo
(but you already knew that a stand-alone number format can be different than the same number as part of the currency, didn’t you?)
// CurrencyFormatStruct.h
// Copyright © January 2004, Mihai Nita
#pragma once
#include
<windows.h>#include
<tchar.h>class
CCurrencyFormatStruct {private
: TCHAR decimalSep[10]; TCHAR thousandSep[10]; TCHAR currencySymbol[10];public
: CURRENCYFMT struCurrencyFormat;virtual
~CCurrencyFormatStruct() {}; CCurrencyFormatStruct( LCID lcid = CP_ACP ) { GetDefaultFormat( lcid ); };int
GetDefaultFormat( LCID lcid ); };
// CurrencyFormatStruct.cpp
// Copyright © January 2004, Mihai Nita
#include
"CurrencyFormatStruct.h"#define
DIM(a) (sizeof
(a)/sizeof
(a[0]))int
CCurrencyFormatStruct::GetDefaultFormat( LCID lcid ) { TCHAR buff[100]; // huge, but just in caseint
nRez = 1; memset( &struCurrencyFormat, 0,sizeof
(struCurrencyFormat) );// Not using LOCALE_RETURN_NUMBER, is not available for Win 95.
// I want to be compatible all the way back (I wonder why :-))
// And in fact the ugliest one is LOCALE_SGROUPING, which does not support LOCALE_RETURN_NUMBER
nRez &= GetLocaleInfo( lcid, LOCALE_ICURRDIGITS, buff, DIM(buff) );// max 3
struCurrencyFormat.NumDigits = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_ILZERO, buff, DIM(buff) );// max 2
struCurrencyFormat.LeadingZero = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_SMONDECIMALSEP, decimalSep, DIM(decimalSep) );// max 4
struCurrencyFormat.lpDecimalSep = decimalSep; nRez &= GetLocaleInfo( lcid, LOCALE_SMONTHOUSANDSEP, thousandSep, DIM(thousandSep) );// max 4
struCurrencyFormat.lpThousandSep = thousandSep; nRez &= GetLocaleInfo( lcid, LOCALE_SCURRENCY, currencySymbol, DIM(currencySymbol) );// max 6
struCurrencyFormat.lpCurrencySymbol = currencySymbol; nRez &= GetLocaleInfo( lcid, LOCALE_INEGCURR, buff, DIM(buff) );// max 3
struCurrencyFormat.NegativeOrder = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_ICURRENCY, buff, DIM(buff) );// max 3
struCurrencyFormat.PositiveOrder = _tcstoul( buff, NULL, 0 ); nRez &= GetLocaleInfo( lcid, LOCALE_SMONGROUPING, buff, DIM(buff) );// max 4
// Now the ugly part. Have to convert from something like string "3;2;0" to int 32
TCHAR *parseGrpS = buff;while
( *parseGrpS ) {if
( (*parseGrpS >= _T('1')) && (*parseGrpS <= _T('9')) ) struCurrencyFormat.Grouping = struCurrencyFormat.Grouping * 10 + (*parseGrpS-_T('0'));if
( (*parseGrpS != _T('0')) && !parseGrpS[1] ) struCurrencyFormat.Grouping *= 10; parseGrpS++; }return
nRez; }
How to use it to localize some USD ammount without affecting the value?
You can load the localized USD name from the resources (i.e. “dolari americani”):
TCHAR currencyName[100]; LoadString( hResInstance, IDS_LOCALIZED_USD_NAME, currencyName, DIM(currencyName) );
or use GetLocaleInfo
to get the ISO 4217 name:
LCID lcidUS = MAKELCID( MAKELANGID( LANG_ENGLISH, SUBLANG_ENGLISH_US ), SORT_DEFAULT ); TCHAR currencyName[100]; GetLocaleInfo( lcidUS, LOCALE_SINTLSYMBOL, currencyName, DIM(currencyName) );
And now you can use the obtained currency string with a pre-filled structure:
CCurrencyFormatStruct cfs(lcid); cfs.struCurrencyFormat.lpCurrencySymbol = currencyName; GetCurrencyFormat( ..., &cfs.struCurrencyFormat, ... );
Hi Mihai, I think I found a bug – the “ugly part” doesn’t handle 3;0;0 properly. Yes, 3;0;0 is a real one (picked by choosing 123456,789). Yours calculates 3 but it should be 30. You will probably have to add this to the loop:
if( (*parseGrpS == _T(‘0’)) && parseGrpS[1] == _T(‘;’) ) // handles 3;0;0
struCurrencyFormat.iGrouping *= 10;
similar for number format.
Thank you, much appreciated!
I will take a look at it over the week-end and update.
Regards,
Mihai