Skip to content

Commit 8561941

Browse files
authored
Merge pull request #1930 from cosmos/support-Decimal-negative
Add negative value support for Decimal
2 parents 07b5e71 + 7f23062 commit 8561941

File tree

3 files changed

+198
-23
lines changed

3 files changed

+198
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,12 @@ and this project adheres to
111111
such that you can pass in BigInts directly. This is more performant than going
112112
through strings in cases where you have a BitInt already. Strings remain
113113
supported for convenient usage with coins.
114+
- @cosmjs/math: `Decimal` now supports negative values. ([#1930])
114115

115116
[#1883]: https://github.com/cosmos/cosmjs/issues/1883
116117
[#1866]: https://github.com/cosmos/cosmjs/issues/1866
117118
[#1903]: https://github.com/cosmos/cosmjs/pull/1903
119+
[#1930]: https://github.com/cosmos/cosmjs/pull/1930
118120

119121
## [0.37.0] - 2025-10-29
120122

packages/math/src/decimal.spec.ts

Lines changed: 153 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ describe("Decimal", () => {
2323
expect(Decimal.fromAtomics("044", 5).atomics).toEqual("44");
2424
expect(Decimal.fromAtomics("0044", 5).atomics).toEqual("44");
2525
expect(Decimal.fromAtomics("00044", 5).atomics).toEqual("44");
26+
27+
expect(Decimal.fromAtomics("-1", 5).atomics).toEqual("-1");
28+
expect(Decimal.fromAtomics("-20", 5).atomics).toEqual("-20");
29+
expect(Decimal.fromAtomics("-335465464384483", 5).atomics).toEqual("-335465464384483");
2630
});
2731

2832
it("leads to correct atomics value (bigint)", () => {
@@ -40,6 +44,10 @@ describe("Decimal", () => {
4044
expect(Decimal.fromAtomics(100000000000000000000000n, 5).atomics).toEqual("100000000000000000000000");
4145
expect(Decimal.fromAtomics(200000000000000000000000n, 5).atomics).toEqual("200000000000000000000000");
4246
expect(Decimal.fromAtomics(300000000000000000000000n, 5).atomics).toEqual("300000000000000000000000");
47+
48+
expect(Decimal.fromAtomics(-1n, 5).atomics).toEqual("-1");
49+
expect(Decimal.fromAtomics(-20n, 5).atomics).toEqual("-20");
50+
expect(Decimal.fromAtomics(-335465464384483n, 5).atomics).toEqual("-335465464384483");
4351
});
4452

4553
it("reads fractional digits correctly", () => {
@@ -55,7 +63,7 @@ describe("Decimal", () => {
5563
expect(Decimal.fromAtomics(44n, 4).toString()).toEqual("0.0044");
5664
});
5765

58-
it("throws for atomics that are not non-negative integers", () => {
66+
it("throws for atomics that are not integers", () => {
5967
expect(() => Decimal.fromAtomics("0xAA", 0)).toThrowError(
6068
"Invalid string format. Only integers in decimal representation supported.",
6169
);
@@ -68,8 +76,6 @@ describe("Decimal", () => {
6876
expect(() => Decimal.fromAtomics("0.7", 0)).toThrowError(
6977
"Invalid string format. Only integers in decimal representation supported.",
7078
);
71-
72-
expect(() => Decimal.fromAtomics("-1", 0)).toThrowError("Only non-negative values supported.");
7379
});
7480
});
7581

@@ -80,6 +86,8 @@ describe("Decimal", () => {
8086
expect(() => Decimal.fromUserInput("13-", 5)).toThrowError(/invalid character at position 3/i);
8187
expect(() => Decimal.fromUserInput("13/", 5)).toThrowError(/invalid character at position 3/i);
8288
expect(() => Decimal.fromUserInput("13\\", 5)).toThrowError(/invalid character at position 3/i);
89+
expect(() => Decimal.fromUserInput("--13", 5)).toThrowError(/invalid character at position 2/i);
90+
expect(() => Decimal.fromUserInput("-1-3", 5)).toThrowError(/invalid character at position 3/i);
8391
});
8492

8593
it("throws for more than one separator", () => {
@@ -96,6 +104,9 @@ describe("Decimal", () => {
96104
expect(() => Decimal.fromUserInput("44.123456", 5)).toThrowError(
97105
/got more fractional digits than supported/i,
98106
);
107+
expect(() => Decimal.fromUserInput("44.000001", 5)).toThrowError(
108+
/got more fractional digits than supported/i,
109+
);
99110
expect(() => Decimal.fromUserInput("44.1", 0)).toThrowError(
100111
/got more fractional digits than supported/i,
101112
);
@@ -155,6 +166,8 @@ describe("Decimal", () => {
155166
expect(Decimal.fromUserInput("4.120", 5).atomics).toEqual("412000");
156167
expect(Decimal.fromUserInput("4.1200", 5).atomics).toEqual("412000");
157168
expect(Decimal.fromUserInput("4.12000", 5).atomics).toEqual("412000");
169+
// The following cases may be controversial and you could argue they should throw. But
170+
// this has been the behaviour for a long time.
158171
expect(Decimal.fromUserInput("4.120000", 5).atomics).toEqual("412000");
159172
expect(Decimal.fromUserInput("4.1200000", 5).atomics).toEqual("412000");
160173
});
@@ -166,10 +179,26 @@ describe("Decimal", () => {
166179
expect(Decimal.fromUserInput("", 3).atomics).toEqual("0");
167180
});
168181

182+
it("works for negative", () => {
183+
expect(Decimal.fromUserInput("-5", 0).atomics).toEqual("-5");
184+
expect(Decimal.fromUserInput("-5.1", 1).atomics).toEqual("-51");
185+
expect(Decimal.fromUserInput("-5.35", 2).atomics).toEqual("-535");
186+
expect(Decimal.fromUserInput("-5.765", 3).atomics).toEqual("-5765");
187+
expect(Decimal.fromUserInput("-545", 0).atomics).toEqual("-545");
188+
expect(Decimal.fromUserInput("-545.1", 1).atomics).toEqual("-5451");
189+
expect(Decimal.fromUserInput("-545.35", 2).atomics).toEqual("-54535");
190+
expect(Decimal.fromUserInput("-545.765", 3).atomics).toEqual("-545765");
191+
});
192+
169193
it("accepts american notation with skipped leading zero", () => {
170194
expect(Decimal.fromUserInput(".1", 3).atomics).toEqual("100");
171195
expect(Decimal.fromUserInput(".12", 3).atomics).toEqual("120");
172196
expect(Decimal.fromUserInput(".123", 3).atomics).toEqual("123");
197+
198+
// ⬇️ strange style but JavaScript supports it too 🤷‍♂️
199+
expect(Decimal.fromUserInput("-.1", 3).atomics).toEqual("-100");
200+
expect(Decimal.fromUserInput("-.12", 3).atomics).toEqual("-120");
201+
expect(Decimal.fromUserInput("-.123", 3).atomics).toEqual("-123");
173202
});
174203
});
175204

@@ -223,15 +252,21 @@ describe("Decimal", () => {
223252
expect(Decimal.fromUserInput("0", 0).floor().toString()).toEqual("0");
224253
expect(Decimal.fromUserInput("1", 0).floor().toString()).toEqual("1");
225254
expect(Decimal.fromUserInput("44", 0).floor().toString()).toEqual("44");
255+
expect(Decimal.fromUserInput("-2", 0).floor().toString()).toEqual("-2");
226256
expect(Decimal.fromUserInput("0", 3).floor().toString()).toEqual("0");
227257
expect(Decimal.fromUserInput("1", 3).floor().toString()).toEqual("1");
228258
expect(Decimal.fromUserInput("44", 3).floor().toString()).toEqual("44");
259+
expect(Decimal.fromUserInput("-2", 3).floor().toString()).toEqual("-2");
229260

230261
// with fractional part
231262
expect(Decimal.fromUserInput("0.001", 3).floor().toString()).toEqual("0");
232263
expect(Decimal.fromUserInput("1.999", 3).floor().toString()).toEqual("1");
233264
expect(Decimal.fromUserInput("0.000000000000000001", 18).floor().toString()).toEqual("0");
234265
expect(Decimal.fromUserInput("1.999999999999999999", 18).floor().toString()).toEqual("1");
266+
expect(Decimal.fromUserInput("-0.001", 3).floor().toString()).toEqual("-1");
267+
expect(Decimal.fromUserInput("-1.999", 3).floor().toString()).toEqual("-2");
268+
expect(Decimal.fromUserInput("-0.000000000000000001", 18).floor().toString()).toEqual("-1");
269+
expect(Decimal.fromUserInput("-1.999999999999999999", 18).floor().toString()).toEqual("-2");
235270
});
236271
});
237272

@@ -241,15 +276,19 @@ describe("Decimal", () => {
241276
expect(Decimal.fromUserInput("0", 0).ceil().toString()).toEqual("0");
242277
expect(Decimal.fromUserInput("1", 0).ceil().toString()).toEqual("1");
243278
expect(Decimal.fromUserInput("44", 0).ceil().toString()).toEqual("44");
279+
expect(Decimal.fromUserInput("-2", 0).ceil().toString()).toEqual("-2");
244280
expect(Decimal.fromUserInput("0", 3).ceil().toString()).toEqual("0");
245281
expect(Decimal.fromUserInput("1", 3).ceil().toString()).toEqual("1");
246282
expect(Decimal.fromUserInput("44", 3).ceil().toString()).toEqual("44");
283+
expect(Decimal.fromUserInput("-2", 3).ceil().toString()).toEqual("-2");
247284

248285
// with fractional part
249286
expect(Decimal.fromUserInput("0.001", 3).ceil().toString()).toEqual("1");
250287
expect(Decimal.fromUserInput("1.999", 3).ceil().toString()).toEqual("2");
251288
expect(Decimal.fromUserInput("0.000000000000000001", 18).ceil().toString()).toEqual("1");
252289
expect(Decimal.fromUserInput("1.999999999999999999", 18).ceil().toString()).toEqual("2");
290+
expect(Decimal.fromUserInput("-0.001", 3).ceil().toString()).toEqual("0");
291+
expect(Decimal.fromUserInput("-1.5", 3).ceil().toString()).toEqual("-1");
253292
});
254293
});
255294

@@ -265,6 +304,17 @@ describe("Decimal", () => {
265304
expect(aaa.fractionalDigits).toEqual(3);
266305
expect(aaaa.toString()).toEqual("1.23");
267306
expect(aaaa.fractionalDigits).toEqual(4);
307+
308+
const n = Decimal.fromUserInput("-1.23", 2);
309+
const nn = n.adjustFractionalDigits(2);
310+
const nnn = n.adjustFractionalDigits(3);
311+
const nnnn = n.adjustFractionalDigits(4);
312+
expect(nn.toString()).toEqual("-1.23");
313+
expect(nn.fractionalDigits).toEqual(2);
314+
expect(nnn.toString()).toEqual("-1.23");
315+
expect(nnn.fractionalDigits).toEqual(3);
316+
expect(nnnn.toString()).toEqual("-1.23");
317+
expect(nnnn.fractionalDigits).toEqual(4);
268318
});
269319

270320
it("can shrink", () => {
@@ -296,6 +346,35 @@ describe("Decimal", () => {
296346
expect(a1.fractionalDigits).toEqual(1);
297347
expect(a0.toString()).toEqual("1");
298348
expect(a0.fractionalDigits).toEqual(0);
349+
350+
const b = Decimal.fromUserInput("-1.23456789", 8);
351+
const b8 = b.adjustFractionalDigits(8);
352+
const b7 = b.adjustFractionalDigits(7);
353+
const b6 = b.adjustFractionalDigits(6);
354+
const b5 = b.adjustFractionalDigits(5);
355+
const b4 = b.adjustFractionalDigits(4);
356+
const b3 = b.adjustFractionalDigits(3);
357+
const b2 = b.adjustFractionalDigits(2);
358+
const b1 = b.adjustFractionalDigits(1);
359+
const b0 = b.adjustFractionalDigits(0);
360+
expect(b8.toString()).toEqual("-1.23456789");
361+
expect(b8.fractionalDigits).toEqual(8);
362+
expect(b7.toString()).toEqual("-1.2345678");
363+
expect(b7.fractionalDigits).toEqual(7);
364+
expect(b6.toString()).toEqual("-1.234567");
365+
expect(b6.fractionalDigits).toEqual(6);
366+
expect(b5.toString()).toEqual("-1.23456");
367+
expect(b5.fractionalDigits).toEqual(5);
368+
expect(b4.toString()).toEqual("-1.2345");
369+
expect(b4.fractionalDigits).toEqual(4);
370+
expect(b3.toString()).toEqual("-1.234");
371+
expect(b3.fractionalDigits).toEqual(3);
372+
expect(b2.toString()).toEqual("-1.23");
373+
expect(b2.fractionalDigits).toEqual(2);
374+
expect(b1.toString()).toEqual("-1.2");
375+
expect(b1.fractionalDigits).toEqual(1);
376+
expect(b0.toString()).toEqual("-1");
377+
expect(b0.fractionalDigits).toEqual(0);
299378
});
300379

301380
it("allows arithmetic between different fractional difits", () => {
@@ -335,6 +414,13 @@ describe("Decimal", () => {
335414
expect(Decimal.fromAtomics("3", 2).toString()).toEqual("0.03");
336415
expect(Decimal.fromAtomics("3", 3).toString()).toEqual("0.003");
337416
});
417+
418+
it("works for negative", () => {
419+
expect(Decimal.fromAtomics(-3n, 0).toString()).toEqual("-3");
420+
expect(Decimal.fromAtomics(-3n, 1).toString()).toEqual("-0.3");
421+
expect(Decimal.fromAtomics(-3n, 2).toString()).toEqual("-0.03");
422+
expect(Decimal.fromAtomics(-3n, 3).toString()).toEqual("-0.003");
423+
});
338424
});
339425

340426
describe("toFloatApproximation", () => {
@@ -344,6 +430,11 @@ describe("Decimal", () => {
344430
expect(Decimal.fromUserInput("1.5", 5).toFloatApproximation()).toEqual(1.5);
345431
expect(Decimal.fromUserInput("0.1", 5).toFloatApproximation()).toEqual(0.1);
346432

433+
expect(Decimal.fromUserInput("-0", 5).toFloatApproximation()).toEqual(0); // -0 cannot be represented in Decimal
434+
expect(Decimal.fromUserInput("-1", 5).toFloatApproximation()).toEqual(-1);
435+
expect(Decimal.fromUserInput("-1.5", 5).toFloatApproximation()).toEqual(-1.5);
436+
expect(Decimal.fromUserInput("-0.1", 5).toFloatApproximation()).toEqual(-0.1);
437+
347438
expect(Decimal.fromUserInput("1234500000000000", 5).toFloatApproximation()).toEqual(1.2345e15);
348439
expect(Decimal.fromUserInput("1234500000000000.002", 5).toFloatApproximation()).toEqual(1.2345e15);
349440
});
@@ -365,6 +456,13 @@ describe("Decimal", () => {
365456
expect(one.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("3.8");
366457
expect(one.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.12345");
367458

459+
const minusOne = Decimal.fromUserInput("-1", 5);
460+
expect(minusOne.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("-1");
461+
expect(minusOne.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("0");
462+
expect(minusOne.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("1");
463+
expect(minusOne.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("1.8");
464+
expect(minusOne.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("-0.87655");
465+
368466
const oneDotFive = Decimal.fromUserInput("1.5", 5);
369467
expect(oneDotFive.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1.5");
370468
expect(oneDotFive.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2.5");
@@ -375,6 +473,7 @@ describe("Decimal", () => {
375473
// original value remain unchanged
376474
expect(zero.toString()).toEqual("0");
377475
expect(one.toString()).toEqual("1");
476+
expect(minusOne.toString()).toEqual("-1");
378477
expect(oneDotFive.toString()).toEqual("1.5");
379478
});
380479

@@ -430,11 +529,11 @@ describe("Decimal", () => {
430529
expect(() => Decimal.fromUserInput("1", 7).minus(zero)).toThrowError(/do not match/i);
431530
});
432531

433-
it("throws for negative results", () => {
532+
it("works for negative results", () => {
434533
const one = Decimal.fromUserInput("1", 5);
435-
expect(() => Decimal.fromUserInput("0", 5).minus(one)).toThrowError(/must not be negative/i);
436-
expect(() => Decimal.fromUserInput("0.5", 5).minus(one)).toThrowError(/must not be negative/i);
437-
expect(() => Decimal.fromUserInput("0.98765", 5).minus(one)).toThrowError(/must not be negative/i);
534+
expect(Decimal.fromUserInput("0", 5).minus(one).toString()).toEqual("-1");
535+
expect(Decimal.fromUserInput("0.5", 5).minus(one).toString()).toEqual("-0.5");
536+
expect(Decimal.fromUserInput("0.98765", 5).minus(one).toString()).toEqual("-0.01235");
438537
});
439538
});
440539

@@ -519,6 +618,38 @@ describe("Decimal", () => {
519618
});
520619
});
521620

621+
describe("neg", () => {
622+
it("works", () => {
623+
// There is only one zero which negates to itself
624+
expect(Decimal.zero(2).neg()).toEqual(Decimal.zero(2));
625+
expect(Decimal.fromUserInput("-0", 4).neg()).toEqual(Decimal.fromUserInput("0", 4));
626+
627+
// positive to negative
628+
expect(Decimal.fromAtomics(1n, 4).neg()).toEqual(Decimal.fromAtomics(-1n, 4));
629+
expect(Decimal.fromAtomics(8743181344348n, 4).neg()).toEqual(Decimal.fromAtomics(-8743181344348n, 4));
630+
631+
// negative to positive
632+
expect(Decimal.fromAtomics(-1n, 4).neg()).toEqual(Decimal.fromAtomics(1n, 4));
633+
expect(Decimal.fromAtomics(-41146784348412n, 4).neg()).toEqual(Decimal.fromAtomics(41146784348412n, 4));
634+
});
635+
});
636+
637+
describe("abs", () => {
638+
it("works", () => {
639+
// There is only one zero which negates to itself
640+
expect(Decimal.zero(2).abs()).toEqual(Decimal.zero(2));
641+
expect(Decimal.fromUserInput("-0", 4).abs()).toEqual(Decimal.fromUserInput("0", 4));
642+
643+
// positive input
644+
expect(Decimal.fromAtomics(1n, 4).abs()).toEqual(Decimal.fromAtomics(1n, 4));
645+
expect(Decimal.fromAtomics(8743181344348n, 4).abs()).toEqual(Decimal.fromAtomics(8743181344348n, 4));
646+
647+
// negative input
648+
expect(Decimal.fromAtomics(-1n, 4).neg()).toEqual(Decimal.fromAtomics(1n, 4));
649+
expect(Decimal.fromAtomics(-41146784348412n, 4).neg()).toEqual(Decimal.fromAtomics(41146784348412n, 4));
650+
});
651+
});
652+
522653
describe("equals", () => {
523654
it("returns correct values", () => {
524655
const zero = Decimal.fromUserInput("0", 5);
@@ -543,10 +674,25 @@ describe("Decimal", () => {
543674
expect(oneDotFive.equals(Decimal.fromUserInput("2.8", 5))).toEqual(false);
544675
expect(oneDotFive.equals(Decimal.fromUserInput("0.12345", 5))).toEqual(false);
545676

677+
const minusTwoDotEight = Decimal.fromUserInput("-2.8", 5);
678+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("0", 5))).toEqual(false);
679+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("1", 5))).toEqual(false);
680+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("1.5", 5))).toEqual(false);
681+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("2", 5))).toEqual(false);
682+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("2.8", 5))).toEqual(false);
683+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("0.12345", 5))).toEqual(false);
684+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-0", 5))).toEqual(false);
685+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-1", 5))).toEqual(false);
686+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-1.5", 5))).toEqual(false);
687+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-2", 5))).toEqual(false);
688+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-2.8", 5))).toEqual(true);
689+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-0.12345", 5))).toEqual(false);
690+
546691
// original value remain unchanged
547692
expect(zero.toString()).toEqual("0");
548693
expect(one.toString()).toEqual("1");
549694
expect(oneDotFive.toString()).toEqual("1.5");
695+
expect(minusTwoDotEight.toString()).toEqual("-2.8");
550696
});
551697

552698
it("throws for different fractional digits", () => {

0 commit comments

Comments
 (0)