diff --git a/src/intf.c b/src/intf.c index 5860459..a5d5dba 100644 --- a/src/intf.c +++ b/src/intf.c @@ -1467,21 +1467,10 @@ int vmkchstr (chtype *restrict chbuf, int chbufsize, chtype attr_norm, case L'N': // Insert a monetary amount (double) into the output - { - /* strfmon() is not available in a wide-char - version, so we need a multibyte char buffer */ - char *buf = xmalloc(BUFSIZE); - - if (xstrfmon(buf, BUFSIZE, spec->flag_nosym ? "%!n" : "%n", - format_arg[spec->arg_num].a.a_double) < 0) { - saved_errno = errno; - free(buf); - errno = saved_errno; - goto error; - } - - xmbstowcs(fmtbuf, buf, BUFSIZE); - free(buf); + if (xwcsfmon(fmtbuf, BUFSIZE, spec->flag_nosym ? + "%!n" : "%n", + format_arg[spec->arg_num].a.a_double) < 0) { + goto error; } str = fmtbuf; diff --git a/src/utils.c b/src/utils.c index ede5501..a9e2573 100644 --- a/src/utils.c +++ b/src/utils.c @@ -570,22 +570,26 @@ void init_locale (void) /***********************************************************************/ -// xstrfmon: Convert monetary value to a string +// xwcsfmon: Convert monetary value to a wide-character string -ssize_t xstrfmon (char *restrict buf, size_t maxsize, +ssize_t xwcsfmon (wchar_t *restrict buf, size_t maxsize, const char *restrict format, double val) { - /* Current and previous versions of ISO/IEC 9945-1 (POSIX), particularly + ssize_t ret; + char *s = xmalloc(BUFSIZE); + + + /* Current and previous versions of ISO/IEC 9945-1 (POSIX), namely SUSv3 (2001) and SUSv4 (2008), require strfmon() to return rather meaningless strings when used with the POSIX "C" locale. In particular, the standard POSIX locale does not define a currency symbol, a monetary radix symbol (decimal point) or a negative sign. This means strfmon(..., "%n", -123.45) is supposed to - produce "12345" instead of something like "$-123.45"! This - function overcomes these limitations by using snprintf(). */ + produce "12345" instead of something like "-$123.45"! The + following code overcomes these limitations by using snprintf(). */ if (! is_posix_locale) { - return strfmon(buf, maxsize, format, val); + ret = strfmon(s, BUFSIZE, format, val); } else { /* The current implementation assumes the monetary decimal point is overridden to "." (ie, MOD_POSIX_MON_DECIMAL_POINT == "."), @@ -609,22 +613,49 @@ ssize_t xstrfmon (char *restrict buf, size_t maxsize, if (strcmp(format, "%n") == 0) { if (val >= 0.0) { - return snprintf(buf, maxsize, MOD_POSIX_FMT_POS, val); + ret = snprintf(s, BUFSIZE, MOD_POSIX_FMT_POS, val); } else { - return snprintf(buf, maxsize, MOD_POSIX_FMT_NEG, -val); + ret = snprintf(s, BUFSIZE, MOD_POSIX_FMT_NEG, -val); } } else if (strcmp(format, "%!n") == 0) { if (val >= 0.0) { - return snprintf(buf, maxsize, MOD_POSIX_FMT_POS_NOSYM, val); + ret = snprintf(s, BUFSIZE, MOD_POSIX_FMT_POS_NOSYM, val); } else { - return snprintf(buf, maxsize, MOD_POSIX_FMT_NEG_NOSYM, -val); + ret = snprintf(s, BUFSIZE, MOD_POSIX_FMT_NEG_NOSYM, -val); } } else { // Other strfmon() formats are not supported errno = EINVAL; - return -1; + ret = -1; } } + + if (ret >= BUFSIZE) { + // Truncate the too-long output with a terminating NUL + s[BUFSIZE - 1] = '\0'; + } + + if (ret >= 0) { + xmbstowcs(buf, s, maxsize); + + /* Some buggy implementations of strfmon(), such as that on + FreeBSD and Cygwin, assume localeconv.mon_thousands_sep and + similar strings contain either a single char or NUL instead of + a multibyte character string. However, this assumption fails + on locales such as ru_RU.UTF-8 which use U+00A0 NO-BREAK SPACE + for mon_thousands_sep (stored in UTF-8 as 0xC2 0xA0. As a + result, incomplete character sequences are copied, which are + translated to EILSEQ_REPL characters by xmbstowcs() above. + Fix such characters by replacing them with a space. */ + for (wchar_t *p = buf; *p != L'\0'; p++) { + if (*p == EILSEQ_REPL) { + *p = L' '; + } + } + } + + free(s); + return ret; } diff --git a/src/utils.h b/src/utils.h index a307870..2fbc7f5 100644 --- a/src/utils.h +++ b/src/utils.h @@ -239,18 +239,19 @@ extern void init_locale (void); /* - Function: xstrfmon - Convert monetary value to a string + Function: xwcsfmon - Convert monetary value to a wide-character string Parameters: buf - Buffer to receive result - maxsize - Maximum size of buffer + maxsize - Maximum size of buffer, in multiples of wchar_t format - strfmon() format to use val - Monetary value to convert Returns: ssize_t - Size of returned string This function calls strfmon() to convert val to a suitable monetary - value string, making appropriate adjustments if the POSIX locale is in - effect. + value string, then converts the result to a wide-character string and + places it in buf. It makes appropriate adjustments to the output if + the POSIX locale is in effect or if the locale uses no-break spaces. */ -extern ssize_t xstrfmon (char *restrict buf, size_t maxsize, +extern ssize_t xwcsfmon (wchar_t *restrict buf, size_t maxsize, const char *restrict format, double val);