Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decimal separator and thousands separator #22

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,62 @@ $ 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 , /"`.

### Separator modes

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:

* `1,234,567.89 $`
* `1.234.567,89 €`
* `12.34.56.789 ₹`

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
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
separator. No thousands separator can be used.
In the output, the `,` is used as the decimal separator.

* `-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.

* `-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 completely ignored in numbers.
In the output, the `.` is used as the decimal separator.

```shell
$ clac -b "1.2 3,4 *"
4.08

$ clac -c "1,2 3,4 *"
4,08

$ clac -C "1.234.567,89"
1234567,89

$ clac -D "1,234,567.89"
1234567.89
```

The definition of words in the configuration file is always parsed
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.


Contributing
------------

Expand Down
34 changes: 34 additions & 0 deletions clac.1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.Sh SYNOPSIS
.
.Nm
.Op Fl bcCdD
.Op Ar expression
.
.Sh DESCRIPTION
Expand Down Expand Up @@ -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
98 changes: 77 additions & 21 deletions clac.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <math.h>
#include "linenoise.h"
#include "sds.h"

/* 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 */
Expand Down Expand Up @@ -75,6 +77,7 @@ static node *head = NULL;
static node *tail = NULL;
static sds result;
static double hole = 0;
static char mode = 'd';

static int isoverflow(stack *s) {
if (isfull(s)) {
Expand Down Expand Up @@ -272,11 +275,27 @@ static void load(sds filename) {
sdsfree(content);
}

static void eval(const char *input);
static char *number(double dbl) {
static char buffer[NUMBER_FMT_MAX_STRLEN + 1];
char *c;

static void process(sds word) {
sprintf(buffer, NUMBER_FMT, dbl);

if (mode == 'c' || mode == 'C') {
for (c = buffer; *c; c++) {
if (*c == '.') {
*c = ',';
}
}
}
return buffer;
}

static void eval(const char *input, int toplevel);

static void process(sds word, int toplevel) {
double a, b;
char *z;
char *c, *d, *z;
node *n;

if (!strcmp(word, "_")) {
Expand Down Expand Up @@ -420,9 +439,29 @@ 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 (toplevel && mode != 'd') {
for (d = c = word; *c; c++) {
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;
}
}
*d = '\0';
}
a = strtod(word, &z);

if (*z == '\0') {
Expand All @@ -433,13 +472,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);
Expand All @@ -453,20 +492,20 @@ static char *hints(const char *input, int *color, int *bold) {
clear(s0);
clear(s1);

eval(input);
eval(input, 1);
sdsclear(result);

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]));
}
}

Expand Down Expand Up @@ -497,27 +536,40 @@ 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++) {
if (strchr("bcCdD", argv[i][j]) == NULL) {
goto usage_error;
}
mode = argv[i][j];
}
} else if (expr == NULL) {
expr = argv[i];
} else {
goto usage_error;
}
}

if (expr != NULL) {
eval(expr, 1);

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);

Expand All @@ -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);
Expand All @@ -547,4 +599,8 @@ int main(int argc, char **argv) {
cleanup();

return 0;

usage_error:
fprintf(stderr, "usage: clac [-bcCdD] [expression]\n");
exit(1);
}
31 changes: 29 additions & 2 deletions test/tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,35 @@ 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 -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 . * , +"`
Expand Down