1import { describe, it } from "@effect/vitest"
2import {
3 assertEquals,
4 assertFalse,
5 assertNone,
6 assertTrue,
7 deepStrictEqual,
8 strictEqual,
9 throws
10} from "@effect/vitest/utils"
11import { BigDecimal, Equal, Hash, Option } from "effect"
12import * as fc from "fast-check"
13
14const $ = BigDecimal.fromStringUnsafe
15
16const assertSomeBigDecimal = (option: Option.Option<BigDecimal.BigDecimal>, expected: BigDecimal.BigDecimal) => {
17 if (Option.isNone(option)) {
18 throw new Error("Expected Option.some")
19 }
20 assertEquals(option.value, expected)
21}
22
23const assertDivide = (x: string, y: string, z: string) => {
24 assertSomeBigDecimal(BigDecimal.divide($(x), $(y)), $(z))
25 assertEquals(BigDecimal.divideUnsafe($(x), $(y)), $(z))
26}
27
28describe("BigDecimal", () => {
29 it("make", () => {
30 strictEqual(BigDecimal.make(1n, Number.MAX_SAFE_INTEGER).scale, Number.MAX_SAFE_INTEGER)
31 strictEqual(BigDecimal.make(1n, Number.MIN_SAFE_INTEGER).scale, Number.MIN_SAFE_INTEGER)
32 for (const scale of [0.5, NaN, Infinity, -Infinity, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 1]) {
33 throws(() => BigDecimal.make(1n, scale), new RangeError(`Scale must be a safe integer, got ${scale}`))
34 }
35 })
36
37 it("makeNormalizedUnsafe validates the value and scale", () => {
38 throws(() => BigDecimal.makeNormalizedUnsafe(120n, 0), new RangeError("Value must be normalized"))
39 throws(() => BigDecimal.makeNormalizedUnsafe(-120n, 0), new RangeError("Value must be normalized"))
40 throws(() => BigDecimal.makeNormalizedUnsafe(1n, 0.5), new RangeError("Scale must be a safe integer, got 0.5"))
41 })
42
43 it("isBigDecimal", () => {
44 assertTrue(BigDecimal.isBigDecimal($("0")))
45 assertTrue(BigDecimal.isBigDecimal($("987")))
46 assertTrue(BigDecimal.isBigDecimal($("123.0")))
47 assertTrue(BigDecimal.isBigDecimal($("0.123")))
48 assertTrue(BigDecimal.isBigDecimal($("123.456")))
49 assertFalse(BigDecimal.isBigDecimal("1"))
50 assertFalse(BigDecimal.isBigDecimal(true))
51 })
52
53 it("sign", () => {
54 strictEqual(BigDecimal.sign($("-5")), -1)
55 strictEqual(BigDecimal.sign($("0")), 0)
56 strictEqual(BigDecimal.sign($("5")), 1)
57 strictEqual(BigDecimal.sign($("-123.456")), -1)
58 strictEqual(BigDecimal.sign($("456.789")), 1)
59 })
60
61 it("equals", () => {
62 assertTrue(BigDecimal.equals($("1"), $("1")))
63 assertTrue(BigDecimal.equals($("0.00012300"), $("0.000123")))
64 assertTrue(BigDecimal.equals($("5"), $("5.0")))
65 assertTrue(BigDecimal.equals($("123.0000"), $("123.00")))
66 assertFalse(BigDecimal.equals($("1"), $("2")))
67 assertFalse(BigDecimal.equals($("1"), $("1.1")))
68 assertFalse(BigDecimal.equals($("1"), $("0.1")))
69 })
70
71 it("sum", () => {
72 assertEquals(BigDecimal.sum($("2"), $("0")), $("2"))
73 assertEquals(BigDecimal.sum($("0"), $("2")), $("2"))
74 assertEquals(BigDecimal.sum($("0"), $("0")), $("0"))
75 assertEquals(BigDecimal.sum($("2"), $("1")), $("3"))
76 assertEquals(BigDecimal.sum($("3.00000"), $("50")), $("53"))
77 assertEquals(BigDecimal.sum($("1.23"), $("0.0045678")), $("1.2345678"))
78 assertEquals(BigDecimal.sum($("123.456"), $("-123.456")), $("0"))
79 })
80
81 it("sumAll", () => {
82 assertEquals(BigDecimal.sumAll([]), $("0"))
83 assertEquals(BigDecimal.sumAll([$("2"), $("3"), $("4")]), $("9"))
84 assertEquals(BigDecimal.sumAll([$("1.5"), $("-1.5")]), $("0"))
85 })
86
87 it("multiply", () => {
88 assertEquals(BigDecimal.multiply($("3"), $("2")), $("6"))
89 assertEquals(BigDecimal.multiply($("3"), $("0")), $("0"))
90 assertEquals(BigDecimal.multiply($("3"), $("-1")), $("-3"))
91 assertEquals(BigDecimal.multiply($("3"), $("0.5")), $("1.5"))
92 assertEquals(BigDecimal.multiply($("3"), $("-2.5")), $("-7.5"))
93 })
94
95 it("multiplyAll", () => {
96 assertEquals(BigDecimal.multiplyAll([]), $("1"))
97 assertEquals(BigDecimal.multiplyAll([$("2"), $("3"), $("4")]), $("24"))
98 assertEquals(BigDecimal.multiplyAll([$("2"), $("0"), $("4")]), $("0"))
99 })
100
101 it("subtract", () => {
102 assertEquals(BigDecimal.subtract($("0"), $("1")), $("-1"))
103 assertEquals(BigDecimal.subtract($("2.1"), $("1")), $("1.1"))
104 assertEquals(BigDecimal.subtract($("3"), $("1")), $("2"))
105 assertEquals(BigDecimal.subtract($("3"), $("0")), $("3"))
106 assertEquals(BigDecimal.subtract($("3"), $("-1")), $("4"))
107 assertEquals(BigDecimal.subtract($("3"), $("0.5")), $("2.5"))
108 assertEquals(BigDecimal.subtract($("3"), $("-2.5")), $("5.5"))
109 })
110
111 it("roundTerminal", () => {
112 strictEqual(BigDecimal.roundTerminal(0n), 0n)
113 strictEqual(BigDecimal.roundTerminal(4n), 0n)
114 strictEqual(BigDecimal.roundTerminal(5n), 1n)
115 strictEqual(BigDecimal.roundTerminal(9n), 1n)
116 strictEqual(BigDecimal.roundTerminal(49n), 0n)
117 strictEqual(BigDecimal.roundTerminal(59n), 1n)
118 strictEqual(BigDecimal.roundTerminal(99n), 1n)
119 strictEqual(BigDecimal.roundTerminal(-4n), 0n)
120 strictEqual(BigDecimal.roundTerminal(-5n), 1n)
121 strictEqual(BigDecimal.roundTerminal(-9n), 1n)
122 strictEqual(BigDecimal.roundTerminal(-49n), 0n)
123 strictEqual(BigDecimal.roundTerminal(-59n), 1n)
124 strictEqual(BigDecimal.roundTerminal(-99n), 1n)
125 })
126
127 it("divide", () => {
128 assertDivide("0", "1", "0")
129 assertDivide("0", "10", "0")
130 assertDivide("2", "1", "2")
131 assertDivide("20", "1", "20")
132 assertDivide("10", "10", "1")
133 assertDivide("100", "10.0", "10")
134 assertDivide("20.0", "200", "0.1")
135 assertDivide("4", "2", "2.0")
136 assertDivide("15", "3", "5.0")
137 assertDivide("1", "2", "0.5")
138 assertDivide("1", "0.02", "50")
139 assertDivide("1", "0.2", "5")
140 assertDivide("1.0", "0.02", "50")
141 assertDivide("1", "0.020", "50")
142 assertDivide("5.0", "4.00", "1.25")
143 assertDivide("5.0", "4.000", "1.25")
144 assertDivide("5", "4.000", "1.25")
145 assertDivide("5", "4", "1.25")
146 assertDivide("100", "5", "20")
147 assertDivide("-50", "5", "-10")
148 assertDivide("200", "-5", "-40.0")
149 assertDivide(
150 "1",
151 "3",
152 "0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
153 )
154 assertDivide(
155 "-2",
156 "-3",
157 "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667"
158 )
159 assertDivide(
160 "-12.34",
161 "1.233",
162 "-10.00811030008110300081103000811030008110300081103000811030008110300081103000811030008110300081103001"
163 )
164 assertDivide(
165 "125348",
166 "352.2283",
167 "355.8714617763535752237966114591019517738921035021887792661748076460636467881768727839301952739175132"
168 )
169
170 assertNone(BigDecimal.divide($("5"), $("0")))
171 throws(() => BigDecimal.divideUnsafe($("5"), $("0")), new RangeError("Division by zero"))
172 })
173
174 it("Equivalence", () => {
175 assertTrue(BigDecimal.Equivalence($("1"), $("1")))
176 assertTrue(BigDecimal.Equivalence($("0.00012300"), $("0.000123")))
177 assertTrue(BigDecimal.Equivalence($("5"), $("5.00")))
178 assertFalse(BigDecimal.Equivalence($("1"), $("2")))
179 assertFalse(BigDecimal.Equivalence($("1"), $("1.1")))
180 })
181
182 it("Order", () => {
183 strictEqual(BigDecimal.Order($("1"), $("2")), -1)
184 strictEqual(BigDecimal.Order($("2"), $("1")), 1)
185 strictEqual(BigDecimal.Order($("2"), $("2")), 0)
186 strictEqual(BigDecimal.Order($("1"), $("1.1")), -1)
187 strictEqual(BigDecimal.Order($("1.1"), $("1")), 1)
188 strictEqual(BigDecimal.Order($("0.00012300"), $("0.000123")), 0)
189 strictEqual(BigDecimal.Order($("5"), $("5.000")), 0)
190 strictEqual(BigDecimal.Order($("5"), $("0.500")), 1)
191 strictEqual(BigDecimal.Order($("5"), $("50.00")), -1)
192 })
193
194 it("Order and Equivalence support extreme scale differences", () => {
195 const positive = BigDecimal.make(1n, 0)
196 const negative = BigDecimal.make(-1n, 0)
197 const positiveTiny = BigDecimal.make(1n, Number.MAX_SAFE_INTEGER)
198 const negativeTiny = BigDecimal.make(-1n, Number.MAX_SAFE_INTEGER)
199 const zeroTiny = BigDecimal.make(0n, Number.MAX_SAFE_INTEGER)
200
201 strictEqual(BigDecimal.Order(positive, positiveTiny), 1)
202 strictEqual(BigDecimal.Order(positiveTiny, positive), -1)
203 strictEqual(BigDecimal.Order(negative, negativeTiny), -1)
204 strictEqual(BigDecimal.Order(negativeTiny, negative), 1)
205 strictEqual(BigDecimal.Order(BigDecimal.make(0n, 0), zeroTiny), 0)
206 assertFalse(BigDecimal.Equivalence(positive, positiveTiny))
207 assertTrue(BigDecimal.Equivalence(BigDecimal.make(0n, 0), zeroTiny))
208
209 const equal = Object.freeze(BigDecimal.make(10n ** 101n, 101))
210 strictEqual(BigDecimal.Order(Object.freeze(BigDecimal.make(1n, 0)), equal), 0)
211 assertTrue(BigDecimal.Equivalence(Object.freeze(BigDecimal.make(1n, 0)), equal))
212
213 const scale = Number.MAX_SAFE_INTEGER
214 const below = BigDecimal.make(9n, -scale)
215 const above = BigDecimal.make(10n ** 102n, -(scale - 101))
216 strictEqual(BigDecimal.Order(below, above), -1)
217 strictEqual(BigDecimal.Order(above, below), 1)
218 assertFalse(BigDecimal.Equivalence(below, above))
219
220 const power = 10n ** 102n
221 for (const sign of [-1n, 1n]) {
222 const exact = BigDecimal.make(sign, -1)
223 const next = BigDecimal.make(sign * (power + 1n), 101)
224 strictEqual(BigDecimal.Order(exact, next), sign === 1n ? -1 : 1)
225 strictEqual(BigDecimal.Order(next, exact), sign === 1n ? 1 : -1)
226 assertFalse(BigDecimal.Equivalence(exact, next))
227 }
228 })
229
230 it("Order and Equivalence agree with exact bounded scale alignment", () => {
231 const values = [-12345n, -100n, -1n, 0n, 1n, 100n, 12345n]
232 const scales = [-102, -101, -100, -8, -1, 0, 1, 8, 100, 101, 102]
233 const decimals = values.flatMap((value) => scales.map((scale) => BigDecimal.make(value, scale)))
234
235 for (const self of decimals) {
236 for (const that of decimals) {
237 const scale = Math.max(self.scale, that.scale)
238 const selfValue = BigDecimal.scale(self, scale).value
239 const thatValue = BigDecimal.scale(that, scale).value
240 const expected = selfValue < thatValue ? -1 : selfValue > thatValue ? 1 : 0
241 strictEqual(BigDecimal.Order(self, that), expected)
242 strictEqual(BigDecimal.Equivalence(self, that), expected === 0)
243 }
244 }
245 })
246
247 it("isLessThan", () => {
248 assertTrue(BigDecimal.isLessThan($("2"), $("3")))
249 assertFalse(BigDecimal.isLessThan($("3"), $("3")))
250 assertFalse(BigDecimal.isLessThan($("4"), $("3")))
251 })
252
253 it("isLessThanOrEqualTo", () => {
254 assertTrue(BigDecimal.isLessThanOrEqualTo($("2"), $("3")))
255 assertTrue(BigDecimal.isLessThanOrEqualTo($("3"), $("3")))
256 assertFalse(BigDecimal.isLessThanOrEqualTo($("4"), $("3")))
257 })
258
259 it("isGreaterThan", () => {
260 assertFalse(BigDecimal.isGreaterThan($("2"), $("3")))
261 assertFalse(BigDecimal.isGreaterThan($("3"), $("3")))
262 assertTrue(BigDecimal.isGreaterThan($("4"), $("3")))
263 })
264
265 it("isGreaterThanOrEqualTo", () => {
266 assertFalse(BigDecimal.isGreaterThanOrEqualTo($("2"), $("3")))
267 assertTrue(BigDecimal.isGreaterThanOrEqualTo($("3"), $("3")))
268 assertTrue(BigDecimal.isGreaterThanOrEqualTo($("4"), $("3")))
269 })
270
271 it("between", () => {
272 assertTrue(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("3")))
273 assertFalse(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("-1")))
274 assertFalse(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("6")))
275 assertFalse(BigDecimal.between({ minimum: $("0.02"), maximum: $("5") })($("0.0123")))
276 assertTrue(BigDecimal.between({ minimum: $("0.02"), maximum: $("5") })($("0.05")))
277
278 assertTrue(BigDecimal.between($("3"), { minimum: $("0"), maximum: $("5") }))
279 })
280
281 it("clamp", () => {
282 assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("3")), $("3"))
283 assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("-1")), $("0"))
284 assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("6")), $("5"))
285 assertEquals(BigDecimal.clamp({ minimum: $("0.02"), maximum: $("5") })($("0.0123")), $("0.02"))
286
287 assertEquals(BigDecimal.clamp($("3"), { minimum: $("0"), maximum: $("5") }), $("3"))
288 })
289
290 it("min", () => {
291 assertEquals(BigDecimal.min($("2"), $("3")), $("2"))
292 assertEquals(BigDecimal.min($("5"), $("0.1")), $("0.1"))
293 assertEquals(BigDecimal.min($("0.005"), $("3")), $("0.005"))
294 assertEquals(BigDecimal.min($("123.456"), $("1.2")), $("1.2"))
295 })
296
297 it("max", () => {
298 assertEquals(BigDecimal.max($("2"), $("3")), $("3"))
299 assertEquals(BigDecimal.max($("5"), $("0.1")), $("5"))
300 assertEquals(BigDecimal.max($("0.005"), $("3")), $("3"))
301 assertEquals(BigDecimal.max($("123.456"), $("1.2")), $("123.456"))
302 })
303
304 it("abs", () => {
305 assertEquals(BigDecimal.abs($("2")), $("2"))
306 assertEquals(BigDecimal.abs($("-3")), $("3"))
307 assertEquals(BigDecimal.abs($("0.000456")), $("0.000456"))
308 assertEquals(BigDecimal.abs($("-0.123")), $("0.123"))
309 })
310
311 it("negate", () => {
312 assertEquals(BigDecimal.negate($("2")), $("-2"))
313 assertEquals(BigDecimal.negate($("-3")), $("3"))
314 assertEquals(BigDecimal.negate($("0.000456")), $("-0.000456"))
315 assertEquals(BigDecimal.negate($("-0.123")), $("0.123"))
316 })
317
318 it("remainder", () => {
319 assertSomeBigDecimal(BigDecimal.remainder($("5"), $("2")), $("1"))
320 assertSomeBigDecimal(BigDecimal.remainder($("4"), $("2")), $("0"))
321 assertSomeBigDecimal(BigDecimal.remainder($("123.456"), $("0.2")), $("0.056"))
322 assertNone(BigDecimal.remainder($("5"), $("0")))
323 })
324
325 it("unsafeRemainder", () => {
326 assertEquals(BigDecimal.remainderUnsafe($("5"), $("2")), $("1"))
327 assertEquals(BigDecimal.remainderUnsafe($("4"), $("2")), $("0"))
328 assertEquals(BigDecimal.remainderUnsafe($("123.456"), $("0.2")), $("0.056"))
329 throws(() => BigDecimal.remainderUnsafe($("5"), $("0")), new RangeError("Division by zero"))
330 })
331
332 it("normalize", () => {
333 deepStrictEqual(BigDecimal.normalize($("0")), BigDecimal.makeNormalizedUnsafe(0n, 0))
334 deepStrictEqual(BigDecimal.normalize($("0.123000")), BigDecimal.makeNormalizedUnsafe(123n, 3))
335 deepStrictEqual(BigDecimal.normalize($("123.000")), BigDecimal.makeNormalizedUnsafe(123n, 0))
336 deepStrictEqual(BigDecimal.normalize($("-0.000123000")), BigDecimal.makeNormalizedUnsafe(-123n, 6))
337 deepStrictEqual(BigDecimal.normalize($("-123.000")), BigDecimal.makeNormalizedUnsafe(-123n, 0))
338 deepStrictEqual(BigDecimal.normalize($("12300000")), BigDecimal.makeNormalizedUnsafe(123n, -5))
339 })
340
341 it("normalize caches results without changing the original value or scale", () => {
342 for (
343 const [value, scale, expectedValue, expectedScale] of [
344 [0n, Number.MAX_SAFE_INTEGER, 0n, 0],
345 [0n, Number.MIN_SAFE_INTEGER, 0n, 0],
346 [123n, 2, 123n, 2],
347 [-123000n, 2, -123n, -1],
348 [10n ** 4097n, 4097, 1n, 0],
349 [100n, Number.MIN_SAFE_INTEGER + 2, 1n, Number.MIN_SAFE_INTEGER]
350 ] as const
351 ) {
352 const decimal = BigDecimal.make(value, scale)
353 const normalized = BigDecimal.normalize(decimal)
354 strictEqual(normalized.value, expectedValue)
355 strictEqual(normalized.scale, expectedScale)
356 strictEqual(BigDecimal.normalize(decimal), normalized)
357 strictEqual(BigDecimal.normalize(normalized), normalized)
358 strictEqual(decimal.value, value)
359 strictEqual(decimal.scale, scale)
360 }
361 })
362
363 it("normalize rejects an unsafe resulting scale", () => {
364 const decimal = BigDecimal.make(10n, Number.MIN_SAFE_INTEGER)
365 const error = new RangeError(`Scale must be a safe integer, got ${Number.MIN_SAFE_INTEGER - 1}`)
366 throws(() => BigDecimal.normalize(decimal), error)
367 throws(() => BigDecimal.normalize(decimal), error)
368 })
369
370 it("fromString", () => {
371 assertSomeBigDecimal(BigDecimal.fromString("2"), BigDecimal.make(2n, 0))
372 assertSomeBigDecimal(BigDecimal.fromString("-2"), BigDecimal.make(-2n, 0))
373 assertSomeBigDecimal(BigDecimal.fromString("0.123"), BigDecimal.make(123n, 3))
374 assertSomeBigDecimal(BigDecimal.fromString("200"), BigDecimal.make(200n, 0))
375 assertSomeBigDecimal(BigDecimal.fromString("20000000"), BigDecimal.make(20000000n, 0))
376 assertSomeBigDecimal(BigDecimal.fromString("-20000000"), BigDecimal.make(-20000000n, 0))
377 assertSomeBigDecimal(BigDecimal.fromString("2.00"), BigDecimal.make(200n, 2))
378 assertSomeBigDecimal(BigDecimal.fromString("0.0000200"), BigDecimal.make(200n, 7))
379 assertSomeBigDecimal(BigDecimal.fromString(""), BigDecimal.normalize(BigDecimal.make(0n, 0)))
380 assertSomeBigDecimal(BigDecimal.fromString("1e5"), BigDecimal.make(1n, -5))
381 assertSomeBigDecimal(BigDecimal.fromString("1E15"), BigDecimal.make(1n, -15))
382 assertSomeBigDecimal(BigDecimal.fromString("1e+5"), BigDecimal.make(1n, -5))
383 assertSomeBigDecimal(BigDecimal.fromString("1E+15"), BigDecimal.make(1n, -15))
384 assertSomeBigDecimal(BigDecimal.fromString("-1.5E3"), BigDecimal.make(-15n, -2))
385 assertSomeBigDecimal(BigDecimal.fromString("-1.5e3"), BigDecimal.make(-15n, -2))
386 assertSomeBigDecimal(BigDecimal.fromString("-.5e3"), BigDecimal.make(-5n, -2))
387 assertSomeBigDecimal(BigDecimal.fromString("-5e3"), BigDecimal.make(-5n, -3))
388 assertSomeBigDecimal(BigDecimal.fromString("-5e-3"), BigDecimal.make(-5n, 3))
389 assertSomeBigDecimal(BigDecimal.fromString("15e-3"), BigDecimal.make(15n, 3))
390 assertSomeBigDecimal(BigDecimal.fromString("0.00002e5"), BigDecimal.make(2n, 0))
391 assertSomeBigDecimal(BigDecimal.fromString("0.00002e-5"), BigDecimal.make(2n, 10))
392 assertNone(BigDecimal.fromString("0.0000e2e1"))
393 assertNone(BigDecimal.fromString("0.1.2"))
394 })
395
396 it("format", () => {
397 strictEqual(BigDecimal.format($("0")), "0")
398 strictEqual(BigDecimal.format($("2")), "2")
399 strictEqual(BigDecimal.format($("-2")), "-2")
400 strictEqual(BigDecimal.format($("0.123")), "0.123")
401 strictEqual(BigDecimal.format($("200")), "200")
402 strictEqual(BigDecimal.format($("20000000")), "20000000")
403 strictEqual(BigDecimal.format($("-20000000")), "-20000000")
404 strictEqual(BigDecimal.format($("2.00")), "2")
405 strictEqual(BigDecimal.format($("0.200")), "0.2")
406 strictEqual(BigDecimal.format($("0.123000")), "0.123")
407 strictEqual(BigDecimal.format($("-456.123")), "-456.123")
408 strictEqual(BigDecimal.format(BigDecimal.make(10n, -1)), "100")
409 strictEqual(BigDecimal.format(BigDecimal.make(1n, -25)), "1e+25")
410 strictEqual(BigDecimal.format(BigDecimal.make(12345n, -25)), "1.2345e+29")
411 strictEqual(BigDecimal.format(BigDecimal.make(12345n, 25)), "1.2345e-21")
412 strictEqual(BigDecimal.format(BigDecimal.make(-12345n, 20)), "-1.2345e-16")
413 })
414
415 it("format preserves the boundary between plain and scientific notation", () => {
416 strictEqual(BigDecimal.format(BigDecimal.make(1n, 15)), "0.000000000000001")
417 strictEqual(BigDecimal.format(BigDecimal.make(-1n, 16)), "-1e-16")
418 strictEqual(BigDecimal.format(BigDecimal.make(-1n, -15)), "-1000000000000000")
419 strictEqual(BigDecimal.format(BigDecimal.make(1n, -16)), "1e+16")
420 })
421
422 it("toExponential", () => {
423 strictEqual(BigDecimal.toExponential(BigDecimal.make(0n, 42)), "0e+0")
424 strictEqual(BigDecimal.toExponential($("-1")), "-1e+0")
425 strictEqual(BigDecimal.toExponential($("123.4500")), "1.2345e+2")
426 strictEqual(BigDecimal.toExponential($("-0.0012345")), "-1.2345e-3")
427 })
428
429 it("toJSON()", () => {
430 deepStrictEqual(JSON.stringify($("2")), JSON.stringify({ _id: "BigDecimal", value: "2", scale: 0 }))
431 })
432
433 it("inspect", () => {
434 if (typeof window === "undefined") {
435 // oxlint-disable-next-line @typescript-eslint/no-require-imports
436 const { inspect } = require("node:util")
437 deepStrictEqual(inspect($("2")), inspect({ _id: "BigDecimal", value: "2", scale: 0 }))
438 }
439 })
440
441 it("toString()", () => {
442 strictEqual(String($("2")), "BigDecimal(2)")
443 })
444
445 it("Equal.symbol", () => {
446 assertTrue(Equal.equals($("2"), $("2")))
447 assertTrue(Equal.equals($("2"), $("2.00")))
448 assertFalse(Equal.equals($("2"), $("2.01")))
449 assertFalse(Equal.equals($("2"), 2))
450 })
451
452 it("Hash.symbol preserves hashes across equivalent representations", () => {
453 for (
454 const [value, scale, normalizedValue, normalizedScale] of [
455 [0n, 42, 0n, 0],
456 [12345n, 0, 12345n, 0],
457 [12345000n, 7, 12345n, 4],
458 [-12345000n, 7, -12345n, 4]
459 ] as const
460 ) {
461 const decimal = BigDecimal.make(value, scale)
462 const normalized = BigDecimal.make(normalizedValue, normalizedScale)
463 strictEqual(Hash.hash(decimal), Hash.combine(Hash.hash(normalizedValue), Hash.number(normalizedScale)))
464 strictEqual(Hash.hash(decimal), Hash.hash(normalized))
465 }
466 })
467
468 it("pipe()", () => {
469 deepStrictEqual($("2").pipe(BigDecimal.multiply($("3"))), $("6"))
470 })
471
472 it("scale", () => {
473 deepStrictEqual(BigDecimal.scale($("3.0005"), 3), $("3.000"))
474 })
475
476 it("fromBigInt", () => {
477 deepStrictEqual(BigDecimal.fromBigInt(1n), BigDecimal.make(1n, 0))
478 })
479
480 it("fromNumber", () => {
481 assertSomeBigDecimal(BigDecimal.fromNumber(123), BigDecimal.make(123n, 0))
482 assertSomeBigDecimal(BigDecimal.fromNumber(123.456), BigDecimal.make(123456n, 3))
483 assertNone(BigDecimal.fromNumber(Infinity))
484 })
485
486 it("toNumberUnsafe", () => {
487 strictEqual(BigDecimal.toNumberUnsafe($("123.456")), 123.456)
488 })
489
490 it("isInteger", () => {
491 assertTrue(BigDecimal.isInteger($("0")))
492 assertTrue(BigDecimal.isInteger($("1")))
493 assertFalse(BigDecimal.isInteger($("1.1")))
494 })
495
496 it("isZero", () => {
497 assertTrue(BigDecimal.isZero($("0")))
498 assertFalse(BigDecimal.isZero($("1")))
499 })
500
501 it("isNegative", () => {
502 assertTrue(BigDecimal.isNegative($("-1")))
503 assertFalse(BigDecimal.isNegative($("0")))
504 assertFalse(BigDecimal.isNegative($("1")))
505 })
506
507 it("isPositive", () => {
508 assertFalse(BigDecimal.isPositive($("-1")))
509 assertFalse(BigDecimal.isPositive($("0")))
510 assertTrue(BigDecimal.isPositive($("1")))
511 })
512
513 it("digitAt", () => {
514 assertEquals(BigDecimal.digitAt($("12.34"), -2), 0n)
515 assertEquals(BigDecimal.digitAt($("12.34"), -1), 1n)
516 assertEquals(BigDecimal.digitAt($("12.34"), 0), 2n)
517 assertEquals(BigDecimal.digitAt($("12.34"), 1), 3n)
518 assertEquals(BigDecimal.digitAt($("12.34"), 2), 4n)
519 assertEquals(BigDecimal.digitAt($("12.34"), 3), 0n)
520 })
521
522 it("round: ceil", () => {
523 assertEquals(BigDecimal.ceil($("145"), -1), $("150"))
524 assertEquals(BigDecimal.ceil(-1)($("145")), $("150"))
525 assertEquals(BigDecimal.ceil($("-14.5")), $("-14"))
526
527 assertEquals(BigDecimal.round($("321.123"), { mode: "ceil", scale: -3 }), $("1000"))
528 assertEquals(BigDecimal.round($("145"), { mode: "ceil", scale: -1 }), $("150"))
529 assertEquals(BigDecimal.round($("-2.0"), { mode: "ceil", scale: 0 }), $("-2"))
530 assertEquals(BigDecimal.round($("-1.9"), { mode: "ceil", scale: 0 }), $("-1"))
531 assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "ceil", scale: 13 }), $("0.1234567898766"))
532 assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "ceil", scale: 13 }), $("-0.1234567898765"))
533 })
534
535 it("round: floor", () => {
536 assertEquals(BigDecimal.floor($("145"), -1), $("140"))
537 assertEquals(BigDecimal.floor(-1)($("145")), $("140"))
538 assertEquals(BigDecimal.floor($("-14.5")), $("-15"))
539
540 assertEquals(BigDecimal.round($("321.123"), { mode: "floor", scale: -3 }), $("0"))
541 assertEquals(BigDecimal.round($("145"), { mode: "floor", scale: -1 }), $("140"))
542 assertEquals(BigDecimal.round($("-2.1"), { mode: "floor", scale: 0 }), $("-3"))
543 assertEquals(BigDecimal.round($("-1.9"), { mode: "floor", scale: 0 }), $("-2"))
544 assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "floor", scale: 13 }), $("0.1234567898765"))
545 assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "floor", scale: 13 }), $("-0.1234567898766"))
546 })
547
548 it("round: to-zero (truncate)", () => {
549 assertEquals(BigDecimal.truncate($("145"), -1), $("140"))
550 assertEquals(BigDecimal.truncate(-1)($("145")), $("140"))
551 assertEquals(BigDecimal.truncate($("-14.5")), $("-14"))
552
553 assertEquals(BigDecimal.round($("321.123"), { mode: "to-zero", scale: -3 }), $("0"))
554 assertEquals(BigDecimal.round($("145"), { mode: "to-zero", scale: -1 }), $("140"))
555 assertEquals(BigDecimal.round($("-2.1"), { mode: "to-zero", scale: 0 }), $("-2"))
556 assertEquals(BigDecimal.round($("-1.9"), { mode: "to-zero", scale: 0 }), $("-1"))
557 assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "to-zero", scale: 13 }), $("0.1234567898765"))
558 assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "to-zero", scale: 13 }), $("-0.1234567898765"))
559 })
560
561 it("round: from-zero", () => {
562 assertEquals(BigDecimal.round($("321.123"), { mode: "from-zero", scale: -3 }), $("1000"))
563 assertEquals(BigDecimal.round($("145"), { mode: "from-zero", scale: -1 }), $("150"))
564 assertEquals(BigDecimal.round($("-2.1"), { mode: "from-zero", scale: 0 }), $("-3"))
565 assertEquals(BigDecimal.round($("-1.9"), { mode: "from-zero", scale: 0 }), $("-2"))
566 assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "from-zero", scale: 13 }), $("0.1234567898766"))
567 assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "from-zero", scale: 13 }), $("-0.1234567898766"))
568 })
569
570 it("round: half-ceil", () => {
571 assertEquals(BigDecimal.round($("321.123"), { mode: "half-ceil", scale: -3 }), $("0"))
572 assertEquals(BigDecimal.round($("145"), { mode: "half-ceil", scale: -1 }), $("150"))
573 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-ceil", scale: 0 }), $("-2"))
574 assertEquals(BigDecimal.round($("1.95"), { mode: "half-ceil", scale: 1 }), $("2"))
575 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-ceil", scale: 1 }), $("-1.9"))
576 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-ceil", scale: 12 }), $("0.123456789877"))
577 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-ceil", scale: 12 }), $("-0.123456789876"))
578 })
579
580 it("round: half-floor", () => {
581 assertEquals(BigDecimal.round($("321.123"), { mode: "half-floor", scale: -3 }), $("0"))
582 assertEquals(BigDecimal.round($("145"), { mode: "half-floor", scale: -1 }), $("140"))
583 assertEquals(BigDecimal.round($("-2.4"), { mode: "half-floor", scale: 0 }), $("-2"))
584 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-floor", scale: 0 }), $("-3"))
585 assertEquals(BigDecimal.round($("1.95"), { mode: "half-floor", scale: 1 }), $("1.9"))
586 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-floor", scale: 1 }), $("-2"))
587 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-floor", scale: 12 }), $("0.123456789876"))
588 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-floor", scale: 12 }), $("-0.123456789877"))
589 })
590
591 it("round: half-to-zero", () => {
592 assertEquals(BigDecimal.round($("321.123"), { mode: "half-to-zero", scale: -3 }), $("0"))
593 assertEquals(BigDecimal.round($("145"), { mode: "half-to-zero", scale: -1 }), $("140"))
594 assertEquals(BigDecimal.round($("-2.4"), { mode: "half-to-zero", scale: 0 }), $("-2"))
595 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-to-zero", scale: 0 }), $("-2"))
596 assertEquals(BigDecimal.round($("1.95"), { mode: "half-to-zero", scale: 1 }), $("1.9"))
597 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-to-zero", scale: 1 }), $("-1.9"))
598 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-to-zero", scale: 12 }), $("0.123456789876"))
599 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-to-zero", scale: 12 }), $("-0.123456789876"))
600 })
601
602 it("round: half-from-zero", () => {
603 assertEquals(BigDecimal.round($("321.123"), { mode: "half-from-zero", scale: -3 }), $("0"))
604 assertEquals(BigDecimal.round($("145"), { mode: "half-from-zero", scale: -1 }), $("150"))
605 assertEquals(BigDecimal.round($("-2.4"), { mode: "half-from-zero", scale: 0 }), $("-2"))
606 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-from-zero", scale: 0 }), $("-3"))
607 assertEquals(BigDecimal.round($("1.95"), { mode: "half-from-zero", scale: 1 }), $("2"))
608 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-from-zero", scale: 1 }), $("-2"))
609 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-from-zero", scale: 12 }), $("0.123456789877"))
610 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-from-zero", scale: 12 }), $("-0.123456789877"))
611 })
612
613 it("round: half-even", () => {
614 assertEquals(BigDecimal.round($("321.123"), { mode: "half-even", scale: -3 }), $("0"))
615 assertEquals(BigDecimal.round($("145"), { mode: "half-even", scale: -1 }), $("140"))
616 assertEquals(BigDecimal.round($("-2.4"), { mode: "half-even", scale: 0 }), $("-2"))
617 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-even", scale: 0 }), $("-2"))
618 assertEquals(BigDecimal.round($("1.95"), { mode: "half-even", scale: 1 }), $("2"))
619 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-even", scale: 1 }), $("-2"))
620 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-even", scale: 12 }), $("0.123456789876"))
621 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-even", scale: 12 }), $("-0.123456789876"))
622 })
623
624 it("round: half-odd", () => {
625 assertEquals(BigDecimal.round($("321.123"), { mode: "half-even", scale: -3 }), $("0"))
626 assertEquals(BigDecimal.round($("145"), { mode: "half-even", scale: -1 }), $("140"))
627 assertEquals(BigDecimal.round($("-2.4"), { mode: "half-even", scale: 0 }), $("-2"))
628 assertEquals(BigDecimal.round($("-2.5"), { mode: "half-even", scale: 0 }), $("-2"))
629 assertEquals(BigDecimal.round($("1.95"), { mode: "half-even", scale: 1 }), $("2"))
630 assertEquals(BigDecimal.round($("-1.95"), { mode: "half-even", scale: 1 }), $("-2"))
631 assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-even", scale: 12 }), $("0.123456789876"))
632 assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-even", scale: 12 }), $("-0.123456789876"))
633 })
634})
635
636// This test is skipped because it is slow. It remains here as an opt-in test for
637// debugging or active development of features in the `BigDecimal` module.
638describe.skip("Property based testing", () => {
639 const zeroArb = fc.constant(BigDecimal.makeNormalizedUnsafe(0n, 0))
640 const bigDecimalArb = fc.tuple(fc.bigInt(), fc.integer()).map(([value, scale]) => BigDecimal.make(value, scale))
641 const arbWithZero = fc.oneof({ arbitrary: zeroArb, weight: 1 }, { arbitrary: bigDecimalArb, weight: 3 })
642
643 it("unsafeFromString and format should be inverses", () => {
644 fc.assert(fc.property(arbWithZero, (bd) => {
645 return BigDecimal.equals(BigDecimal.fromStringUnsafe(BigDecimal.format(bd)), bd)
646 }))
647 })
648
649 it("toExponential should harmonize with Number.prototype.toExponential", () => {
650 const actualNumbers = fc.float().filter((n) => Number.isFinite(n))
651 fc.assert(fc.property(actualNumbers, (n) => {
652 return n.toExponential() === BigDecimal.toExponential(BigDecimal.fromNumberUnsafe(n))
653 }))
654 })
655})