Skip to content

Commit 0f280ae

Browse files
authored
Merge pull request #1916 from cosmos/decimal-expand-shrink
Add `Decimal.adjustFractionalDigits`
2 parents 0cf58e5 + cf77ab6 commit 0f280ae

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ and this project adheres to
1414
specifying the buffer type but you need an `ArrayBuffer`. Internally it might
1515
perform a copy but in the vast majority of cases it will just change the type
1616
after ensuring `ArrayBuffer` is used. ([#1883])
17+
- @cosmjs/math: Add `Decimal.adjustFractionalDigits` which allows you to change
18+
the fractional digits of a Decimal without changing its value. ([#1916])
1719

1820
[#1883]: https://github.com/cosmos/cosmjs/issues/1883
21+
[#1916]: https://github.com/cosmos/cosmjs/pull/1916
1922

2023
### Changed
2124

packages/math/src/decimal.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,64 @@ describe("Decimal", () => {
225225
});
226226
});
227227

228+
describe("adjustFractionalDigits", () => {
229+
it("can expand", () => {
230+
const a = Decimal.fromUserInput("1.23", 2);
231+
const aa = a.adjustFractionalDigits(2);
232+
const aaa = a.adjustFractionalDigits(3);
233+
const aaaa = a.adjustFractionalDigits(4);
234+
expect(aa.toString()).toEqual("1.23");
235+
expect(aa.fractionalDigits).toEqual(2);
236+
expect(aaa.toString()).toEqual("1.23");
237+
expect(aaa.fractionalDigits).toEqual(3);
238+
expect(aaaa.toString()).toEqual("1.23");
239+
expect(aaaa.fractionalDigits).toEqual(4);
240+
});
241+
242+
it("can shrink", () => {
243+
const a = Decimal.fromUserInput("1.23456789", 8);
244+
const a8 = a.adjustFractionalDigits(8);
245+
const a7 = a.adjustFractionalDigits(7);
246+
const a6 = a.adjustFractionalDigits(6);
247+
const a5 = a.adjustFractionalDigits(5);
248+
const a4 = a.adjustFractionalDigits(4);
249+
const a3 = a.adjustFractionalDigits(3);
250+
const a2 = a.adjustFractionalDigits(2);
251+
const a1 = a.adjustFractionalDigits(1);
252+
const a0 = a.adjustFractionalDigits(0);
253+
expect(a8.toString()).toEqual("1.23456789");
254+
expect(a8.fractionalDigits).toEqual(8);
255+
expect(a7.toString()).toEqual("1.2345678");
256+
expect(a7.fractionalDigits).toEqual(7);
257+
expect(a6.toString()).toEqual("1.234567");
258+
expect(a6.fractionalDigits).toEqual(6);
259+
expect(a5.toString()).toEqual("1.23456");
260+
expect(a5.fractionalDigits).toEqual(5);
261+
expect(a4.toString()).toEqual("1.2345");
262+
expect(a4.fractionalDigits).toEqual(4);
263+
expect(a3.toString()).toEqual("1.234");
264+
expect(a3.fractionalDigits).toEqual(3);
265+
expect(a2.toString()).toEqual("1.23");
266+
expect(a2.fractionalDigits).toEqual(2);
267+
expect(a1.toString()).toEqual("1.2");
268+
expect(a1.fractionalDigits).toEqual(1);
269+
expect(a0.toString()).toEqual("1");
270+
expect(a0.fractionalDigits).toEqual(0);
271+
});
272+
273+
it("allows arithmetic between different fractional difits", () => {
274+
const a = Decimal.fromUserInput("5.33", 2);
275+
const b = Decimal.fromUserInput("2.1", 1);
276+
expect(() => a.plus(b)).toThrowError(/Fractional digits do not match/i); // maybe not convenient but this is what we expect
277+
278+
const bb = b.adjustFractionalDigits(a.fractionalDigits);
279+
expect(a.plus(bb).toString()).toEqual("7.43");
280+
281+
const aa = a.adjustFractionalDigits(b.fractionalDigits);
282+
expect(aa.plus(b).toString()).toEqual("7.4");
283+
});
284+
});
285+
228286
describe("toString", () => {
229287
it("displays no decimal point for full numbers", () => {
230288
expect(Decimal.fromUserInput("44", 0).toString()).toEqual("44");

packages/math/src/decimal.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,19 @@ export class Decimal {
4949
throw new Error("Got more fractional digits than supported");
5050
}
5151

52-
const quantity = `${whole}${fractional.padEnd(fractionalDigits, "0")}`;
52+
const quantity = BigInt(`${whole}${fractional.padEnd(fractionalDigits, "0")}`);
5353

5454
return new Decimal(quantity, fractionalDigits);
5555
}
5656

5757
public static fromAtomics(atomics: string, fractionalDigits: number): Decimal {
5858
Decimal.verifyFractionalDigits(fractionalDigits);
59-
return new Decimal(atomics, fractionalDigits);
59+
if (!atomics.match(/^[0-9]+$/)) {
60+
throw new Error(
61+
"Invalid string format. Only non-negative integers in decimal representation supported.",
62+
);
63+
}
64+
return new Decimal(BigInt(atomics), fractionalDigits);
6065
}
6166

6267
/**
@@ -67,7 +72,7 @@ export class Decimal {
6772
*/
6873
public static zero(fractionalDigits: number): Decimal {
6974
Decimal.verifyFractionalDigits(fractionalDigits);
70-
return new Decimal("0", fractionalDigits);
75+
return new Decimal(0n, fractionalDigits);
7176
}
7277

7378
/**
@@ -78,7 +83,7 @@ export class Decimal {
7883
*/
7984
public static one(fractionalDigits: number): Decimal {
8085
Decimal.verifyFractionalDigits(fractionalDigits);
81-
return new Decimal("1" + "0".repeat(fractionalDigits), fractionalDigits);
86+
return new Decimal(10n ** BigInt(fractionalDigits), fractionalDigits);
8287
}
8388

8489
private static verifyFractionalDigits(fractionalDigits: number): void {
@@ -110,22 +115,16 @@ export class Decimal {
110115
readonly fractionalDigits: number;
111116
};
112117

113-
private constructor(atomics: string, fractionalDigits: number) {
114-
if (!atomics.match(/^[0-9]+$/)) {
115-
throw new Error(
116-
"Invalid string format. Only non-negative integers in decimal representation supported.",
117-
);
118-
}
119-
118+
private constructor(atomics: bigint, fractionalDigits: number) {
120119
this.data = {
121-
atomics: BigInt(atomics),
120+
atomics: atomics,
122121
fractionalDigits: fractionalDigits,
123122
};
124123
}
125124

126125
/** Creates a new instance with the same value */
127126
private clone(): Decimal {
128-
return new Decimal(this.atomics, this.fractionalDigits);
127+
return new Decimal(this.data.atomics, this.data.fractionalDigits);
129128
}
130129

131130
/** Returns the greatest decimal <= this which has no fractional part (rounding down) */
@@ -137,7 +136,7 @@ export class Decimal {
137136
if (fractional === 0n) {
138137
return this.clone();
139138
} else {
140-
return Decimal.fromAtomics((whole * factor).toString(), this.fractionalDigits);
139+
return new Decimal(whole * factor, this.fractionalDigits);
141140
}
142141
}
143142

@@ -150,7 +149,31 @@ export class Decimal {
150149
if (fractional === 0n) {
151150
return this.clone();
152151
} else {
153-
return Decimal.fromAtomics(((whole + 1n) * factor).toString(), this.fractionalDigits);
152+
return new Decimal((whole + 1n) * factor, this.fractionalDigits);
153+
}
154+
}
155+
156+
/**
157+
* Creates a new Decimal with the same value using the new fractional digits.
158+
* Roughly speaking this can expand an 3.24 to 3.24000 or shrink a 5.4321 to 5.4.
159+
*
160+
* This allows you to perform arithmetic operations given two decimals
161+
* with different fractional digits by normalizing them.
162+
*
163+
* When new fractional digis is smaller than the original value, the amount
164+
* is truncated (not rounded!).
165+
*/
166+
public adjustFractionalDigits(newFractionalDigits: number): Decimal {
167+
Decimal.verifyFractionalDigits(newFractionalDigits);
168+
const diff = newFractionalDigits - this.fractionalDigits;
169+
if (diff > 0) {
170+
// expand
171+
return new Decimal(this.data.atomics * 10n ** BigInt(diff), newFractionalDigits);
172+
} else if (diff === 0) {
173+
return this.clone();
174+
} else {
175+
// shrink
176+
return new Decimal(this.data.atomics / 10n ** BigInt(-diff), newFractionalDigits);
154177
}
155178
}
156179

@@ -186,7 +209,7 @@ export class Decimal {
186209
public plus(b: Decimal): Decimal {
187210
if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match");
188211
const sum = this.data.atomics + b.data.atomics;
189-
return new Decimal(sum.toString(), this.fractionalDigits);
212+
return new Decimal(sum, this.fractionalDigits);
190213
}
191214

192215
/**
@@ -199,7 +222,7 @@ export class Decimal {
199222
if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match");
200223
const difference = this.data.atomics - b.data.atomics;
201224
if (difference < 0n) throw new Error("Difference must not be negative");
202-
return new Decimal(difference.toString(), this.fractionalDigits);
225+
return new Decimal(difference, this.fractionalDigits);
203226
}
204227

205228
/**
@@ -209,7 +232,7 @@ export class Decimal {
209232
*/
210233
public multiply(b: Uint32 | Uint53 | Uint64): Decimal {
211234
const product = this.data.atomics * b.toBigInt();
212-
return new Decimal(product.toString(), this.fractionalDigits);
235+
return new Decimal(product, this.fractionalDigits);
213236
}
214237

215238
public equals(b: Decimal): boolean {

0 commit comments

Comments
 (0)