1 /*******************************************************************************
2 * Copyright (c) 2013-2021, Andrés Martinelli <andmarti@gmail.com> *
3 * All rights reserved. *
4 * *
5 * This file is a part of SC-IM *
6 * *
7 * SC-IM is a spreadsheet program that is based on SC. The original authors *
8 * of SC are James Gosling and Mark Weiser, and mods were later added by *
9 * Chuck Martin. *
10 * *
11 * Redistribution and use in source and binary forms, with or without *
12 * modification, are permitted provided that the following conditions are met: *
13 * 1. Redistributions of source code must retain the above copyright *
14 * notice, this list of conditions and the following disclaimer. *
15 * 2. Redistributions in binary form must reproduce the above copyright *
16 * notice, this list of conditions and the following disclaimer in the *
17 * documentation and/or other materials provided with the distribution. *
18 * 3. All advertising materials mentioning features or use of this software *
19 * must display the following acknowledgement: *
20 * This product includes software developed by Andrés Martinelli *
21 * <andmarti@gmail.com>. *
22 * 4. Neither the name of the Andrés Martinelli nor the *
23 * names of other contributors may be used to endorse or promote products *
24 * derived from this software without specific prior written permission. *
25 * *
26 * THIS SOFTWARE IS PROVIDED BY ANDRES MARTINELLI ''AS IS'' AND ANY *
27 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED *
28 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE *
29 * DISCLAIMED. IN NO EVENT SHALL ANDRES MARTINELLI BE LIABLE FOR ANY *
30 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES *
31 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;*
32 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND *
33 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *
34 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE *
35 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
36 *******************************************************************************/
37
38 /**
39 * \file format.c
40 * \author Andrés Martinelli <andmarti@gmail.com>
41 * \date 2017-07-18
42 * \brief TODO Write a tbrief file description.
43 */
44
45 /*
46 * Based on code of Mark Nagel <nagel@ics.uci.edu>, 20 July 1989
47 *
48 * int
49 * format(fmt, precision, num, buf, buflen)
50 * char *fmt;
51 * double num;
52 * char buf[];
53 * int buflen;
54 *
55 * The format function will produce a string representation of a number
56 * given a _format_ (described below) and a double value. The result is
57 * written into the passed buffer -- if the resulting string is too
58 * long to fit into the passed buffer, the function returns false.
59 * Otherwise the function returns true.
60 *
61 * The fmt parameter contains the format to use to convert the number.
62 *
63 * # Digit placeholder. If the number has fewer digits on either
64 * side of the decimal point than there are '#' characters in
65 * the format, the extra '#' characters are ignored. The number
66 * is rounded to the number of digit placeholders as there are
67 * to the right of the decimal point. If there are more digits
68 * in the number than there are digit placeholders on the left
69 * side of the decimal point, then those digits are displayed.
70 *
71 * 0 Digit placeholder. Same as for '#' except that the number
72 * is padded with zeroes on either side of the decimal point.
73 * The number of zeroes used in padding is determined by the
74 * number of digit placeholders after the '0' for digits on
75 * the left side of the decimal point and by the number of
76 * digit placeholders before the '0' for digits on the right
77 * side of the decimal point.
78 *
79 * . Decimal point. Determines how many digits are placed on
80 * the right and left sides of the decimal point in the number.
81 * Note that numbers smaller than 1 will begin with a decimal
82 * point if the left side of the decimal point contains only
83 * a '#' digit placeholder. Use a '0' placeholder to get a
84 * leading zero in decimal formats.
85 *
86 * % Percentage. For each '%' character in the format, the actual
87 * number gets multiplied by 100 (only for purposes of formatting
88 * -- the original number is left unmodified) and the '%' character
89 * is placed in the same position as it is in the format.
90 *
91 * , Thousands separator. The presence of a ',' in the format
92 * (multiple commas are treated as one) will cause the number
93 * to be formatted with a ',' separating each set of three digits
94 * in the integer part of the number with numbering beginning
95 * from the right end of the integer.
96 *
97 * & Precision. When this character is present in the fractional
98 * part of the number, it is equavalent to a number of 0's equal
99 * to the precision specified in the column format command. For
100 * example, if the precision is 3, "&" is equivalent to "000".
101 *
102 * \ Quote. This character causes the next character to be
103 * inserted into the formatted string directly with no
104 * special interpretation.
105 *
106 * E- E+ e- e+
107 * Scientific format. Causes the number to formatted in scientific
108 * notation. The case of the 'E' or 'e' given is preserved. If
109 * the format uses a '+', then the sign is always given for the
110 * exponent value. If the format uses a '-', then the sign is
111 * only given when the exponent value is negative. Note that if
112 * there is no digit placeholder following the '+' or '-', then
113 * that part of the formatted number is left out. In general,
114 * there should be one or more digit placeholders after the '+'
115 * or '-'.
116 *
117 * ; Format selector. Use this character to separate the format
118 * into two distinct formats. The format to the left of the
119 * ';' character will be used if the number given is zero or
120 * positive. The format to the right of the ';' character is
121 * used if the number given is negative.
122 *
123 * Any
124 * Self insert. Any other character will be inserted directly
125 * into the formatted number with no change made to the actual
126 * number.
127 */
128
129 #include <stdio.h>
130 #include <string.h>
131 #include <sys/types.h>
132 #include <time.h>
133 #include "sc.h"
134
135 #define true 1
136 #define false 0
137 #define EOS '\0'
138 #define MAXBUF 256
139
140 static char *fmt_int(char *val, char *fmt, int comma, int negative);
141 static char *fmt_frac(char *val, char *fmt, int lprecision);
142 static char *fmt_exp(int val, char *fmt);
143 static void reverse(register char *buf);
144 char * colformat[COLFORMATS];
145
146 /**
147 * \brief TODO Document format()
148 *
149 * \param[in] fmt
150 * \param[in] lprecision
151 * \param[in] val
152 * \param[in] buf
153 * \param[in] buflen
154 *
155 * returns: ret
156 */
157
format(char * fmt,int lprecision,double val,char * buf,int buflen)158 int format(char *fmt, int lprecision, double val, char *buf, int buflen) {
159 register char *cp;
160 char *tmp, *tp;
161 int comma = false, negative = false;
162 char *integer = NULL, *decimal = NULL;
163 char *exponent = NULL;
164 int exp_val = 0;
165 int width;
166 char prtfmt[32];
167 static char * mantissa = NULL;
168 static char * tmpfmt1 = NULL, * tmpfmt2 = NULL, * exptmp = NULL;
169 static unsigned mantlen = 0, fmtlen = 0;
170 char * fraction = NULL;
171 int zero_pad = 0;
172
173 if (fmt == NULL)
174 return(true);
175
176 if (strlen(fmt) + 1 > fmtlen) {
177 fmtlen = strlen(fmt) + 40;
178 tmpfmt1 = scxrealloc(tmpfmt1, fmtlen);
179 tmpfmt2 = scxrealloc(tmpfmt2, fmtlen);
180 exptmp = scxrealloc(exptmp, fmtlen);
181 }
182 fmt = strcpy(tmpfmt1, fmt);
183 if (buflen + 1 > mantlen) {
184 mantlen = buflen + 40;
185 mantissa = scxrealloc(mantissa, mantlen);
186 }
187
188 /*
189 * select positive or negative format if necessary
190 */
191 for (cp = fmt; *cp != ';' && *cp != EOS; cp++) {
192 if (*cp == '\\')
193 cp++;
194 }
195 if (*cp == ';') {
196 if (val < 0.0) {
197 val = -val; /* format should provide sign if desired */
198 fmt = cp + 1;
199 } else
200 *cp = EOS;
201 }
202
203 /*
204 * extract other information from format and produce a
205 * format string stored in tmpfmt2 also scxmalloc()'d above
206 */
207 tmp = tmpfmt2;
208 for (cp = fmt, tp = tmp; *cp != EOS; cp++) {
209 switch (*cp) {
210 case '\\':
211 *tp++ = *cp++;
212 *tp++ = *cp;
213 break;
214
215 case ',':
216 comma = true;
217 break;
218
219 case '.':
220 if (decimal == NULL)
221 decimal = tp;
222 *tp++ = *cp;
223 break;
224
225 case '%':
226 val *= 100.0;
227 *tp++ = *cp;
228 break;
229
230 default:
231 *tp++ = *cp;
232 break;
233 }
234 }
235 *tp = EOS;
236 fmt = tmpfmt2;
237
238 /*
239 * The following line was necessary due to problems with the gcc
240 * compiler and val being a negative zero. Thanks to Mike Novack for
241 * the suggestion. - CRM
242 */
243 val = (val + 1.0) - 1.0;
244 if (val < 0.0) {
245 negative = true;
246 val = -val;
247 }
248 /*
249 * extract the exponent from the format if present
250 */
251 for (cp = fmt; *cp != EOS; cp++) {
252 if (*cp == '\\')
253 cp++;
254 else if (*cp == 'e' || *cp == 'E') {
255 if (cp[1] == '+' || cp[1] == '-') {
256 exponent = strcpy(exptmp, cp);
257 *cp = EOS;
258 if (val != 0.0) {
259 while (val < 1.0) {
260 val *= 10.0;
261 exp_val--;
262 }
263 while (val >= 10.0) {
264 val /= 10.0;
265 exp_val++;
266 }
267 }
268 }
269 break;
270 }
271 }
272
273 /*
274 * determine maximum decimal places and use sprintf
275 * to build initial character form of formatted value.
276 */
277 width = 0;
278 if (decimal) {
279 *decimal++ = EOS;
280 for (cp = decimal; *cp != EOS; cp++) {
281 switch (*cp) {
282 case '\\':
283 cp++;
284 break;
285
286 case '#':
287 width++;
288 break;
289
290 case '0':
291 zero_pad = ++width;
292 break;
293
294 case '&':
295 width += lprecision;
296 zero_pad = width;
297 break;
298 }
299 }
300 zero_pad = strlen(decimal) - zero_pad;
301 }
302 (void) sprintf(prtfmt, "%%.%dlf", width);
303 (void) sprintf(mantissa, prtfmt, val);
304 for (cp = integer = mantissa; *cp != dpoint && *cp != EOS; cp++) {
305 if (*integer == '0')
306 integer++;
307 }
308 if (*cp == dpoint) {
309 fraction = cp + 1;
310 *cp = EOS;
311 cp = fraction + strlen(fraction) - 1;
312 for (; zero_pad > 0 && *cp != EOS; zero_pad--, cp--) { // scim
313 //for (; zero_pad > 0; zero_pad--, cp--) { // sc
314 if (*cp == '0')
315 *cp = EOS;
316 else
317 break;
318 }
319 } else
320 fraction = "";
321
322 /*
323 * format the puppy
324 */
325 {
326 static char * citmp = NULL, * cftmp = NULL;
327 static unsigned cilen = 0, cflen = 0;
328 char * ci, * cf, * ce;
329 int len_ci, len_cf, len_ce;
330 int ret = false;
331
332 ci = fmt_int(integer, fmt, comma, negative);
333 len_ci = strlen(ci);
334 if (len_ci >= cilen) {
335 cilen = len_ci + 40;
336 citmp = scxrealloc(citmp, cilen);
337 }
338 ci = strcpy(citmp, ci);
339
340 cf = decimal ? fmt_frac(fraction, decimal, lprecision) : "";
341 len_cf = strlen(cf);
342 if (len_cf >= cflen) {
343 cflen = len_cf + 40;
344 cftmp = scxrealloc(cftmp, cflen); // scim
345 //cftmp = scxrealloc(cftmp, cilen); // sc
346 }
347 cf = strcpy(cftmp, cf);
348
349 ce = (exponent) ? fmt_exp(exp_val, exponent) : "";
350 len_ce = strlen(ce);
351 /*
352 * Skip copy assuming sprintf doesn't call our format functions
353 * ce = strcpy(scxmalloc((unsigned)((len_ce = strlen(ce)) + 1)), ce);
354 */
355 if (len_ci + len_cf + len_ce < buflen) {
356 (void) sprintf(buf, "%s%s%s", ci, cf, ce);
357 ret = true;
358 }
359
360 return (ret);
361 }
362 }
363
364 /**
365 * \brief TODO Document fmt_int()
366 *
367 * \param[in] val integer part of the value to be formatted
368 * \param[in] fmt integer part of the format
369 * \param[in] comma true if we should comma-ify the value
370 * \param[in] negative true if the value is actually negative
371 * \return none
372 */
373
fmt_int(char * val,char * fmt,int comma,int negative)374 static char * fmt_int(char *val, char *fmt, int comma, int negative) {
375
376 int digit, f, v;
377 int thousands = 0;
378 char * cp;
379 static char buf[MAXBUF];
380 char * bufptr = buf;
381
382 /*
383 * locate the leftmost digit placeholder
384 */
385 for (cp = fmt; *cp != EOS; cp++) {
386 if (*cp == '\\')
387 cp++;
388 else if (*cp == '#' || *cp == '0')
389 break;
390 }
391 digit = (*cp == EOS) ? -1 : cp - fmt;
392
393 /*
394 * format the value
395 */
396 f = strlen(fmt) - 1;
397 v = (digit >= 0) ? strlen(val) - 1 : -1;
398 while (f >= 0 || v >= 0) {
399 if (f > 0 && fmt[f-1] == '\\') {
400 *bufptr++ = fmt[f--];
401 } else if (f >= 0 && (fmt[f] == '#' || fmt[f] == '0')) {
402 if (v >= 0 || fmt[f] == '0') {
403 *bufptr++ = v < 0 ? '0' : val[v];
404 if (comma && (thousands = (thousands + 1) % 3) == 0 &&
405 v > 0 && thsep != '\0')
406 *bufptr++ = thsep;
407 v--;
408 }
409 } else if (f >= 0) {
410 *bufptr++ = fmt[f];
411 }
412 if (v >= 0 && f == digit) {
413 continue;
414 }
415 f--;
416 }
417
418 if (negative && digit >= 0)
419 *bufptr++ = '-';
420 *bufptr = EOS;
421 reverse(buf);
422
423 return (buf);
424 }
425
426 /**
427 * \brief TODO Document fmt_frac()
428 *
429 * \param[in] val fractional part of the value to be formatted
430 * \param[in] fmt fractional portion of format
431 * \param[in] precision, for interpreting the "&"
432 *
433 * \return none
434 */
435
fmt_frac(char * val,char * fmt,int lprecision)436 static char * fmt_frac(char *val, char *fmt, int lprecision) {
437
438 static char buf[MAXBUF];
439 register char * bufptr = buf;
440 register char * fmtptr = fmt, *valptr = val;
441
442 *bufptr++ = dpoint;
443 while (*fmtptr != EOS) {
444 if (*fmtptr == '&') {
445 int i;
446 for (i = 0; i < lprecision; i++)
447 *bufptr++ = (*valptr != EOS) ? *valptr++ : '0';
448 } else if (*fmtptr == '\\')
449 *bufptr++ = *++fmtptr;
450 else if (*fmtptr == '#' || *fmtptr == '0') {
451 if (*valptr != EOS || *fmtptr == '0')
452 *bufptr++ = (*valptr != EOS) ? *valptr++ : '0';
453 } else
454 *bufptr++ = *fmtptr;
455 fmtptr++;
456 }
457 *bufptr = EOS;
458
459 if (buf[1] < '0' || buf[1] > '9')
460 return (buf + 1);
461 else
462 return (buf);
463 }
464
465 /**
466 * \brief TODO Document fmt_exp
467 *
468 * \param[in] val value of the exponent
469 * \param[in] fmt exponent part of the format
470 *
471 * \return none
472 */
473
fmt_exp(int val,char * fmt)474 static char * fmt_exp(int val, char *fmt) {
475 static char buf[MAXBUF];
476 register char *bufptr = buf;
477 char valbuf[64];
478 int negative = false;
479
480 *bufptr++ = *fmt++;
481 if (*fmt == '+')
482 *bufptr++ = (val < 0) ? '-' : '+';
483 else if (val < 0)
484 *bufptr++ = '-';
485 fmt++;
486 *bufptr = EOS;
487
488 if (val < 0) {
489 val = -val;
490 negative = false;
491 }
492 (void) sprintf(valbuf, "%d", val);
493
494 (void) strcat(buf, fmt_int(valbuf, fmt, false, negative));
495 return (buf);
496 }
497
498 /**
499 * \brief TODO Document reverse()
500 *
501 * \param[in] buf
502 *
503 * \return none
504 */
505
reverse(register char * buf)506 static void reverse(register char *buf) {
507 register char *cp = buf + strlen(buf) - 1;
508 register char tmp;
509
510 while (buf < cp) {
511 tmp = *cp;
512 *cp-- = *buf;
513 *buf++ = tmp;
514 }
515 }
516
517 /*
518 * Tom Anderson <toma@hpsad.hp.com>
519 * 10/14/90
520 *
521 * This routine takes a value and formats it using fixed, scientific,
522 * or engineering notation. The format command 'f' determines which
523 * format is used. The formats are: example
524 * 0: Fixed point (default) 0.00010
525 * 1: Scientific 1.00E-04
526 * 2: Engineering 100.00e-06
527 *
528 * The format command 'f' now uses three values. The first two are the
529 * width and precision, and the last one is the format value 0, 1, or 2 as
530 * described above. The format value is passed in the variable fmt.
531 *
532 * This formatted value is written into the passed buffer. if the
533 * resulting string is too long to fit into the passed buffer, the
534 * function returns false. Otherwise the function returns true.
535 *
536 * When a number is formatted as engineering and is outside of the range,
537 * the format reverts to scientific.
538 *
539 * To preserve compatability with old spreadsheet files, the third value
540 * may be missing, and the default will be fixed point (format 0).
541 *
542 * When an old style sheet is saved, the third value will be stored.
543 */
544
545 /* defined in sc.h */
546 #ifndef REFMTFIX
547 #define REFMTFIX 0
548 #define REFMTFLT 1
549 #define REFMTENG 2
550 #define REFMTDATE 3
551 #define REFMTLDATE 4
552 #endif
553
554 /**
555 * \brief TODO Document engformat()
556 *
557 * \param[in] fmt
558 * \param[in] width
559 * \param[in] lprecision
560 * \param[in] val
561 * \param[in] buf
562 * \param[in] buflen
563 *
564 * \return none
565 */
566
engformat(int fmt,int width,int lprecision,double val,char * buf,int buflen)567 int engformat(int fmt, int width, int lprecision, double val, char *buf, int buflen) {
568
569 static char * engmult[] = {
570 "-18", "-15", "-12", "-09", "-06", "-03",
571 "+00",
572 "+03", "+06", "+09", "+12", "+15", "+18"
573 };
574 int engind = 0;
575 double engmant, pow(), engabs, engexp;
576
577 if (buflen < width) return (false);
578 if (fmt >= 0 && fmt < COLFORMATS && colformat[fmt])
579 return (format(colformat[fmt], lprecision, val, buf, buflen));
580 if (fmt == REFMTFIX)
581 (void) sprintf(buf,"%*.*f", width, lprecision, val);
582 if (fmt == REFMTFLT)
583 (void) sprintf(buf,"%*.*e", width, lprecision, val);
584 if (fmt == REFMTENG) {
585 if (val == 0e0) { /* Hack to get zeroes to line up in engr fmt */
586 (void) sprintf((buf-1),"%*.*f ", width, lprecision, val);
587 } else {
588 engabs = (val);
589 if ( engabs < 0e0) engabs = -engabs;
590 if ((engabs >= 1e-18) && (engabs < 1e-15)) engind=0;
591 if ((engabs >= 1e-15) && (engabs < 1e-12)) engind=1;
592 if ((engabs >= 1e-12) && (engabs < 1e-9 )) engind=2;
593 if ((engabs >= 1e-9) && (engabs < 1e-6 )) engind=3;
594 if ((engabs >= 1e-6) && (engabs < 1e-3 )) engind=4;
595 if ((engabs >= 1e-3) && (engabs < 1 )) engind=5;
596 if ((engabs >= 1) && (engabs < 1e3 )) engind=6;
597 if ((engabs >= 1e3) && (engabs < 1e6 )) engind=7;
598 if ((engabs >= 1e6) && (engabs < 1e9 )) engind=8;
599 if ((engabs >= 1e9) && (engabs < 1e12 )) engind=9;
600 if ((engabs >= 1e12) && (engabs < 1e15 )) engind=10;
601 if ((engabs >= 1e15) && (engabs < 1e18 )) engind=11;
602 if ((engabs >= 1e18) && (engabs < 1e21 )) engind=12;
603 if ((engabs < 1e-18) || (engabs >= 1e21 )) {
604 /* Revert to floating point */
605 (void) sprintf(buf,"%*.*e", width, lprecision, val);
606 } else {
607 engexp = (double) (engind-6)*3;
608 engmant = val/pow(10.0e0,engexp);
609 (void) sprintf(buf,"%*.*fe%s", width-4, lprecision, engmant, engmult[engind]);
610 }
611 }
612 }
613 if (fmt == REFMTDATE) {
614 int i;
615 time_t secs;
616
617 if (buflen < 9) {
618 for (i = 0; i < width; i++) buf[i] = '*';
619 buf[i] = '\0';
620 } else {
621 secs = (time_t)val;
622 strftime(buf,buflen,"%e %b %y",localtime(&secs));
623 for (i = 9; i < width; i++) buf[i] = ' ';
624 buf[i] = '\0';
625 }
626 }
627 if (fmt == REFMTLDATE) {
628 int i;
629 time_t secs;
630
631 if (buflen < 11) {
632 for (i = 0; i < width; i++) buf[i] = '*';
633 buf[i] = '\0';
634 } else {
635 secs = (time_t)val;
636 strftime(buf,buflen,"%e %b %Y",localtime(&secs));
637 for (i = 11; i < width; i++) buf[i] = ' ';
638 buf[i] = '\0';
639 }
640 }
641 return (true);
642 }
643