NetBSD-Bugs archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
lib/60677: strfmon(3) produces incorrect output in the C [POSIX] locale
>Number: 60677
>Category: lib
>Synopsis: strfmon(3) produces incorrect output in the C [POSIX] locale
>Confidential: no
>Severity: non-critical
>Priority: medium
>Responsible: lib-bug-people
>State: open
>Class: sw-bug
>Submitter-Id: net
>Arrival-Date: Wed Sep 02 05:15:00 +0000 2026
>Originator: Robert Elz
>Release: NetBSD 11.99.6 (anything in recent memory)
>Organization:
Pedantic International Standards Scholars
>Environment:
System: NetBSD jacaranda.noi.kre.to 11.99.6 NetBSD 11.99.6 (JACARANDA:1.1-20260627) #287: Sun Jun 28 03:17:40 +07 2026 kre%jacaranda.noi.kre.to@localhost:/usr/obj/testing/kernels/amd64/JACARANDA amd64
Architecture: x86_64
Machine: amd64
>Description:
The C (aka POSIX) locale for LC_MONETARY is all empty (specifies
nothing at all). That's what the standard specifies, and is also
what our C locale contains for LC_MONETARY.
According to the standard for strfmon() when that happens:
If the '+' flag is included (requiring a sign, which the C locale
does not have any definition for) EINVAL is the required result.
If neither the '(' (requesting () around negative values) nor the '+'
flag is given, then the locale default is used - the C locale does
not have one of those, in which case strfmon() must assume '+', except
if the locale hasn't specified what the sign strings should be, "" for
positive (which is the only method to not specify something, so is
what the C locale does), and "-" for negative (if the locale specifies
the same value as for positive, which it does, it is also "") are used.
There are also defaults for the positioning of the sign if the locale
doesn't specify any (the C locale doesn't).
There is no default monetary radix character in the C locale either,
nor any default number of digits to come after it. Unfortunately the
standard has nothing to say about what should be done in that case,
whether the "." is acceptable as a default radix character, and
perhaps the common 2 for the default precision.
Our strfmon includes the following (and more similar) in its
__setup_vars() internal function:
*signstr = (lc->negative_sign[0] == '\0') ? "-"
: lc->negative_sign;
/* Set default values for unspecified information. */
if (*cs_precedes != 0)
*cs_precedes = 1;
if (*sep_by_space == CHAR_MAX)
*sep_by_space = 0;
if (*sign_posn == CHAR_MAX)
*sign_posn = 0;
All of which make it impossible for the rest of the code to do
what the standard specifies, as the values that are actually
in the locale are unavailable because of these default values
being inserted (just as if the locale defined them that way).
The internal __format_grouped_double() function (for formatting
the actual numeric parameter) includes these:
decimal_point = lc->mon_decimal_point;
if (*decimal_point == '\0')
decimal_point = lc->decimal_point;
thousands_sep = lc->mon_thousands_sep;
if (*thousands_sep == '\0')
thousands_sep = lc->thousands_sep;
which amount to "if the LC_MONETARY locale definition provides
no values for the decimal-point (radix char) or the
thousand_sep (grouping char left of the decimal point), just
use the similar values from LC_NUMERIC instead. While that
is probably sane, I can find no justification for doing that
anywhere in the standards.
It also contains
if (right_prec == CHAR_MAX) /* POSIX locale ? */
right_prec = 2;
"right_prec" in this context is the number of digits to place
after the decimal point ... the LC_NUMERIC locale has no
concept of a default for that, so the code simply invents one.
I can find no justification for that either.
In general, using strfmon() at all in the C (or POSIX) locales
makes little sense, as there is no C (or POSIX) currency, hence
no standard (or even non-standard) way to format such a thing.
That's what the default LC_MONETARY settings for the C locale
are intended to convey.
But strfmon() is supposed to work regardless, even regardless
of the missing necessary information. For the parts where
the strfmon() standard gives no guidance, doing something reasonable
as is done for the decimal_point setting shown above, is probably
OK (for the C locale, the thousands_sep is irrelevant, as it doesn't
define any grouping, so that one will never be used in this context).
I am less certain about simply assuming that "it must mean 2 digits
after the decimal point" when there is no specification, 0 would be
just as good a choice. It doesn't fit EUC, USD, GBP, etc
(and of course, the LC_MONETARY locale definition is woefully
deficient in handling formatting currency values that are not
written like a floating point number, consider pounds, shillings,
and pence for example - but nothing we can do about that here).
>How-To-Repeat:
Use a small program like the following:
#include <err.h>
#include <locale.h>
#include <monetary.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int
main(int argc, char **argv)
{
locale_t loc;
char buf[80];
ssize_t s;
if (argc != 4)
exit(1);
loc = newlocale(LC_MONETARY_MASK, argv[1], 0);
if (loc == NULL)
errx(1, "Locale '%s' not found", argv[1]);
s = strfmon_l(buf, sizeof buf, loc, argv[2], strtod(argv[3], NULL));
if (s == -1)
err(1, "strfmon() failed");
printf("%zd: [[%s]]\n", s, buf);
}
Its usage of strfmon_l() rather than strfmon() is irrelevant,
that just alters the way the locale information to use is
obtained, about which there are no issues I am aware of.
Compile it (cc -o foo foo.c) and run it like
./foo C %n -123.75
and expect to see something, but certainly not what
is produced:
8: [[(123.75)]]
I kind of expect the correct resulting string (forget
the length result part, and the "[[" "]]" that are just
noise, that's all always OK) should be:
-124
Certainly it should be using '-' instead of (), and
without an explicit precision (as in %.3n) since the
C locale doesn't specify one, a 0 default (or an error)
would be better that simply inventing "2" out of nowhere.
If run as
./foo C %+n -1
it should return EINVAL, as the C locale has no sign strings,
and this usage explicitly requests one (the POSIX standard
takes pains to point out this as a potential pitfall of
using the '+' flag - any locale, not just the C locale might
be missing sign strings if their normal usage is to use ()
around negative numbers). Instead we get:
6: [[(1.00)]]
which is so wrong it is just about unbelievable. Even if
the locale specified use of () for negative values, which it
can do, explicitly asking for a +/- result should override
that - and if there are no (differing) +/- sign strings to
use, EINVAL is required.
>Fix:
For some of this, coming soon. For other parts, it will depend
upon what kind of feedback this PR generates.
[Aside: all this comes from attempts to test the new fake
locale I have invented, which needed a LC_MONETARY definition, so
I gave it a strange one, and then started to look at the
ATF libc locale/t_strfmon test, and extending that to test
this new locale, along with similar extensions to the
locale/t_sprintf tests for its LC_NUMERIC definition, which were
the reason for creating it ... all for PR bin/60496.]
Home |
Main Index |
Thread Index |
Old Index