diff --git a/lib/libc/stdlib/strfmon.3 b/lib/libc/stdlib/strfmon.3 index 20cc560d401d..d537d9d05fa7 100644 --- a/lib/libc/stdlib/strfmon.3 +++ b/lib/libc/stdlib/strfmon.3 @@ -1,216 +1,226 @@ .\" Copyright (c) 2001 Jeroen Ruigrok van der Werven .\" All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted provided that the following conditions .\" are met: .\" 1. Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" 2. Redistributions in binary form must reproduce the above copyright .\" notice, this list of conditions and the following disclaimer in the .\" documentation and/or other materials provided with the distribution. .\" .\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE .\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd December 6, 2023 +.Dd November 24, 2025 .Dt STRFMON 3 .Os .Sh NAME .Nm strfmon , .Nm strfmon_l .Nd convert monetary value to string .Sh LIBRARY .Lb libc .Sh SYNOPSIS .In monetary.h .Ft ssize_t .Fn strfmon "char * restrict s" "size_t maxsize" "const char * restrict format" "..." .In monetary.h .In xlocale.h .Ft ssize_t .Fn strfmon_l "char * restrict s" "size_t maxsize" "locale_t loc" "const char * restrict format" "..." .Sh DESCRIPTION The .Fn strfmon function places characters into the array pointed to by .Fa s , as controlled by the string pointed to by .Fa format . No more than .Fa maxsize bytes are placed into the array. .Pp The .Fn strfmon_l function takes an explicit locale argument, whereas the .Fn strfmon function uses the current global or per-thread locale. .Pp The format string is composed of zero or more directives: ordinary characters (not .Cm % ) , which are copied unchanged to the output stream; and conversion specifications, each of which results in fetching zero or more subsequent arguments. Each conversion specification is introduced by the .Cm % character. After the .Cm % , the following appear in sequence: .Bl -bullet .It Zero or more of the following flags: .Bl -tag -width "XXX" .It Cm = Ns Ar f A .Sq Cm = character followed by another character .Ar f which is used as the numeric fill character. .It Cm ^ Do not use grouping characters, regardless of the current locale default. .It Cm + Represent positive values by prefixing them with a positive sign, and negative values by prefixing them with a negative sign. This is the default. .It Cm \&( Enclose negative values in parentheses. .It Cm \&! Do not include a currency symbol in the output. .It Cm \- Left justify the result. Only valid when a field width is specified. .El .It An optional minimum field width as a decimal number. By default, there is no minimum width. .It A .Sq Cm # sign followed by a decimal number specifying the maximum expected number of digits before the radix character. When this option is used, values that do not exceed the specified number of digits are formatted so they will be correctly aligned with other values printed using the same format. This includes always leaving space for a possible sign indicator, even if none is needed for a particular value. .It A .Sq Cm \&. character followed by a decimal number specifying the number of digits after the radix character. .It One of the following conversion specifiers: .Bl -tag -width "XXX" .It Cm i The .Vt double argument is formatted as an international monetary amount. .It Cm n The .Vt double argument is formatted as a national monetary amount. .It Cm % A .Sq Li % character is written. .El .El .Sh RETURN VALUES If the total number of resulting bytes, including the terminating .Dv NUL byte, is not more than .Fa maxsize , .Fn strfmon and .Fn strfmon_l return the number of bytes placed into the array pointed to by .Fa s , not including the terminating .Dv NUL byte. Otherwise, \-1 is returned, the contents of the array are indeterminate, and .Va errno is set to indicate the error. .Sh EXAMPLES The following example will format the value .Dq Li 1234567.89 to the string .Dq Li $1,234,567.89 : .Bd -literal -offset indent #include #include #include int main(void) { char string[100]; double money = 1234567.89; if (setlocale(LC_MONETARY, "en_US.UTF-8") == NULL) { fprintf(stderr, "Unable to setlocale().\\n"); return (1); } strfmon(string, sizeof(string) - 1, "%n", money); printf("%s\\n", string); return (0); } .Ed .Sh ERRORS The .Fn strfmon function will fail if: .Bl -tag -width Er .It Bq Er E2BIG Conversion stopped due to lack of space in the buffer. .It Bq Er EINVAL The format string is invalid. +.It Bq Er EINVAL +The +.Cm + +flag was included in a conversion specification and the locale's +.Va positive_sign +and +.Va negative_sign +values would both be returned by +.Xr localeconv 3 +as empty strings. .It Bq Er ENOMEM Not enough memory for temporary buffers. .El .Sh SEE ALSO .Xr localeconv 3 , .Xr xlocale 3 .Sh STANDARDS The .Fn strfmon function conforms to .St -p1003.1-2001 . The .Fn strfmon_l function conforms to .St -p1003.1-2008 . .Sh AUTHORS .An -nosplit The .Fn strfmon function was implemented by .An Alexey Zelkin Aq Mt phantom@FreeBSD.org . .Pp This manual page was written by .An Jeroen Ruigrok van der Werven Aq Mt asmodai@FreeBSD.org based on the standards' text. .Sh BUGS The .Fn strfmon function does not correctly handle multibyte characters in the .Fa format argument. diff --git a/lib/libc/stdlib/strfmon.c b/lib/libc/stdlib/strfmon.c index caccbdb476b0..2dfc7d99510e 100644 --- a/lib/libc/stdlib/strfmon.c +++ b/lib/libc/stdlib/strfmon.c @@ -1,668 +1,670 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2001 Alexey Zelkin * All rights reserved. * * Copyright (c) 2011 The FreeBSD Foundation * * Portions of this software were developed by David Chisnall * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * */ #include #include #include #include #include #include #include #include #include #include #include #include "xlocale_private.h" /* internal flags */ #define NEED_GROUPING 0x01 /* print digits grouped (default) */ #define SIGN_POSN_USED 0x02 /* '+' or '(' usage flag */ #define LOCALE_POSN 0x04 /* use locale defined +/- (default) */ #define PARENTH_POSN 0x08 /* enclose negative amount in () */ #define SUPPRESS_CURR_SYMBOL 0x10 /* suppress the currency from output */ #define LEFT_JUSTIFY 0x20 /* left justify */ #define USE_INTL_CURRENCY 0x40 /* use international currency symbol */ #define IS_NEGATIVE 0x80 /* is argument value negative ? */ /* internal macros */ #define PRINT(CH) do { \ if (dst >= s + maxsize) \ goto e2big_error; \ *dst++ = CH; \ } while (0) #define PRINTS(STR) do { \ char *tmps = STR; \ while (*tmps != '\0') \ PRINT(*tmps++); \ } while (0) #define GET_NUMBER(VAR, LOC) do { \ VAR = 0; \ while (isdigit_l((unsigned char)*fmt, LOC)) { \ if (VAR > INT_MAX / 10) \ goto e2big_error; \ VAR *= 10; \ VAR += *fmt - '0'; \ if (VAR < 0) \ goto e2big_error; \ fmt++; \ } \ } while (0) #define GRPCPY(howmany) do { \ int i = howmany; \ while (i-- > 0) { \ avalue_size--; \ *--bufend = *(avalue + avalue_size + padded); \ } \ } while (0) #define GRPSEP do { \ bufend -= thousands_sep_size; \ memcpy(bufend, thousands_sep, thousands_sep_size); \ groups++; \ } while (0) static void __setup_vars(int, char *, char *, char *, char **, struct lconv *); static int __calc_left_pad(int, char *, struct lconv *); static char *__format_grouped_double(double, int *, int, int, int, struct lconv *, locale_t); static ssize_t vstrfmon_l(char *__restrict s, size_t maxsize, locale_t loc, const char *__restrict format, va_list ap) { char *dst; /* output destination pointer */ const char *fmt; /* current format position pointer */ struct lconv *lc; /* pointer to lconv structure */ char *asciivalue; /* formatted double pointer */ int flags; /* formatting options */ int pad_char; /* padding character */ int pad_size; /* pad size */ int width; /* field width */ int left_prec; /* left precision */ int right_prec; /* right precision */ double value; /* just value */ char space_char = ' '; /* space after currency */ char cs_precedes, /* values gathered from struct lconv */ sep_by_space, sign_posn, *signstr, *currency_symbol; char *tmpptr; /* temporary vars */ int sverrno; FIX_LOCALE(loc); lc = localeconv_l(loc); dst = s; fmt = format; asciivalue = NULL; currency_symbol = NULL; while (*fmt != 0) { /* pass nonformating characters AS IS */ if (*fmt != '%') goto literal; /* '%' found ! */ /* "%%" mean just '%' */ if (*(fmt + 1) == '%') { fmt++; literal: PRINT(*fmt++); continue; } /* set up initial values */ flags = NEED_GROUPING | LOCALE_POSN; pad_char = ' '; /* padding character is "space" */ pad_size = 0; /* no padding initially */ left_prec = -1; /* no left precision specified */ right_prec = -1; /* no right precision specified */ width = -1; /* no width specified */ /* Flags */ while (1) { switch (*++fmt) { case '=': /* fill character */ pad_char = *++fmt; if (pad_char == '\0') goto format_error; continue; case '^': /* not group currency */ flags &= ~(NEED_GROUPING); continue; case '+': /* use locale defined signs */ - if (flags & SIGN_POSN_USED) + if ((flags & SIGN_POSN_USED) || + ((lc->positive_sign[0] == '\0') && + (lc->negative_sign[0] == '\0'))) goto format_error; flags |= (SIGN_POSN_USED | LOCALE_POSN); continue; case '(': /* enclose negatives with () */ if (flags & SIGN_POSN_USED) goto format_error; flags |= (SIGN_POSN_USED | PARENTH_POSN); continue; case '!': /* suppress currency symbol */ flags |= SUPPRESS_CURR_SYMBOL; continue; case '-': /* alignment (left) */ flags |= LEFT_JUSTIFY; continue; default: break; } break; } /* field Width */ if (isdigit_l((unsigned char)*fmt, loc)) { GET_NUMBER(width, loc); /* * Do we have enough space to put number with * required width ? */ if ((unsigned int)width >= maxsize - (dst - s)) goto e2big_error; } /* Left precision */ if (*fmt == '#') { if (!isdigit_l((unsigned char)*++fmt, loc)) goto format_error; GET_NUMBER(left_prec, loc); if ((unsigned int)left_prec >= maxsize - (dst - s)) goto e2big_error; } /* Right precision */ if (*fmt == '.') { if (!isdigit_l((unsigned char)*++fmt, loc)) goto format_error; GET_NUMBER(right_prec, loc); if ((unsigned int)right_prec >= maxsize - (dst - s) - left_prec) goto e2big_error; } /* Conversion Characters */ switch (*fmt++) { case 'i': /* use international currency format */ flags |= USE_INTL_CURRENCY; break; case 'n': /* use national currency format */ flags &= ~(USE_INTL_CURRENCY); break; default: /* * required character is missing or * premature EOS */ goto format_error; } if (currency_symbol != NULL) free(currency_symbol); if (flags & USE_INTL_CURRENCY) { currency_symbol = strdup(lc->int_curr_symbol); if (currency_symbol != NULL && strlen(currency_symbol) > 3) { space_char = currency_symbol[3]; currency_symbol[3] = '\0'; } } else currency_symbol = strdup(lc->currency_symbol); if (currency_symbol == NULL) goto end_error; /* ENOMEM. */ /* value itself */ value = va_arg(ap, double); /* detect sign */ if (value < 0) { flags |= IS_NEGATIVE; value = -value; } /* fill left_prec with amount of padding chars */ if (left_prec >= 0) { pad_size = __calc_left_pad((flags ^ IS_NEGATIVE), currency_symbol, lc) - __calc_left_pad(flags, currency_symbol, lc); if (pad_size < 0) pad_size = 0; } if (asciivalue != NULL) free(asciivalue); asciivalue = __format_grouped_double(value, &flags, left_prec, right_prec, pad_char, lc, loc); if (asciivalue == NULL) goto end_error; /* * errno already set to ENOMEM by * malloc() */ /* set some variables for later use */ __setup_vars(flags, &cs_precedes, &sep_by_space, &sign_posn, &signstr, lc); /* * Description of some LC_MONETARY's values: * * p_cs_precedes & n_cs_precedes * * = 1 - $currency_symbol precedes the value * for a monetary quantity with a non-negative value * = 0 - symbol succeeds the value * * p_sep_by_space & n_sep_by_space * * = 0 - no space separates $currency_symbol * from the value for a monetary quantity with a * non-negative value * = 1 - space separates the symbol from the value * = 2 - space separates the symbol and the sign string, * if adjacent; otherwise, a space separates * the sign string from the value * * p_sign_posn & n_sign_posn * * = 0 - parentheses enclose the quantity and the * $currency_symbol * = 1 - the sign string precedes the quantity and the * $currency_symbol * = 2 - the sign string succeeds the quantity and the * $currency_symbol * = 3 - the sign string precedes the $currency_symbol * = 4 - the sign string succeeds the $currency_symbol */ tmpptr = dst; while (pad_size-- > 0) PRINT(' '); if (sign_posn == 0 && (flags & IS_NEGATIVE)) PRINT('('); if (cs_precedes == 1) { if (sign_posn == 1 || sign_posn == 3) { PRINTS(signstr); if (sep_by_space == 2) PRINT(' '); } if (!(flags & SUPPRESS_CURR_SYMBOL)) { PRINTS(currency_symbol); if (sign_posn == 4) { if (sep_by_space == 2) PRINT(space_char); PRINTS(signstr); if (sep_by_space == 1) PRINT(' '); } else if (sep_by_space == 1) PRINT(space_char); } } else if (sign_posn == 1) { PRINTS(signstr); if (sep_by_space == 2) PRINT(' '); } PRINTS(asciivalue); if (cs_precedes == 0) { if (sign_posn == 3) { if (sep_by_space == 1) PRINT(' '); PRINTS(signstr); } if (!(flags & SUPPRESS_CURR_SYMBOL)) { if ((sign_posn == 3 && sep_by_space == 2) || (sep_by_space == 1 && (sign_posn == 0 || sign_posn == 1 || sign_posn == 2 || sign_posn == 4))) PRINT(space_char); PRINTS(currency_symbol); if (sign_posn == 4) { if (sep_by_space == 2) PRINT(' '); PRINTS(signstr); } } } if (sign_posn == 2) { if (sep_by_space == 2) PRINT(' '); PRINTS(signstr); } if (sign_posn == 0) { if (flags & IS_NEGATIVE) PRINT(')'); else if (left_prec >= 0) PRINT(' '); } if (dst - tmpptr < width) { if (flags & LEFT_JUSTIFY) { while (dst - tmpptr < width) PRINT(' '); } else { pad_size = dst - tmpptr; memmove(tmpptr + width - pad_size, tmpptr, pad_size); memset(tmpptr, ' ', width - pad_size); dst += width - pad_size; } } } PRINT('\0'); free(asciivalue); free(currency_symbol); return (dst - s - 1); /* size of put data except trailing '\0' */ e2big_error: errno = E2BIG; goto end_error; format_error: errno = EINVAL; end_error: sverrno = errno; if (asciivalue != NULL) free(asciivalue); if (currency_symbol != NULL) free(currency_symbol); errno = sverrno; return (-1); } static void __setup_vars(int flags, char *cs_precedes, char *sep_by_space, char *sign_posn, char **signstr, struct lconv *lc) { if ((flags & IS_NEGATIVE) && (flags & USE_INTL_CURRENCY)) { *cs_precedes = lc->int_n_cs_precedes; *sep_by_space = lc->int_n_sep_by_space; *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->int_n_sign_posn; *signstr = (lc->negative_sign[0] == '\0') ? "-" : lc->negative_sign; } else if (flags & USE_INTL_CURRENCY) { *cs_precedes = lc->int_p_cs_precedes; *sep_by_space = lc->int_p_sep_by_space; *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->int_p_sign_posn; *signstr = lc->positive_sign; } else if (flags & IS_NEGATIVE) { *cs_precedes = lc->n_cs_precedes; *sep_by_space = lc->n_sep_by_space; *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->n_sign_posn; *signstr = (lc->negative_sign[0] == '\0') ? "-" : lc->negative_sign; } else { *cs_precedes = lc->p_cs_precedes; *sep_by_space = lc->p_sep_by_space; *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->p_sign_posn; *signstr = lc->positive_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; } static int __calc_left_pad(int flags, char *cur_symb, struct lconv *lc) { char cs_precedes, sep_by_space, sign_posn, *signstr; int left_chars = 0; __setup_vars(flags, &cs_precedes, &sep_by_space, &sign_posn, &signstr, lc); if (cs_precedes != 0) { left_chars += strlen(cur_symb); if (sep_by_space != 0) left_chars++; } switch (sign_posn) { case 0: if (flags & IS_NEGATIVE) left_chars++; break; case 1: left_chars += strlen(signstr); break; case 3: case 4: if (cs_precedes != 0) left_chars += strlen(signstr); } return (left_chars); } static int get_groups(int size, char *grouping) { int chars = 0; if (*grouping == CHAR_MAX || *grouping <= 0) /* no grouping ? */ return (0); while (size > (int)*grouping) { chars++; size -= (int)*grouping++; /* no more grouping ? */ if (*grouping == CHAR_MAX) break; /* rest grouping with same value ? */ if (*grouping == 0) { chars += (size - 1) / *(grouping - 1); break; } } return (chars); } /* convert double to locale-encoded string */ static char * __format_grouped_double(double value, int *flags, int left_prec, int right_prec, int pad_char, struct lconv *lc, locale_t loc) { char *rslt; char *avalue; int avalue_size; size_t bufsize; char *bufend; int padded; char *grouping; const char *decimal_point; const char *thousands_sep; size_t decimal_point_size; size_t thousands_sep_size; int groups = 0; grouping = lc->mon_grouping; 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; decimal_point_size = strlen(decimal_point); thousands_sep_size = strlen(thousands_sep); /* fill left_prec with default value */ if (left_prec == -1) left_prec = 0; /* fill right_prec with default value */ if (right_prec == -1) { if (*flags & USE_INTL_CURRENCY) right_prec = lc->int_frac_digits; else right_prec = lc->frac_digits; if (right_prec == CHAR_MAX) /* POSIX locale ? */ right_prec = 2; } if (*flags & NEED_GROUPING) left_prec += get_groups(left_prec, grouping); /* convert to string */ avalue_size = asprintf_l(&avalue, loc, "%*.*f", left_prec + right_prec + 1, right_prec, value); if (avalue_size < 0) return (NULL); /* make sure that we've enough space for result string */ bufsize = avalue_size * (1 + thousands_sep_size) + decimal_point_size + 1; rslt = calloc(1, bufsize); if (rslt == NULL) { free(avalue); return (NULL); } bufend = rslt + bufsize - 1; /* reserve space for trailing '\0' */ /* skip spaces at beginning */ padded = 0; while (avalue[padded] == ' ') { padded++; avalue_size--; } if (right_prec > 0) { bufend -= right_prec; memcpy(bufend, avalue + avalue_size + padded - right_prec, right_prec); bufend -= decimal_point_size; memcpy(bufend, decimal_point, decimal_point_size); avalue_size -= (right_prec + 1); } if ((*flags & NEED_GROUPING) && thousands_sep_size > 0 && *grouping != CHAR_MAX && *grouping > 0) { while (avalue_size > (int)*grouping) { GRPCPY(*grouping); GRPSEP; grouping++; /* no more grouping ? */ if (*grouping == CHAR_MAX) break; /* rest grouping with same value ? */ if (*grouping == 0) { grouping--; while (avalue_size > *grouping) { GRPCPY(*grouping); GRPSEP; } } } if (avalue_size != 0) GRPCPY(avalue_size); padded -= groups; } else { bufend -= avalue_size; memcpy(bufend, avalue + padded, avalue_size); /* decrease assumed $decimal_point */ if (right_prec == 0) padded -= decimal_point_size; } /* do padding with pad_char */ if (padded > 0) { bufend -= padded; memset(bufend, pad_char, padded); } bufsize = rslt + bufsize - bufend; memmove(rslt, bufend, bufsize); free(avalue); return (rslt); } ssize_t strfmon(char *restrict s, size_t maxsize, const char *restrict format, ...) { ssize_t ret; va_list ap; va_start(ap, format); ret = vstrfmon_l(s, maxsize, __get_locale(), format, ap); va_end(ap); return (ret); } ssize_t strfmon_l(char *restrict s, size_t maxsize, locale_t loc, const char *restrict format, ...) { ssize_t ret; va_list ap; va_start(ap, format); ret = vstrfmon_l(s, maxsize, loc, format, ap); va_end(ap); return (ret); } diff --git a/lib/libc/tests/stdlib/strfmon_test.c b/lib/libc/tests/stdlib/strfmon_test.c index 0126c2f3f8a8..7fc0afc9e229 100644 --- a/lib/libc/tests/stdlib/strfmon_test.c +++ b/lib/libc/tests/stdlib/strfmon_test.c @@ -1,287 +1,287 @@ /* * Copyright (c) 2018 Conrad Meyer * All rights reserved. * Copyright (c) 2022-2025 Jose Luis Duran * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include ATF_TC_WITHOUT_HEAD(strfmon_locale_thousands); ATF_TC_BODY(strfmon_locale_thousands, tc) { char actual[40], expected[40]; struct lconv *lc; const char *ts; double n; setlocale(LC_MONETARY, "sv_SE.UTF-8"); lc = localeconv(); ts = lc->mon_thousands_sep; if (strlen(ts) == 0) ts = lc->thousands_sep; if (strlen(ts) < 2) atf_tc_skip("multi-byte thousands-separator not found"); n = 1234.56; strfmon(actual, sizeof(actual) - 1, "%i", n); strcpy(expected, "1"); strlcat(expected, ts, sizeof(expected)); strlcat(expected, "234", sizeof(expected)); /* We're just testing the thousands separator, not all of strfmon. */ actual[strlen(expected)] = '\0'; ATF_CHECK_STREQ(expected, actual); } ATF_TC_WITHOUT_HEAD(strfmon_examples); ATF_TC_BODY(strfmon_examples, tc) { const struct { const char *format; const char *expected; } tests[] = { { "%n", "[$123.45] [-$123.45] [$3,456.78]" }, { "%11n", "[ $123.45] [ -$123.45] [ $3,456.78]" }, { "%#5n", "[ $ 123.45] [-$ 123.45] [ $ 3,456.78]" }, { "%=*#5n", "[ $***123.45] [-$***123.45] [ $*3,456.78]" }, { "%=0#5n", "[ $000123.45] [-$000123.45] [ $03,456.78]" }, { "%^#5n", "[ $ 123.45] [-$ 123.45] [ $ 3456.78]" }, { "%^#5.0n", "[ $ 123] [-$ 123] [ $ 3457]" }, { "%^#5.4n", "[ $ 123.4500] [-$ 123.4500] [ $ 3456.7810]" }, { "%(#5n", "[ $ 123.45 ] [($ 123.45)] [ $ 3,456.78 ]" }, { "%!(#5n", "[ 123.45 ] [( 123.45)] [ 3,456.78 ]" }, { "%-14#5.4n", "[ $ 123.4500 ] [-$ 123.4500 ] [ $ 3,456.7810 ]" }, { "%14#5.4n", "[ $ 123.4500] [ -$ 123.4500] [ $ 3,456.7810]" }, }; size_t i; char actual[100], format[50]; if (setlocale(LC_MONETARY, "en_US.UTF-8") == NULL) atf_tc_skip("unable to setlocale()"); for (i = 0; i < nitems(tests); ++i) { snprintf(format, sizeof(format), "[%s] [%s] [%s]", tests[i].format, tests[i].format, tests[i].format); strfmon(actual, sizeof(actual) - 1, format, 123.45, -123.45, 3456.781); ATF_CHECK_STREQ_MSG(tests[i].expected, actual, "[%s]", tests[i].format); } } ATF_TC(strfmon_cs_precedes_0); ATF_TC_HEAD(strfmon_cs_precedes_0, tc) { atf_tc_set_md_var(tc, "descr", "sep_by_space x sign_posn when cs_precedes = 0"); } ATF_TC_BODY(strfmon_cs_precedes_0, tc) { const struct { const char *expected; } tests[] = { /* sep_by_space x sign_posn */ { "[(123.00$)] [-123.00$] [123.00$-] [123.00-$] [123.00$-]" }, { "[(123.00 $)] [-123.00 $] [123.00 $-] [123.00 -$] [123.00 $-]" }, { "[(123.00$)] [- 123.00$] [123.00$ -] [123.00- $] [123.00$ -]" }, }; size_t i, j; struct lconv *lc; char actual[100], buf[100]; if (setlocale(LC_MONETARY, "en_US.UTF-8") == NULL) atf_tc_skip("unable to setlocale()"); lc = localeconv(); lc->n_cs_precedes = 0; for (i = 0; i < nitems(tests); ++i) { actual[0] = '\0'; lc->n_sep_by_space = i; for (j = 0; j < 5; ++j) { lc->n_sign_posn = j; strfmon(buf, sizeof(buf) - 1, "[%n] ", -123.0); strlcat(actual, buf, sizeof(actual)); } actual[strlen(actual) - 1] = '\0'; ATF_CHECK_STREQ_MSG(tests[i].expected, actual, "sep_by_space = %zu", i); } } ATF_TC(strfmon_cs_precedes_1); ATF_TC_HEAD(strfmon_cs_precedes_1, tc) { atf_tc_set_md_var(tc, "descr", "sep_by_space x sign_posn when cs_precedes = 1"); } ATF_TC_BODY(strfmon_cs_precedes_1, tc) { const struct { const char *expected; } tests[] = { /* sep_by_space x sign_posn */ { "[($123.00)] [-$123.00] [$123.00-] [-$123.00] [$-123.00]" }, { "[($ 123.00)] [-$ 123.00] [$ 123.00-] [-$ 123.00] [$- 123.00]" }, { "[($123.00)] [- $123.00] [$123.00 -] [- $123.00] [$ -123.00]" }, }; size_t i, j; struct lconv *lc; char actual[100], buf[100]; if (setlocale(LC_MONETARY, "en_US.UTF-8") == NULL) atf_tc_skip("unable to setlocale()"); lc = localeconv(); lc->n_cs_precedes = 1; for (i = 0; i < nitems(tests); ++i) { actual[0] = '\0'; lc->n_sep_by_space = i; for (j = 0; j < 5; ++j) { lc->n_sign_posn = j; strfmon(buf, sizeof(buf) - 1, "[%n] ", -123.0); strlcat(actual, buf, sizeof(actual)); } actual[strlen(actual) - 1] = '\0'; ATF_CHECK_STREQ_MSG(tests[i].expected, actual, "sep_by_space = %zu", i); } } ATF_TC_WITHOUT_HEAD(strfmon_international_currency_code); ATF_TC_BODY(strfmon_international_currency_code, tc) { const struct { const char *locale; const char *expected; } tests[] = { { "en_US.UTF-8", "[USD123.45]" }, { "de_DE.UTF-8", "[123,45 EUR]" }, { "C", "[123.45]" }, }; size_t i; char actual[100]; for (i = 0; i < nitems(tests); ++i) { if (setlocale(LC_MONETARY, tests[i].locale) == NULL) atf_tc_skip("unable to setlocale()"); strfmon(actual, sizeof(actual) - 1, "[%i]", 123.45); ATF_CHECK_STREQ(tests[i].expected, actual); } } ATF_TC_WITHOUT_HEAD(strfmon_plus_or_parenthesis); ATF_TC_BODY(strfmon_plus_or_parenthesis, tc) { const struct { const char *format; const char *locale; const char *expected; } tests[] = { { "%+n", "en_US.UTF-8", "[$123.45] [-$123.45]" }, { "%+i", "en_US.UTF-8", "[USD123.45] [-USD123.45]" }, { "%(n", "C", "[123.45] [(123.45)]" }, { "%(i", "C", "[123.45] [(123.45)]" }, { "%(n", "en_US.UTF-8", "[$123.45] [($123.45)]" }, { "%(i", "en_US.UTF-8", "[USD123.45] [(USD123.45)]" }, { "%n", "C", "[123.45] [(123.45)]" }, /* XXX */ { "%i", "C", "[123.45] [(123.45)]" }, /* XXX */ { "%n", "en_US.UTF-8", "[$123.45] [-$123.45]" }, { "%i", "en_US.UTF-8", "[USD123.45] [-USD123.45]" }, }; size_t i; char actual[100], format[50]; for (i = 0; i < nitems(tests); ++i) { if (setlocale(LC_MONETARY, tests[i].locale) == NULL) atf_tc_skip("unable to setlocale(): %s", tests[i].locale); snprintf(format, sizeof(format), "[%s] [%s]", tests[i].format, tests[i].format); strfmon(actual, sizeof(actual) - 1, format, 123.45, -123.45); ATF_CHECK_STREQ_MSG(tests[i].expected, actual, "[%s] %s", tests[i].format, tests[i].locale); } /* * The '+' flag was included in a conversion specification and * the locale's positive_sign and negative_sign values would * both be returned by localeconv() as empty strings. */ if (setlocale(LC_MONETARY, "C") == NULL) atf_tc_skip("unable to setlocale(): %s", tests[i].locale); - /* ATF_CHECK_ERRNO(EINVAL, strfmon(actual, sizeof(actual) - 1, - "[%+n] [%+n]", 123.45, -123.45)); XXX */ + ATF_CHECK_ERRNO(EINVAL, strfmon(actual, sizeof(actual) - 1, + "[%+n] [%+n]", 123.45, -123.45)); - /* ATF_CHECK_ERRNO(EINVAL, strfmon(actual, sizeof(actual) - 1, - "[%+i] [%+i]", 123.45, -123.45)); XXX */ + ATF_CHECK_ERRNO(EINVAL, strfmon(actual, sizeof(actual) - 1, + "[%+i] [%+i]", 123.45, -123.45)); } ATF_TC(strfmon_l); ATF_TC_HEAD(strfmon_l, tc) { atf_tc_set_md_var(tc, "descr", "checks strfmon_l under different locales"); } ATF_TC_BODY(strfmon_l, tc) { const struct { const char *locale; const char *expected; } tests[] = { { "C", "[ **1234.57 ] [ **1234.57 ]" }, /* XXX */ { "de_DE.UTF-8", "[ **1234,57 €] [ **1.234,57 EUR]" }, { "en_GB.UTF-8", "[ £**1234.57] [ GBP**1,234.57]" }, }; locale_t loc; size_t i; char buf[100]; for (i = 0; i < nitems(tests); ++i) { loc = newlocale(LC_MONETARY_MASK, tests[i].locale, NULL); ATF_REQUIRE(loc != NULL); strfmon_l(buf, sizeof(buf) - 1, loc, "[%^=*#6n] [%=*#6i]", 1234.567, 1234.567); ATF_REQUIRE_STREQ(tests[i].expected, buf); freelocale(loc); } } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, strfmon_locale_thousands); ATF_TP_ADD_TC(tp, strfmon_examples); ATF_TP_ADD_TC(tp, strfmon_cs_precedes_0); ATF_TP_ADD_TC(tp, strfmon_cs_precedes_1); ATF_TP_ADD_TC(tp, strfmon_international_currency_code); ATF_TP_ADD_TC(tp, strfmon_plus_or_parenthesis); ATF_TP_ADD_TC(tp, strfmon_l); return (atf_no_error()); }