Skip to content

Commit 0672a45

Browse files
committed
Add negative value support for Decimal
1 parent 724ca30 commit 0672a45

File tree

3 files changed

+171
-23
lines changed

3 files changed

+171
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ 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 in all interfaces.
114115

115116
[#1883]: https://github.com/cosmos/cosmjs/issues/1883
116117
[#1866]: https://github.com/cosmos/cosmjs/issues/1866

packages/math/src/decimal.spec.ts

Lines changed: 127 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", () => {
@@ -171,6 +179,17 @@ describe("Decimal", () => {
171179
expect(Decimal.fromUserInput("", 3).atomics).toEqual("0");
172180
});
173181

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+
174193
it("accepts american notation with skipped leading zero", () => {
175194
expect(Decimal.fromUserInput(".1", 3).atomics).toEqual("100");
176195
expect(Decimal.fromUserInput(".12", 3).atomics).toEqual("120");
@@ -228,15 +247,21 @@ describe("Decimal", () => {
228247
expect(Decimal.fromUserInput("0", 0).floor().toString()).toEqual("0");
229248
expect(Decimal.fromUserInput("1", 0).floor().toString()).toEqual("1");
230249
expect(Decimal.fromUserInput("44", 0).floor().toString()).toEqual("44");
250+
expect(Decimal.fromUserInput("-2", 0).floor().toString()).toEqual("-2");
231251
expect(Decimal.fromUserInput("0", 3).floor().toString()).toEqual("0");
232252
expect(Decimal.fromUserInput("1", 3).floor().toString()).toEqual("1");
233253
expect(Decimal.fromUserInput("44", 3).floor().toString()).toEqual("44");
254+
expect(Decimal.fromUserInput("-2", 3).floor().toString()).toEqual("-2");
234255

235256
// with fractional part
236257
expect(Decimal.fromUserInput("0.001", 3).floor().toString()).toEqual("0");
237258
expect(Decimal.fromUserInput("1.999", 3).floor().toString()).toEqual("1");
238259
expect(Decimal.fromUserInput("0.000000000000000001", 18).floor().toString()).toEqual("0");
239260
expect(Decimal.fromUserInput("1.999999999999999999", 18).floor().toString()).toEqual("1");
261+
expect(Decimal.fromUserInput("-0.001", 3).floor().toString()).toEqual("-1");
262+
expect(Decimal.fromUserInput("-1.999", 3).floor().toString()).toEqual("-2");
263+
expect(Decimal.fromUserInput("-0.000000000000000001", 18).floor().toString()).toEqual("-1");
264+
expect(Decimal.fromUserInput("-1.999999999999999999", 18).floor().toString()).toEqual("-2");
240265
});
241266
});
242267

@@ -246,15 +271,19 @@ describe("Decimal", () => {
246271
expect(Decimal.fromUserInput("0", 0).ceil().toString()).toEqual("0");
247272
expect(Decimal.fromUserInput("1", 0).ceil().toString()).toEqual("1");
248273
expect(Decimal.fromUserInput("44", 0).ceil().toString()).toEqual("44");
274+
expect(Decimal.fromUserInput("-2", 0).ceil().toString()).toEqual("-2");
249275
expect(Decimal.fromUserInput("0", 3).ceil().toString()).toEqual("0");
250276
expect(Decimal.fromUserInput("1", 3).ceil().toString()).toEqual("1");
251277
expect(Decimal.fromUserInput("44", 3).ceil().toString()).toEqual("44");
278+
expect(Decimal.fromUserInput("-2", 3).ceil().toString()).toEqual("-2");
252279

253280
// with fractional part
254281
expect(Decimal.fromUserInput("0.001", 3).ceil().toString()).toEqual("1");
255282
expect(Decimal.fromUserInput("1.999", 3).ceil().toString()).toEqual("2");
256283
expect(Decimal.fromUserInput("0.000000000000000001", 18).ceil().toString()).toEqual("1");
257284
expect(Decimal.fromUserInput("1.999999999999999999", 18).ceil().toString()).toEqual("2");
285+
expect(Decimal.fromUserInput("-0.001", 3).ceil().toString()).toEqual("0");
286+
expect(Decimal.fromUserInput("-1.5", 3).ceil().toString()).toEqual("-1");
258287
});
259288
});
260289

@@ -270,6 +299,17 @@ describe("Decimal", () => {
270299
expect(aaa.fractionalDigits).toEqual(3);
271300
expect(aaaa.toString()).toEqual("1.23");
272301
expect(aaaa.fractionalDigits).toEqual(4);
302+
303+
const n = Decimal.fromUserInput("-1.23", 2);
304+
const nn = n.adjustFractionalDigits(2);
305+
const nnn = n.adjustFractionalDigits(3);
306+
const nnnn = n.adjustFractionalDigits(4);
307+
expect(nn.toString()).toEqual("-1.23");
308+
expect(nn.fractionalDigits).toEqual(2);
309+
expect(nnn.toString()).toEqual("-1.23");
310+
expect(nnn.fractionalDigits).toEqual(3);
311+
expect(nnnn.toString()).toEqual("-1.23");
312+
expect(nnnn.fractionalDigits).toEqual(4);
273313
});
274314

275315
it("can shrink", () => {
@@ -301,6 +341,35 @@ describe("Decimal", () => {
301341
expect(a1.fractionalDigits).toEqual(1);
302342
expect(a0.toString()).toEqual("1");
303343
expect(a0.fractionalDigits).toEqual(0);
344+
345+
const b = Decimal.fromUserInput("-1.23456789", 8);
346+
const b8 = b.adjustFractionalDigits(8);
347+
const b7 = b.adjustFractionalDigits(7);
348+
const b6 = b.adjustFractionalDigits(6);
349+
const b5 = b.adjustFractionalDigits(5);
350+
const b4 = b.adjustFractionalDigits(4);
351+
const b3 = b.adjustFractionalDigits(3);
352+
const b2 = b.adjustFractionalDigits(2);
353+
const b1 = b.adjustFractionalDigits(1);
354+
const b0 = b.adjustFractionalDigits(0);
355+
expect(b8.toString()).toEqual("-1.23456789");
356+
expect(b8.fractionalDigits).toEqual(8);
357+
expect(b7.toString()).toEqual("-1.2345678");
358+
expect(b7.fractionalDigits).toEqual(7);
359+
expect(b6.toString()).toEqual("-1.234567");
360+
expect(b6.fractionalDigits).toEqual(6);
361+
expect(b5.toString()).toEqual("-1.23456");
362+
expect(b5.fractionalDigits).toEqual(5);
363+
expect(b4.toString()).toEqual("-1.2345");
364+
expect(b4.fractionalDigits).toEqual(4);
365+
expect(b3.toString()).toEqual("-1.234");
366+
expect(b3.fractionalDigits).toEqual(3);
367+
expect(b2.toString()).toEqual("-1.23");
368+
expect(b2.fractionalDigits).toEqual(2);
369+
expect(b1.toString()).toEqual("-1.2");
370+
expect(b1.fractionalDigits).toEqual(1);
371+
expect(b0.toString()).toEqual("-1");
372+
expect(b0.fractionalDigits).toEqual(0);
304373
});
305374

306375
it("allows arithmetic between different fractional difits", () => {
@@ -340,6 +409,13 @@ describe("Decimal", () => {
340409
expect(Decimal.fromAtomics("3", 2).toString()).toEqual("0.03");
341410
expect(Decimal.fromAtomics("3", 3).toString()).toEqual("0.003");
342411
});
412+
413+
it("works for negative", () => {
414+
expect(Decimal.fromAtomics(-3n, 0).toString()).toEqual("-3");
415+
expect(Decimal.fromAtomics(-3n, 1).toString()).toEqual("-0.3");
416+
expect(Decimal.fromAtomics(-3n, 2).toString()).toEqual("-0.03");
417+
expect(Decimal.fromAtomics(-3n, 3).toString()).toEqual("-0.003");
418+
});
343419
});
344420

345421
describe("toFloatApproximation", () => {
@@ -349,6 +425,11 @@ describe("Decimal", () => {
349425
expect(Decimal.fromUserInput("1.5", 5).toFloatApproximation()).toEqual(1.5);
350426
expect(Decimal.fromUserInput("0.1", 5).toFloatApproximation()).toEqual(0.1);
351427

428+
expect(Decimal.fromUserInput("-0", 5).toFloatApproximation()).toEqual(0); // -0 cannot be represented in Decimal
429+
expect(Decimal.fromUserInput("-1", 5).toFloatApproximation()).toEqual(-1);
430+
expect(Decimal.fromUserInput("-1.5", 5).toFloatApproximation()).toEqual(-1.5);
431+
expect(Decimal.fromUserInput("-0.1", 5).toFloatApproximation()).toEqual(-0.1);
432+
352433
expect(Decimal.fromUserInput("1234500000000000", 5).toFloatApproximation()).toEqual(1.2345e15);
353434
expect(Decimal.fromUserInput("1234500000000000.002", 5).toFloatApproximation()).toEqual(1.2345e15);
354435
});
@@ -370,6 +451,13 @@ describe("Decimal", () => {
370451
expect(one.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("3.8");
371452
expect(one.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.12345");
372453

454+
const minusOne = Decimal.fromUserInput("-1", 5);
455+
expect(minusOne.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("-1");
456+
expect(minusOne.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("0");
457+
expect(minusOne.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("1");
458+
expect(minusOne.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("1.8");
459+
expect(minusOne.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("-0.87655");
460+
373461
const oneDotFive = Decimal.fromUserInput("1.5", 5);
374462
expect(oneDotFive.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1.5");
375463
expect(oneDotFive.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2.5");
@@ -380,6 +468,7 @@ describe("Decimal", () => {
380468
// original value remain unchanged
381469
expect(zero.toString()).toEqual("0");
382470
expect(one.toString()).toEqual("1");
471+
expect(minusOne.toString()).toEqual("-1");
383472
expect(oneDotFive.toString()).toEqual("1.5");
384473
});
385474

@@ -435,11 +524,11 @@ describe("Decimal", () => {
435524
expect(() => Decimal.fromUserInput("1", 7).minus(zero)).toThrowError(/do not match/i);
436525
});
437526

438-
it("throws for negative results", () => {
527+
it("works for negative results", () => {
439528
const one = Decimal.fromUserInput("1", 5);
440-
expect(() => Decimal.fromUserInput("0", 5).minus(one)).toThrowError(/must not be negative/i);
441-
expect(() => Decimal.fromUserInput("0.5", 5).minus(one)).toThrowError(/must not be negative/i);
442-
expect(() => Decimal.fromUserInput("0.98765", 5).minus(one)).toThrowError(/must not be negative/i);
529+
expect(Decimal.fromUserInput("0", 5).minus(one).toString()).toEqual("-1");
530+
expect(Decimal.fromUserInput("0.5", 5).minus(one).toString()).toEqual("-0.5");
531+
expect(Decimal.fromUserInput("0.98765", 5).minus(one).toString()).toEqual("-0.01235");
443532
});
444533
});
445534

@@ -524,6 +613,22 @@ describe("Decimal", () => {
524613
});
525614
});
526615

616+
describe("neg", () => {
617+
it("works", () => {
618+
// There is only one zero which negates to itself
619+
expect(Decimal.zero(2).neg()).toEqual(Decimal.zero(2));
620+
expect(Decimal.fromUserInput("-0", 4).neg()).toEqual(Decimal.fromUserInput("0", 4));
621+
622+
// positive to negative
623+
expect(Decimal.fromAtomics(1n, 4).neg()).toEqual(Decimal.fromAtomics(-1n, 4));
624+
expect(Decimal.fromAtomics(8743181344348n, 4).neg()).toEqual(Decimal.fromAtomics(-8743181344348n, 4));
625+
626+
// negative to positive
627+
expect(Decimal.fromAtomics(-1n, 4).neg()).toEqual(Decimal.fromAtomics(1n, 4));
628+
expect(Decimal.fromAtomics(-41146784348412n, 4).neg()).toEqual(Decimal.fromAtomics(41146784348412n, 4));
629+
});
630+
});
631+
527632
describe("equals", () => {
528633
it("returns correct values", () => {
529634
const zero = Decimal.fromUserInput("0", 5);
@@ -548,10 +653,25 @@ describe("Decimal", () => {
548653
expect(oneDotFive.equals(Decimal.fromUserInput("2.8", 5))).toEqual(false);
549654
expect(oneDotFive.equals(Decimal.fromUserInput("0.12345", 5))).toEqual(false);
550655

656+
const minusTwoDotEight = Decimal.fromUserInput("-2.8", 5);
657+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("0", 5))).toEqual(false);
658+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("1", 5))).toEqual(false);
659+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("1.5", 5))).toEqual(false);
660+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("2", 5))).toEqual(false);
661+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("2.8", 5))).toEqual(false);
662+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("0.12345", 5))).toEqual(false);
663+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-0", 5))).toEqual(false);
664+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-1", 5))).toEqual(false);
665+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-1.5", 5))).toEqual(false);
666+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-2", 5))).toEqual(false);
667+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-2.8", 5))).toEqual(true);
668+
expect(minusTwoDotEight.equals(Decimal.fromUserInput("-0.12345", 5))).toEqual(false);
669+
551670
// original value remain unchanged
552671
expect(zero.toString()).toEqual("0");
553672
expect(one.toString()).toEqual("1");
554673
expect(oneDotFive.toString()).toEqual("1.5");
674+
expect(minusTwoDotEight.toString()).toEqual("-2.8");
555675
});
556676

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

0 commit comments

Comments
 (0)