3

I'm using the Windows API GetNumberFormatEx to format some numbers for display with the appropriate localization choices for the current user (e.g., to make sure they have the right separators in the right places). This is trivial when you want exactly the user default.

But in some cases I sometimes have to override the number of digits after the radix separator. That requires providing a NUMBERFMT structure. What I'd like to do is to call an API that returns the NUMBERFMT populated with the appropriate defaults for the user, and then override just the fields I need to change. But there doesn't seem to be an API to get the defaults.

Currently, I'm calling GetLocaleInfoEx over and over and then translating that data into the form NUMBERFMT requires.

NUMBERFMT fmt = {0};
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT,
                  LOCALE_IDIGITS | LOCALE_RETURN_NUMBER,
                  reinterpret_cast<LPWSTR>(&fmt.NumDigits),
                  sizeof(fmt.NumDigits)/sizeof(WCHAR));
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT,
                  LOCALE_ILZERO | LOCALE_RETURN_NUMBER,
                  reinterpret_cast<LPWSTR>(&fmt.LeadingZero),
                  sizeof(fmt.LeadingZero)/sizeof(WCHAR));
WCHAR szGrouping[32] = L"";
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, LOCALE_SGROUPING, szGrouping,
                  ARRAYSIZE(szGrouping));
if (::lstrcmp(szGrouping, L"3;0") == 0 ||
    ::lstrcmp(szGrouping, L"3") == 0
) {
    fmt.Grouping = 3;
} else if (::lstrcmp(szGrouping, L"3;2;0") == 0) {
    fmt.Grouping = 32;
} else {
    assert(false);  // unexpected grouping string
}
WCHAR szDecimal[16] = L"";
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, LOCALE_SDECIMAL, szDecimal,
                  ARRAYSIZE(szDecimal));
fmt.lpDecimalSep = szDecimal;
WCHAR szThousand[16] = L"";
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, LOCALE_STHOUSAND, szThousand,
                  ARRAYSIZE(szThousand));
fmt.lpThousandSep = szThousand;
::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT,
                  LOCALE_INEGNUMBER | LOCALE_RETURN_NUMBER,
                  reinterpret_cast<LPWSTR>(&fmt.NegativeOrder),
                  sizeof(fmt.NegativeOrder)/sizeof(WCHAR));

Isn't there an API that already does this?

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175

2 Answers2

2

I just wrote some code to do this last week. Alas, there does not seem to be a GetDefaultNumberFormat(LCID lcid, NUMBERFMT* fmt) function; you will have to write it yourself as you've already started. On a side note, the grouping string has a well-defined format that can be easily parsed; your current code is wrong for "3" (should be 30) and obviously will fail on more exotic groupings (though this is probably not much of a concern, really).

Luke
  • 11,211
  • 2
  • 27
  • 38
  • Darn, I was hoping there was an API. I'm pretty sure I got the grouping field correct. See http://msdn.microsoft.com/en-us/library/dd319095(v=vs.85).aspx where it starts, "Values in the range 0 through 9 and 32 are valid." (Therefore, 30 is not valid.) – Adrian McCarthy Aug 28 '11 at 18:00
  • In practice, 0, 3, and possibly 32 are all you'll ever really see. But if you want to handle all scenarios just in case, take a look at [this](http://blogs.msdn.com/b/shawnste/archive/2006/07/17/668741.aspx). – Luke Aug 28 '11 at 22:13
  • that's a good link even though it sort of contradicts MSDN, it seems accurate. – Adrian McCarthy Aug 29 '11 at 15:36
0

If all you want to do is cut off the fractional digits from the end of the string, you can go with one of the default formats (like LOCALE_NAME_USER_DEFAULT), then check for the presence of the fractional separator (comma in continental languages, point in English) in the resulting character string, and then chop off the fractional part by replacing it with a null byte:

#define cut_off_decimals(sz, cch) \
    if (cch >= 5 && (sz[cch-4] == _T('.') || sz[cch-4] == _T(','))) \
        sz[cch-4] = _T('\0');

(Hungarian alert: sz is the C string, cch is character count, including the terminating null byte. And _T is the Windows generic text makro for either char or wchar_t depending on whether UNICODE is defined, only needed for compatibility with Windows 9x/ME.)

Note that this will produce incorrect results for the very odd case of a user-defined format where the third-to-last character is a dot or a comma that has some special meaning to the user other than fractional separator. I have never seen such a number format in my whole life, and hence I conclude that this is good and safe enough.

And of course this won't do anything if the third-to-last character is neither a dot nor a comma.

Lumi
  • 14,775
  • 8
  • 59
  • 92
  • This is too specific for what I'm after. I really want to choose the number of fractional digits rather than to lop them all off. I could use a similar approach to this, but I'd still need to look up the number of fractional digits so I know which position to check for the radix separator (and make sure I'm not confusing it for the grouping separator). All in all, it seems a hack. – Adrian McCarthy Oct 26 '15 at 17:41
  • 1
    Also, a minor nit: `_UNICODE` is actually the MS CRT macro that controls how `_T` and `_TEXT` are defined. `UNICODE` is the Windows macro that controls how `TEXT` is defined. – Adrian McCarthy Oct 26 '15 at 17:43