From 35d67051bfd72187a03190d0ebff6d2add4a0c8f Mon Sep 17 00:00:00 2001 From: Walter Tross Date: Sat, 11 Jan 2020 15:59:48 +0100 Subject: [PATCH 1/4] Decimal separator and thousands separator --- README.md | 34 +++++++++++++++++++++ clac.c | 84 ++++++++++++++++++++++++++++++++++++++++++--------- test/tests.sh | 21 +++++++++++-- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 96eff7e..d0bd6f4 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,40 @@ $ clac "1 2 3 4 count . sum , /" In fact, if you find yourself calculating averages very often, you can define the word `avg` as `"count . sum , /"`. +### Changing the decimal separator and the thousands separator + +In many countries, the comma is the decimal separator. +To make clac handle the comma in numbers as if it were a dot, +start it with the `-c` option. To make it display commas instead of +dots start it with the `-d` option: + +```shell +$ clac -c "1.2 3,4 *" +4.08 + +$ clac -cd "1.2 3,4 *" +4,08 +``` + +To make clac ignore thousands separators in numbers, start it with +the `-t` option. What the thousands separator is, depends on whether +`-c` is present or not. The default thousands separator is the comma, +with `-c` it becomes the dot. This means that when clac is started +with both the `-c` and `-t` options (e.g., with `-ct`), the dot is +not recognized as a decimal separator. + +```shell +$ clac -t "123,456.99" +123456.99 + +$ clac -cd "123.000 4,567 +" +127,567 + +$ clac -cdt "123.000 4,567 +" +123004,567 +``` + + Contributing ------------ diff --git a/clac.c b/clac.c index 62e389e..120952f 100644 --- a/clac.c +++ b/clac.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include "linenoise.h" #include "sds.h" @@ -40,7 +41,8 @@ /* UI */ #define HINT_COLOR 33 #define NUMBER_FMT "%.15g" -#define OUTPUT_FMT "\x1b[33m= " NUMBER_FMT "\x1b[0m\n" +#define NUMBER_FMT_MAX_STRLEN 22 +#define OUTPUT_FMT "\x1b[33m= %s\x1b[0m\n" #define WORDEF_FMT "%s \x1b[33m\"%s\"\x1b[0m\n" /* Config */ @@ -75,6 +77,9 @@ static node *head = NULL; static node *tail = NULL; static sds result; static double hole = 0; +static char decsep = '.'; +static char thousep = '\0'; +static int displaycomma = 0; static int isoverflow(stack *s) { if (isfull(s)) { @@ -272,11 +277,25 @@ static void load(sds filename) { sdsfree(content); } +static char *number(double dbl) { + static char buffer[NUMBER_FMT_MAX_STRLEN + 1]; + char *c; + + sprintf(buffer, NUMBER_FMT, dbl); + + if (displaycomma) { + for (c = buffer; *c; c++) { + if (*c == '.') *c = ','; + } + } + return buffer; +} + static void eval(const char *input); static void process(sds word) { double a, b; - char *z; + char *c, *d, *z; node *n; if (!strcmp(word, "_")) { @@ -423,6 +442,16 @@ static void process(sds word) { } else if ((n = get(word)) != NULL) { eval(n->meaning); } else { + if (decsep != '.' || thousep != '\0') { + for (d = c = word; *c; c++) { + if (*c == decsep) { + *d++ = '.'; + } else if (*c != thousep) { + *d++ = *c; + } + } + *d = '\0'; + } a = strtod(word, &z); if (*z == '\0') { @@ -459,14 +488,14 @@ static char *hints(const char *input, int *color, int *bold) { result = sdscat(result, " "); for (i = 0; i < count(s0); i++) { - result = sdscatprintf(result, " " NUMBER_FMT, s0->items[i]); + result = sdscatprintf(result, " %s", number(s0->items[i])); } if (!isempty(s1)) { result = sdscat(result, " ⋮"); for (i = s1->top-1; i > -1; i--) { - result = sdscatprintf(result, " " NUMBER_FMT, s1->items[i]); + result = sdscatprintf(result, " %s", number(s1->items[i])); } } @@ -497,27 +526,50 @@ static void config() { } int main(int argc, char **argv) { - char *line; + char *line, *expr = NULL; + int i, j; + + setlocale(LC_NUMERIC, "C"); result = sdsempty(); config(); - if (argc == 2) { - eval(argv[1]); + for (i = 1; i < argc; i++) { + if (strlen(argv[i]) > 1 && argv[i][0] == '-' && isalpha(argv[i][1])) { + for (j = 1; j < strlen(argv[i]); j++) { + switch (argv[i][j]) { + case 'c': + decsep = ','; + if (thousep) thousep = '.'; + break; + case 'd': + displaycomma = 1; + break; + case 't': + thousep = (decsep == '.') ? ',' : '.'; + break; + default: + goto usage_error; + } + } + } else if (expr == NULL) { + expr = argv[i]; + } else { + goto usage_error; + } + } + + if (expr != NULL) { + eval(expr); while (count(s0) > 0) { - printf(NUMBER_FMT "\n", pop(s0)); + printf("%s\n", number(pop(s0))); } exit(0); } - if (argc > 2) { - fprintf(stderr, "usage: clac [expression]\n"); - exit(1); - } - linenoiseSetHintsCallback(hints); linenoiseSetCompletionCallback(completion); @@ -535,7 +587,7 @@ int main(int argc, char **argv) { } else if (!isempty(s0)) { hole = peek(s0); clear(s0); - printf(OUTPUT_FMT, hole); + printf(OUTPUT_FMT, number(hole)); } sdsclear(result); @@ -547,4 +599,8 @@ int main(int argc, char **argv) { cleanup(); return 0; + +usage_error: + fprintf(stderr, "usage: clac [-cdt] [expression]\n"); + exit(1); } diff --git a/test/tests.sh b/test/tests.sh index 81564ce..522a274 100755 --- a/test/tests.sh +++ b/test/tests.sh @@ -33,8 +33,25 @@ assert_equal "nan" `./clac 3+` # Not found words starting with alpha are ignored assert_equal "" `./clac foo` -# Argument error (too many arguments) -assert_equal "" `./clac 1 2 2> /dev/null` +# Usage errors +assert_equal "usage:" `./clac 1 2 2>&1` # too many arguments +assert_equal "usage:" `./clac -o 2>&1` # unknown argument +assert_equal "usage:" `./clac -cto 2>&1` # known and unknown arguments + +# Decimal and thousands separator +assert_equal "3001000.08" `./clac -c "1000,2 3000.4 *"` +assert_equal "nan" `./clac -c "1.000,2"` +assert_equal "1000,2" `./clac -d "1000.2"` +assert_equal "1000.2" `./clac -t "1,000.2"` +assert_equal "1000.2" `./clac -ct "1.000,2"` +assert_equal "1000,2" `./clac -cdt "1.000,2"` +assert_equal "1000.2" `./clac -c "3001000,08 3000.4 /"` +assert_equal "1000,2" `./clac -cd "3001000,08 3000.4 /"` +assert_equal "100.02" `./clac -ct "3.001.000,08 3000.4 /"` +assert_equal "1000,2" `./clac -ctd "3.001.000,08 3.000,40 /"` +assert_equal "3001000.08" `./clac -t "1,000.20 3,000.40 *"` +assert_equal "3001000,08" `./clac -td "1,000.20 3,000.40 *"` +assert_equal "3001000,08" `./clac -tcd "1.000,20 3.000,40 *"` # Stashing numbers assert_equal "21" `./clac "4 3 9 . * , +"` From 94eac4f9136c2bf1c73064134810550c8b25dd60 Mon Sep 17 00:00:00 2001 From: Walter Tross Date: Sun, 12 Jan 2020 00:28:46 +0100 Subject: [PATCH 2/4] words are parsed without -c and -t --- README.md | 6 ++++++ clac.c | 18 +++++++++--------- test/tests.sh | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d0bd6f4..d245a34 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,12 @@ $ clac -cdt "123.000 4,567 +" 123004,567 ``` +The definition of words in the configuration file is always parsed +_without_ the `-c` and `-t` options, i.e., numbers therein may only +use the dot as the decimal separator and may not use any thousands +separator. This way the configuration files doesn't change its +meaning depending on the options clac is started with. + Contributing ------------ diff --git a/clac.c b/clac.c index 120952f..e1b0ab2 100644 --- a/clac.c +++ b/clac.c @@ -291,9 +291,9 @@ static char *number(double dbl) { return buffer; } -static void eval(const char *input); +static void eval(const char *input, int toplevel); -static void process(sds word) { +static void process(sds word, int toplevel) { double a, b; char *c, *d, *z; node *n; @@ -439,10 +439,10 @@ static void process(sds word) { move(s0, s1, count(s0)); } else if (!strcasecmp(word, ";")) { move(s1, s0, count(s1)); - } else if ((n = get(word)) != NULL) { - eval(n->meaning); + } else if ((n = get(word)) != NULL) { + eval(n->meaning, 0); } else { - if (decsep != '.' || thousep != '\0') { + if (toplevel && (decsep != '.' || thousep != '\0')) { for (d = c = word; *c; c++) { if (*c == decsep) { *d++ = '.'; @@ -462,13 +462,13 @@ static void process(sds word) { } } -static void eval(const char *input) { +static void eval(const char *input, int toplevel) { int i, argc; sds *argv = sdssplitargs(input, &argc); for (i = 0; i < argc; i++) { - process(argv[i]); + process(argv[i], toplevel); } sdsfreesplitres(argv, argc); @@ -482,7 +482,7 @@ static char *hints(const char *input, int *color, int *bold) { clear(s0); clear(s1); - eval(input); + eval(input, 1); sdsclear(result); result = sdscat(result, " "); @@ -561,7 +561,7 @@ int main(int argc, char **argv) { } if (expr != NULL) { - eval(expr); + eval(expr, 1); while (count(s0) > 0) { printf("%s\n", number(pop(s0))); diff --git a/test/tests.sh b/test/tests.sh index 522a274..2632a01 100755 --- a/test/tests.sh +++ b/test/tests.sh @@ -52,6 +52,7 @@ assert_equal "1000,2" `./clac -ctd "3.001.000,08 3.000,40 /"` assert_equal "3001000.08" `./clac -t "1,000.20 3,000.40 *"` assert_equal "3001000,08" `./clac -td "1,000.20 3,000.40 *"` assert_equal "3001000,08" `./clac -tcd "1.000,20 3.000,40 *"` +assert_equal "3141,592" `./clac -cdt "pi 1.000,000 *"` # Stashing numbers assert_equal "21" `./clac "4 3 9 . * , +"` From 21fb19a37fc237904ddc4f55e9f760cea57c6edb Mon Sep 17 00:00:00 2001 From: Walter Tross Date: Sun, 12 Jan 2020 13:48:53 +0100 Subject: [PATCH 3/4] 5 separator modes --- README.md | 68 +++++++++++++++++++++++++++++++-------------------- clac.c | 46 +++++++++++++++++----------------- test/tests.sh | 43 +++++++++++++++++++------------- 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d245a34..8e38dda 100644 --- a/README.md +++ b/README.md @@ -305,43 +305,59 @@ $ clac "1 2 3 4 count . sum , /" In fact, if you find yourself calculating averages very often, you can define the word `avg` as `"count . sum , /"`. -### Changing the decimal separator and the thousands separator +### Separator modes -In many countries, the comma is the decimal separator. -To make clac handle the comma in numbers as if it were a dot, -start it with the `-c` option. To make it display commas instead of -dots start it with the `-d` option: +In many countries the comma is the decimal separator. +Furthermore a thousands separator is often used (in India it's a bit +more complicated, but let's still call it like that). +The following could be prices in three different countries: -```shell -$ clac -c "1.2 3,4 *" -4.08 +* `1,234,567.89 $` +* `1.234.567,89 €` +* `12.34.56.789 ₹` -$ clac -cd "1.2 3,4 *" -4,08 -``` +Clac has a total of 5 separator modes, that can be selected by +starting it with one of the following options (if more are provided, +the last one wins; if none is provided, `-d` is implied): + +* `-b`: "both" mode: both the `.` and the `,` are accepted as the + decimal separator. No thousands separator can be used. + In the output, the `.` is used as the decimal separator. + +* `-c`: "comma" mode: only the `,` is accepted as the decimal + separator. No thousands separator can be used. + In the output, the `,` is used as the decimal separator. -To make clac ignore thousands separators in numbers, start it with -the `-t` option. What the thousands separator is, depends on whether -`-c` is present or not. The default thousands separator is the comma, -with `-c` it becomes the dot. This means that when clac is started -with both the `-c` and `-t` options (e.g., with `-ct`), the dot is -not recognized as a decimal separator. +* `-C`: "super comma" mode: the `,` is the decimal + separator, while the `.` is totally ignored in numbers. + In the output, the `,` is used as the decimal separator. + +* `-d`: "dot" mode (default): only the `.` is accepted as the + decimal separator. No thousands separator can be used. + In the output, the `.` is used as the decimal separator. + +* `-D`: "super dot" mode: the `.` is the decimal + separator, while the `,` is totally ignored in numbers. + In the output, the `.` is used as the decimal separator. ```shell -$ clac -t "123,456.99" -123456.99 +$ clac -b "1.2 3,4 *" +4.08 + +$ clac -c "1,2 3,4 *" +4,08 -$ clac -cd "123.000 4,567 +" -127,567 +$ clac -C "1.234.567,89" +1234567,89 -$ clac -cdt "123.000 4,567 +" -123004,567 +$ clac -D "1,234,567.89" +1234567.89 ``` The definition of words in the configuration file is always parsed -_without_ the `-c` and `-t` options, i.e., numbers therein may only -use the dot as the decimal separator and may not use any thousands -separator. This way the configuration files doesn't change its +in "dot" mode, i.e., numbers therein may only use the dot as the +decimal separator and may not use any thousands separator. +This way the configuration file doesn't change its meaning depending on the options clac is started with. diff --git a/clac.c b/clac.c index e1b0ab2..9f4c10d 100644 --- a/clac.c +++ b/clac.c @@ -77,9 +77,7 @@ static node *head = NULL; static node *tail = NULL; static sds result; static double hole = 0; -static char decsep = '.'; -static char thousep = '\0'; -static int displaycomma = 0; +static char mode = 'd'; static int isoverflow(stack *s) { if (isfull(s)) { @@ -283,9 +281,11 @@ static char *number(double dbl) { sprintf(buffer, NUMBER_FMT, dbl); - if (displaycomma) { + if (mode == 'c' || mode == 'C') { for (c = buffer; *c; c++) { - if (*c == '.') *c = ','; + if (*c == '.') { + *c = ','; + } } } return buffer; @@ -442,11 +442,21 @@ static void process(sds word, int toplevel) { } else if ((n = get(word)) != NULL) { eval(n->meaning, 0); } else { - if (toplevel && (decsep != '.' || thousep != '\0')) { + if (toplevel && mode != 'd') { for (d = c = word; *c; c++) { - if (*c == decsep) { - *d++ = '.'; - } else if (*c != thousep) { + if (*c == ',') { + if (mode == 'b' || mode == 'c' || mode == 'C') { + *d++ = '.'; + } else if (mode != 'D') { + *d++ = ','; + } + } else if (*c == '.') { + if (mode == 'b' || mode == 'd' || mode == 'D') { + *d++ = '.'; + } else if (mode != 'C') { + *d++ = ','; + } + } else { *d++ = *c; } } @@ -538,20 +548,10 @@ int main(int argc, char **argv) { for (i = 1; i < argc; i++) { if (strlen(argv[i]) > 1 && argv[i][0] == '-' && isalpha(argv[i][1])) { for (j = 1; j < strlen(argv[i]); j++) { - switch (argv[i][j]) { - case 'c': - decsep = ','; - if (thousep) thousep = '.'; - break; - case 'd': - displaycomma = 1; - break; - case 't': - thousep = (decsep == '.') ? ',' : '.'; - break; - default: - goto usage_error; + if (strchr("bcCdD", argv[i][j]) == NULL) { + goto usage_error; } + mode = argv[i][j]; } } else if (expr == NULL) { expr = argv[i]; @@ -601,6 +601,6 @@ int main(int argc, char **argv) { return 0; usage_error: - fprintf(stderr, "usage: clac [-cdt] [expression]\n"); + fprintf(stderr, "usage: clac [-bcCdD] [expression]\n"); exit(1); } diff --git a/test/tests.sh b/test/tests.sh index 2632a01..e5f0c79 100755 --- a/test/tests.sh +++ b/test/tests.sh @@ -36,23 +36,32 @@ assert_equal "" `./clac foo` # Usage errors assert_equal "usage:" `./clac 1 2 2>&1` # too many arguments assert_equal "usage:" `./clac -o 2>&1` # unknown argument -assert_equal "usage:" `./clac -cto 2>&1` # known and unknown arguments - -# Decimal and thousands separator -assert_equal "3001000.08" `./clac -c "1000,2 3000.4 *"` -assert_equal "nan" `./clac -c "1.000,2"` -assert_equal "1000,2" `./clac -d "1000.2"` -assert_equal "1000.2" `./clac -t "1,000.2"` -assert_equal "1000.2" `./clac -ct "1.000,2"` -assert_equal "1000,2" `./clac -cdt "1.000,2"` -assert_equal "1000.2" `./clac -c "3001000,08 3000.4 /"` -assert_equal "1000,2" `./clac -cd "3001000,08 3000.4 /"` -assert_equal "100.02" `./clac -ct "3.001.000,08 3000.4 /"` -assert_equal "1000,2" `./clac -ctd "3.001.000,08 3.000,40 /"` -assert_equal "3001000.08" `./clac -t "1,000.20 3,000.40 *"` -assert_equal "3001000,08" `./clac -td "1,000.20 3,000.40 *"` -assert_equal "3001000,08" `./clac -tcd "1.000,20 3.000,40 *"` -assert_equal "3141,592" `./clac -cdt "pi 1.000,000 *"` +assert_equal "usage:" `./clac -co 2>&1` # known and unknown argument + +# Separator modes +assert_equal "1234.99" `./clac -b "1234.99"` +assert_equal "1234.99" `./clac -b "1234,99"` +assert_equal "nan" `./clac -c "1234.99"` +assert_equal "1234,99" `./clac -c "1234,99"` +assert_equal "1234.99" `./clac -d "1234.99"` +assert_equal "nan" `./clac -d "1234,99"` +assert_equal "123499" `./clac -C "1234.99"` +assert_equal "1234,99" `./clac -C "1234,99"` +assert_equal "1234.99" `./clac -D "1234.99"` +assert_equal "123499" `./clac -D "1234,99"` +assert_equal "nan" `./clac -b "1,234.99"` +assert_equal "nan" `./clac -b "1.234,99"` +assert_equal "nan" `./clac -c "1,234.99"` +assert_equal "nan" `./clac -c "1.234,99"` +assert_equal "nan" `./clac -d "1,234.99"` +assert_equal "nan" `./clac -d "1.234,99"` +assert_equal "1,23499" `./clac -C "1,234.99"` +assert_equal "1234,99" `./clac -C "1.234,99"` +assert_equal "1234.99" `./clac -D "1,234.99"` +assert_equal "1.23499" `./clac -D "1.234,99"` +assert_equal "3001000.08" `./clac -b "1000,2 3000.4 *"` +assert_equal "0,003141592" `./clac -c "pi 0,001 *"` +assert_equal "3141,592" `./clac -C "pi 1.000,000 *"` # Stashing numbers assert_equal "21" `./clac "4 3 9 . * , +"` From f45ae18ee1285d23a73b26918be20bcee5a8159e Mon Sep 17 00:00:00 2001 From: Walter Tross Date: Mon, 13 Jan 2020 15:57:31 +0100 Subject: [PATCH 4/4] Separator modes also in man --- README.md | 8 ++++---- clac.1 | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8e38dda..597d56e 100644 --- a/README.md +++ b/README.md @@ -320,8 +320,8 @@ Clac has a total of 5 separator modes, that can be selected by starting it with one of the following options (if more are provided, the last one wins; if none is provided, `-d` is implied): -* `-b`: "both" mode: both the `.` and the `,` are accepted as the - decimal separator. No thousands separator can be used. +* `-b`: "both" mode: both the `.` and the `,` are accepted as + decimal separators. No thousands separator can be used. In the output, the `.` is used as the decimal separator. * `-c`: "comma" mode: only the `,` is accepted as the decimal @@ -329,7 +329,7 @@ the last one wins; if none is provided, `-d` is implied): In the output, the `,` is used as the decimal separator. * `-C`: "super comma" mode: the `,` is the decimal - separator, while the `.` is totally ignored in numbers. + separator, while the `.` is completely ignored in numbers. In the output, the `,` is used as the decimal separator. * `-d`: "dot" mode (default): only the `.` is accepted as the @@ -337,7 +337,7 @@ the last one wins; if none is provided, `-d` is implied): In the output, the `.` is used as the decimal separator. * `-D`: "super dot" mode: the `.` is the decimal - separator, while the `,` is totally ignored in numbers. + separator, while the `,` is completely ignored in numbers. In the output, the `.` is used as the decimal separator. ```shell diff --git a/clac.1 b/clac.1 index ad6a412..fe42b2c 100644 --- a/clac.1 +++ b/clac.1 @@ -9,6 +9,7 @@ .Sh SYNOPSIS . .Nm +.Op Fl bcCdD .Op Ar expression . .Sh DESCRIPTION @@ -300,5 +301,38 @@ define the word as .Qq Sy "count . sum , /" . . +.Sh OPTIONS +.Bl -tag -width 6n +.It Fl b +"both" mode: both the `.` and the `,` are accepted as decimal separators. +No thousands separator can be used. +In the output, the `.` is used as the decimal separator. +. +.It Fl c +"comma" mode: only the `,` is accepted as the decimal separator. +No thousands separator can be used. +In the output, the `,` is used as the decimal separator. +. +.It Fl C +"super comma" mode: the `,` is the decimal separator, +while the `.` is completely ignored in numbers. +In the output, the `,` is used as the decimal separator. +. +.It Fl d +"dot" mode (default): only the `.` is accepted as the decimal separator. +No thousands separator can be used. +In the output, the `.` is used as the decimal separator. +. +.It Fl D +"super dot" mode: the `.` is the decimal separator, +while the `,` is completely ignored in numbers. +In the output, the `.` is used as the decimal separator. +. +.El +.Pp +The default separator mode is +.Fl d . +If more than one mode is provided, the last one wins. +. .Sh AUTHOR .An Michel Martens Aq mail@soveran.com