Skip to content

Commit

Permalink
fix: preserve numerical precision on unary negation (fix jqlang#2704)
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Jan 30, 2025
1 parent dea5e61 commit 14eb4a8
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 23 deletions.
13 changes: 7 additions & 6 deletions docs/content/manual/dev/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,7 @@ sections:
such `1E1234567890`, precision will be lost if the exponent
is too large.
(3) In jq programs, a leading minus sign will trigger the
conversion of the number to an IEEE754 representation.
(4) Comparisons are carried out using the untruncated
(3) Comparisons are carried out using the untruncated
big decimal representation of numbers if available, as
illustrated in one of the following examples.
Expand All @@ -395,15 +392,19 @@ sections:
input: '0.12345678901234567890123456789'
output: ['0.12345678901234567890123456789']

- program: '[., tojson] | . == if have_decnum then [12345678909876543212345,"12345678909876543212345"] else [12345678909876543000000,"12345678909876543000000"] end'
- program: '[., tojson] == if have_decnum then [12345678909876543212345,"12345678909876543212345"] else [12345678909876543000000,"12345678909876543000000"] end'
input: '12345678909876543212345'
output: ['true']

- program: '[1234567890987654321,-1234567890987654321 | tojson] == if have_decnum then ["1234567890987654321","-1234567890987654321"] else ["1234567890987654400","-1234567890987654400"] end'
input: 'null'
output: ['true']

- program: '. < 0.12345678901234567890123456788'
input: '0.12345678901234567890123456789'
output: ['false']

- program: 'map([., . == 1]) | tojson | . == if have_decnum then "[[1,true],[1.000,true],[1.0,true],[1.00,true]]" else "[[1,true],[1,true],[1,true],[1,true]]" end'
- program: 'map([., . == 1]) | tojson == if have_decnum then "[[1,true],[1.000,true],[1.0,true],[1.00,true]]" else "[[1,true],[1,true],[1,true],[1,true]]" end'
input: '[1, 1.000, 1.0, 100e-2]'
output: ['true']

Expand Down
13 changes: 7 additions & 6 deletions jq.1.prebuilt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ static jv f_negate(jq_state *jq, jv input) {
if (jv_get_kind(input) != JV_KIND_NUMBER) {
return type_error(input, "cannot be negated");
}
jv ret = jv_number(-jv_number_value(input));
jv ret = jv_number_negate(input);
jv_free(input);
return ret;
}
Expand Down
27 changes: 19 additions & 8 deletions src/jv.c
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,6 @@ static decNumber* jvp_dec_number_ptr(jv j) {
}

static jvp_literal_number* jvp_literal_number_alloc(unsigned literal_length) {

/* The number of units needed is ceil(DECNUMDIGITS/DECDPUN) */
int units = ((literal_length+DECDPUN-1)/DECDPUN);

Expand All @@ -571,27 +570,25 @@ static jvp_literal_number* jvp_literal_number_alloc(unsigned literal_length) {
+ sizeof(decNumberUnit) * units
);

n->refcnt = JV_REFCNT_INIT;
n->num_double = NAN;
n->literal_data = NULL;
return n;
}

static jv jvp_literal_number_new(const char * literal) {
jvp_literal_number* n = jvp_literal_number_alloc(strlen(literal));

jvp_literal_number * n = jvp_literal_number_alloc(strlen(literal));

n->refcnt = JV_REFCNT_INIT;
n->literal_data = NULL;
decContext *ctx = DEC_CONTEXT();
decContextClearStatus(ctx, DEC_Conversion_syntax);
decNumberFromString(&n->num_decimal, literal, ctx);
n->num_double = NAN;

if (ctx->status & DEC_Conversion_syntax) {
jv_mem_free(n);
return JV_INVALID;
}

jv r = {JVP_FLAGS_NUMBER_LITERAL, 0, 0, JV_NUMBER_SIZE_INIT, {&n->refcnt}};
return r;
return (jv){JVP_FLAGS_NUMBER_LITERAL, 0, 0, JV_NUMBER_SIZE_INIT, {&n->refcnt}};
}

static double jvp_literal_number_to_double(jv j) {
Expand Down Expand Up @@ -734,6 +731,20 @@ int jvp_number_is_nan(jv n) {
return n.u.number != n.u.number;
}

jv jv_number_negate(jv n) {
assert(JVP_HAS_KIND(n, JV_KIND_NUMBER));

#ifdef USE_DECNUM
if (JVP_HAS_FLAGS(n, JVP_FLAGS_NUMBER_LITERAL)) {
jvp_literal_number* m = jvp_literal_number_alloc(jvp_dec_number_ptr(n)->digits);

decNumberMinus(&m->num_decimal, jvp_dec_number_ptr(n), DEC_CONTEXT());
return (jv){JVP_FLAGS_NUMBER_LITERAL, 0, 0, 0, {&m->refcnt}};
}
#endif
return jv_number(-jv_number_value(n));
}

int jvp_number_cmp(jv a, jv b) {
assert(JVP_HAS_KIND(a, JV_KIND_NUMBER));
assert(JVP_HAS_KIND(b, JV_KIND_NUMBER));
Expand Down
1 change: 1 addition & 0 deletions src/jv.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jv jv_number(double);
jv jv_number_with_literal(const char*);
double jv_number_value(jv);
int jv_is_integer(jv);
jv jv_number_negate(jv n);

int jv_number_has_literal(jv n);
const char* jv_number_get_literal(jv);
Expand Down
13 changes: 13 additions & 0 deletions tests/jq.test
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,19 @@ true
{"x":13911860366432393}
13911860366432382

# Unary negation preserves numerical precision
-. | tojson == if have_decnum then "-13911860366432393" else "-13911860366432392" end
13911860366432393
true

-. | tojson == if have_decnum then "0.12345678901234567890123456789" else "0.12345678901234568" end
-0.12345678901234567890123456789
true

[1E+1000,-1E+1000] | map(tojson) == if have_decnum then ["1E+1000","-1E+1000"] else ["1.7976931348623157e+308","-1.7976931348623157e+308"] end
null
true

. |= try . catch .
1
1
Expand Down
8 changes: 6 additions & 2 deletions tests/man.test

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 14eb4a8

Please sign in to comment.