1 /*- 2 * Copyright (c) 2001 Alexey Zelkin <phantom@FreeBSD.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 * SUCH DAMAGE. 25 * 26 */ 27 28 #include <sys/cdefs.h> 29 __FBSDID("$FreeBSD$"); 30 31 #include <sys/types.h> 32 #include <ctype.h> 33 #include <errno.h> 34 #include <limits.h> 35 #include <locale.h> 36 #if __STDC__ 37 #include <stdarg.h> 38 #else 39 #include <varargs.h> 40 #endif 41 #include <stdio.h> 42 #include <stdlib.h> 43 #include <string.h> 44 45 /* internal flags */ 46 #define NEED_GROUPING 0x01 /* print digits grouped (default) */ 47 #define SIGN_POSN_USED 0x02 /* '+' or '(' usage flag */ 48 #define LOCALE_POSN 0x04 /* use locale defined +/- (default) */ 49 #define PARENTH_POSN 0x08 /* enclose negative amount in () */ 50 #define SUPRESS_CURR_SYMBOL 0x10 /* supress the currency from output */ 51 #define LEFT_JUSTIFY 0x20 /* left justify */ 52 #define USE_INTL_CURRENCY 0x40 /* use international currency symbol */ 53 #define IS_NEGATIVE 0x80 /* is argument value negative ? */ 54 55 /* internal macros */ 56 #define PRINT(CH) do { \ 57 if (dst >= s + maxsize) \ 58 goto e2big_error; \ 59 *dst++ = CH; \ 60 } while (0) 61 62 #define PRINTS(STR) do { \ 63 char *tmps = STR; \ 64 while (*tmps != '\0') \ 65 PRINT(*tmps++); \ 66 } while (0) 67 68 #define GET_NUMBER(VAR) do { \ 69 VAR = 0; \ 70 while (isdigit((unsigned char)*fmt)) { \ 71 VAR *= 10; \ 72 VAR += *fmt - '0'; \ 73 fmt++; \ 74 } \ 75 } while (0) 76 77 #define GRPCPY(howmany) do { \ 78 int i = howmany; \ 79 while (i-- > 0) { \ 80 avalue_size--; \ 81 *--bufend = *(avalue+avalue_size+padded); \ 82 } \ 83 } while (0) 84 85 #define GRPSEP do { \ 86 *--bufend = thousands_sep; \ 87 groups++; \ 88 } while (0) 89 90 static void __setup_vars(int, char *, char *, char *, char **); 91 static int __calc_left_pad(int, char *); 92 static char *__format_grouped_double(double, int *, int, int, int); 93 94 #if __STDC__ 95 ssize_t 96 strfmon(char *s, size_t maxsize, const char *format, ...) 97 #else 98 ssize_t 99 strfmon(s, maxsize, format, va_alist) 100 char *s; 101 size_t maxsize; 102 const char *format; 103 va_dcl 104 #endif 105 { 106 va_list ap; 107 char *dst; /* output destination pointer */ 108 const char *fmt; /* current format poistion pointer */ 109 struct lconv *lc; /* pointer to lconv structure */ 110 char *asciivalue; /* formatted double pointer */ 111 112 int flags; /* formatting options */ 113 int pad_char; /* padding character */ 114 int pad_size; /* pad size */ 115 int width; /* field width */ 116 int left_prec; /* left precision */ 117 int right_prec; /* right precision */ 118 double value; /* just value */ 119 char space_char = ' '; /* space after currency */ 120 121 char cs_precedes, /* values gathered from struct lconv */ 122 sep_by_space, 123 sign_posn, 124 *signstr, 125 *currency_symbol; 126 127 char *tmpptr; /* temporary vars */ 128 int *ntmp; 129 130 #if __STDC__ 131 va_start(ap, format); 132 #else 133 va_start(ap); 134 #endif 135 136 lc = localeconv(); 137 dst = s; 138 fmt = format; 139 asciivalue = NULL; 140 currency_symbol = NULL; 141 pad_size = 0; 142 143 while (*fmt) { 144 /* pass nonformating characters AS IS */ 145 if (*fmt != '%') 146 goto literal; 147 148 /* '%' found ! */ 149 150 /* "%%" mean just '%' */ 151 if (*(fmt+1) == '%') { 152 fmt++; 153 literal: 154 PRINT(*fmt++); 155 continue; 156 } 157 158 /* set up initial values */ 159 flags = (NEED_GROUPING|LOCALE_POSN); 160 pad_char = ' '; /* padding character is "space" */ 161 left_prec = -1; /* no left precision specified */ 162 right_prec = -1; /* no right precision specified */ 163 width = -1; /* no width specified */ 164 value = 0; /* we have no value to print now */ 165 166 /* Flags */ 167 while (1) { 168 switch (*++fmt) { 169 case '=': /* fill character */ 170 pad_char = *++fmt; 171 if (pad_char == '\0') 172 goto format_error; 173 continue; 174 case '^': /* not group currency */ 175 flags &= ~(NEED_GROUPING); 176 continue; 177 case '+': /* use locale defined signs */ 178 if (flags & SIGN_POSN_USED) 179 goto format_error; 180 flags |= (SIGN_POSN_USED|LOCALE_POSN); 181 continue; 182 case '(': /* enclose negatives with () */ 183 if (flags & SIGN_POSN_USED) 184 goto format_error; 185 flags |= (SIGN_POSN_USED|PARENTH_POSN); 186 continue; 187 case '!': /* suppress currency symbol */ 188 flags |= SUPRESS_CURR_SYMBOL; 189 continue; 190 case '-': /* alignment (left) */ 191 flags |= LEFT_JUSTIFY; 192 continue; 193 case '#': /* left || right precision */ 194 case '.': 195 if (*fmt == '#') 196 ntmp = &left_prec; 197 else 198 ntmp = &right_prec; 199 200 if (!isdigit((unsigned char)*++fmt)) 201 goto format_error; 202 GET_NUMBER(*ntmp); 203 fmt--; 204 continue; 205 default: 206 break; 207 } 208 break; 209 } 210 211 /* field Width */ 212 if (isdigit((unsigned char)*fmt)) { 213 GET_NUMBER(width); 214 /* Do we have enough space to put number with 215 * required width ? 216 */ 217 if (dst + width >= s + maxsize) 218 goto e2big_error; 219 } 220 221 /* Conversion Characters */ 222 switch (*fmt++) { 223 case 'i': /* use internaltion currency format */ 224 flags |= USE_INTL_CURRENCY; 225 break; 226 case 'n': /* use national currency format */ 227 flags &= ~(USE_INTL_CURRENCY); 228 break; 229 default: /* required character is missing or 230 premature EOS */ 231 goto format_error; 232 } 233 234 if (flags & USE_INTL_CURRENCY) { 235 currency_symbol = strdup(lc->int_curr_symbol); 236 if (currency_symbol != NULL) 237 space_char = *(currency_symbol+3); 238 } else 239 currency_symbol = strdup(lc->currency_symbol); 240 241 if (currency_symbol == NULL) 242 goto end_error; /* ENOMEM. */ 243 244 /* value itself */ 245 value = va_arg(ap, double); 246 247 /* detect sign */ 248 if (value < 0) { 249 flags |= IS_NEGATIVE; 250 value = -value; 251 } 252 253 /* fill left_prec with amount of padding chars */ 254 if (left_prec >= 0) { 255 pad_size = __calc_left_pad((flags ^ IS_NEGATIVE), 256 currency_symbol) - 257 __calc_left_pad(flags, currency_symbol); 258 if (pad_size < 0) 259 pad_size = 0; 260 } 261 262 asciivalue = __format_grouped_double(value, &flags, 263 left_prec, right_prec, pad_char); 264 if (asciivalue == NULL) 265 goto end_error; /* errno already set */ 266 /* to ENOMEM by malloc() */ 267 268 /* set some variables for later use */ 269 __setup_vars(flags, &cs_precedes, &sep_by_space, 270 &sign_posn, &signstr); 271 272 /* 273 * Description of some LC_MONETARY's values: 274 * 275 * p_cs_precedes & n_cs_precedes 276 * 277 * = 1 - $currency_symbol precedes the value 278 * for a monetary quantity with a non-negative value 279 * = 0 - symbol succeeds the value 280 * 281 * p_sep_by_space & n_sep_by_space 282 * 283 * = 0 - no space separates $currency_symbol 284 * from the value for a monetary quantity with a 285 * non-negative value 286 * = 1 - space separates the symbol from the value 287 * = 2 - space separates the symbol and the sign string, 288 * if adjacent. 289 * 290 * p_sign_posn & n_sign_posn 291 * 292 * = 0 - parentheses enclose the quantity and the 293 * $currency_symbol 294 * = 1 - the sign string precedes the quantity and the 295 * $currency_symbol 296 * = 2 - the sign string succeeds the quantity and the 297 * $currency_symbol 298 * = 3 - the sign string precedes the $currency_symbol 299 * = 4 - the sign string succeeds the $currency_symbol 300 * 301 */ 302 303 tmpptr = dst; 304 305 while (pad_size-- > 0) 306 PRINT(' '); 307 308 if (sign_posn == 0) { 309 if (flags & IS_NEGATIVE) 310 PRINT('('); 311 else 312 PRINT(' '); 313 } 314 315 if (cs_precedes == 1) { 316 if (sign_posn == 1 || sign_posn == 3) { 317 PRINTS(signstr); 318 if (sep_by_space == 2) /* XXX: ? */ 319 PRINT(' '); 320 } 321 322 if (!(flags & SUPRESS_CURR_SYMBOL)) { 323 PRINTS(currency_symbol); 324 325 if (sign_posn == 4) { 326 if (sep_by_space == 2) 327 PRINT(space_char); 328 PRINTS(signstr); 329 if (sep_by_space == 1) 330 PRINT(' '); 331 } else if (sep_by_space == 1) 332 PRINT(space_char); 333 } 334 } else if (sign_posn == 1) 335 PRINTS(signstr); 336 337 PRINTS(asciivalue); 338 339 if (cs_precedes == 0) { 340 if (sign_posn == 3) { 341 if (sep_by_space == 1) 342 PRINT(' '); 343 PRINTS(signstr); 344 } 345 346 if (!(flags & SUPRESS_CURR_SYMBOL)) { 347 if ((sign_posn == 3 && sep_by_space == 2) 348 || (sep_by_space == 1 349 && (sign_posn = 0 350 || sign_posn == 1 351 || sign_posn == 2 352 || sign_posn == 4))) 353 PRINT(space_char); 354 PRINTS(currency_symbol); /* XXX: len */ 355 if (sign_posn == 4) { 356 if (sep_by_space == 2) 357 PRINT(' '); 358 PRINTS(signstr); 359 } 360 } 361 } 362 363 if (sign_posn == 2) { 364 if (sep_by_space == 2) 365 PRINT(' '); 366 PRINTS(signstr); 367 } 368 369 if (sign_posn == 0 && (flags & IS_NEGATIVE)) 370 PRINT(')'); 371 372 if (dst - tmpptr < width) { 373 if (flags & LEFT_JUSTIFY) { 374 while (dst - tmpptr <= width) 375 PRINT(' '); 376 } else { 377 pad_size = dst-tmpptr; 378 memmove(tmpptr + width-pad_size, tmpptr, 379 pad_size); 380 memset(tmpptr, ' ', width-pad_size); 381 dst += width-pad_size; 382 } 383 } 384 } 385 386 PRINT('\0'); 387 va_end(ap); 388 return (dst - s - 1); /* return size of put data except trailing '\0' */ 389 390 e2big_error: 391 errno = E2BIG; 392 goto end_error; 393 394 format_error: 395 errno = EINVAL; 396 397 end_error: 398 if (asciivalue != NULL) 399 free(asciivalue); 400 if (currency_symbol != NULL) 401 free(currency_symbol); 402 va_end(ap); 403 return (-1); 404 } 405 406 static void 407 __setup_vars(int flags, char *cs_precedes, char *sep_by_space, 408 char *sign_posn, char **signstr) { 409 410 struct lconv *lc = localeconv(); 411 412 if (flags & IS_NEGATIVE) { 413 *cs_precedes = lc->n_cs_precedes; 414 *sep_by_space = lc->n_sep_by_space; 415 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->n_sign_posn; 416 *signstr = (lc->negative_sign == '\0') ? "-" 417 : lc->negative_sign; 418 } else { 419 *cs_precedes = lc->p_cs_precedes; 420 *sep_by_space = lc->p_sep_by_space; 421 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->p_sign_posn; 422 *signstr = lc->positive_sign; 423 } 424 425 /* Set defult values for unspecified information. */ 426 if (*cs_precedes != 0) 427 *cs_precedes = 1; 428 if (*sep_by_space == CHAR_MAX) 429 *sep_by_space = 0; 430 if (*sign_posn == CHAR_MAX) 431 *sign_posn = 0; 432 } 433 434 static int 435 __calc_left_pad(int flags, char *cur_symb) { 436 437 char cs_precedes, sep_by_space, sign_posn, *signstr; 438 int left_chars = 0; 439 440 __setup_vars(flags, &cs_precedes, &sep_by_space, &sign_posn, &signstr); 441 442 if (cs_precedes != 0) { 443 left_chars += strlen(cur_symb); 444 if (sep_by_space != 0) 445 left_chars++; 446 } 447 448 switch (sign_posn) { 449 case 1: 450 left_chars += strlen(signstr); 451 break; 452 case 3: 453 case 4: 454 if (cs_precedes != 0) 455 left_chars += strlen(signstr); 456 } 457 return (left_chars); 458 } 459 460 static int 461 get_groups(int size, char *grouping) { 462 463 int chars = 0; 464 465 if (*grouping == CHAR_MAX || *grouping <= 0) /* no grouping ? */ 466 return (0); 467 468 while (size > (int)*grouping) { 469 chars++; 470 size -= (int)*grouping++; 471 /* no more grouping ? */ 472 if (*grouping == CHAR_MAX) 473 break; 474 /* rest grouping with same value ? */ 475 if (*grouping == 0) { 476 chars += (size - 1) / *(grouping - 1); 477 break; 478 } 479 } 480 return (chars); 481 } 482 483 /* convert double to ASCII */ 484 static char * 485 __format_grouped_double(double value, int *flags, 486 int left_prec, int right_prec, int pad_char) { 487 488 char *rslt; 489 char *avalue; 490 int avalue_size; 491 char fmt[32]; 492 493 size_t bufsize; 494 char *bufend; 495 496 int padded; 497 498 struct lconv *lc = localeconv(); 499 char *grouping; 500 char decimal_point; 501 char thousands_sep; 502 503 int groups = 0; 504 505 grouping = lc->mon_grouping; 506 decimal_point = *lc->mon_decimal_point; 507 if (decimal_point == '\0') { 508 decimal_point = *lc->decimal_point; 509 if (decimal_point == '\0') 510 decimal_point = '.'; 511 } 512 thousands_sep = *lc->mon_thousands_sep; 513 if (thousands_sep == '\0') 514 thousands_sep = *lc->thousands_sep; 515 516 /* fill left_prec with default value */ 517 if (left_prec == -1) 518 left_prec = 0; 519 520 /* fill right_prec with default value */ 521 if (right_prec == -1) { 522 if (*flags & USE_INTL_CURRENCY) 523 right_prec = lc->int_frac_digits; 524 else 525 right_prec = lc->frac_digits; 526 527 if (right_prec == CHAR_MAX) /* POSIX locale ? */ 528 right_prec = 2; 529 } 530 531 if (*flags & NEED_GROUPING) 532 left_prec += get_groups(left_prec, grouping); 533 534 /* convert to string */ 535 snprintf(fmt, sizeof(fmt), "%%%d.%df", left_prec + right_prec + 1, 536 right_prec); 537 avalue_size = asprintf(&avalue, fmt, value); 538 if (avalue_size < 0) 539 return (NULL); 540 541 /* make sure that we've enough space for result string */ 542 bufsize = strlen(avalue)*2+1; 543 rslt = malloc(bufsize); 544 if (rslt == NULL) { 545 free(avalue); 546 return (NULL); 547 } 548 memset(rslt, 0, bufsize); 549 bufend = rslt + bufsize - 1; /* reserve space for trailing '\0' */ 550 551 /* skip spaces at beggining */ 552 padded = 0; 553 while (avalue[padded] == ' ') { 554 padded++; 555 avalue_size--; 556 } 557 558 if (right_prec > 0) { 559 bufend -= right_prec; 560 memcpy(bufend, avalue + avalue_size+padded-right_prec, 561 right_prec); 562 *--bufend = decimal_point; 563 avalue_size -= (right_prec + 1); 564 } 565 566 if ((*flags & NEED_GROUPING) && 567 thousands_sep != '\0' && /* XXX: need investigation */ 568 *grouping != CHAR_MAX && 569 *grouping > 0) { 570 while (avalue_size > (int)*grouping) { 571 GRPCPY(*grouping); 572 GRPSEP; 573 grouping++; 574 575 /* no more grouping ? */ 576 if (*grouping == CHAR_MAX) 577 break; 578 579 /* rest grouping with same value ? */ 580 if (*grouping == 0) { 581 grouping--; 582 while (avalue_size > *grouping) { 583 GRPCPY(*grouping); 584 GRPSEP; 585 } 586 } 587 } 588 if (avalue_size != 0) 589 GRPCPY(avalue_size); 590 padded -= groups; 591 592 } else { 593 bufend -= avalue_size; 594 memcpy(bufend, avalue+padded, avalue_size); 595 if (right_prec == 0) 596 padded--; /* decrease assumed $decimal_point */ 597 } 598 599 /* do padding with pad_char */ 600 if (padded > 0) { 601 bufend -= padded; 602 memset(bufend, pad_char, padded); 603 } 604 605 bufsize = bufsize - (rslt - bufend); 606 memmove(rslt, bufend, bufsize); 607 free(avalue); 608 return (rslt); 609 } 610