bigdecimal-string 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,574 @@
1
+ // src/index.ts
2
+ var RoundingMode = /* @__PURE__ */ ((RoundingMode2) => {
3
+ RoundingMode2["CEILING"] = "CEILING";
4
+ RoundingMode2["FLOOR"] = "FLOOR";
5
+ RoundingMode2["DOWN"] = "DOWN";
6
+ RoundingMode2["UP"] = "UP";
7
+ RoundingMode2["HALF_EVEN"] = "HALF_EVEN";
8
+ RoundingMode2["HALF_UP"] = "HALF_UP";
9
+ RoundingMode2["HALF_DOWN"] = "HALF_DOWN";
10
+ return RoundingMode2;
11
+ })(RoundingMode || {});
12
+ var DEFAULT_PRECISION = 2;
13
+ var DEFAULT_ROUNDING_MODE = "HALF_UP" /* HALF_UP */;
14
+ var BigDecimal = class _BigDecimal {
15
+ /**
16
+ * Creates a new BigDecimal instance
17
+ * @param value - The value to create from (string, number, bigint, or another BigDecimal)
18
+ * @param precision - Number of decimal places (default: auto-detected or 2)
19
+ */
20
+ constructor(value, precision) {
21
+ if (value === null || value === void 0 || value === "") {
22
+ this.unscaledValue = 0n;
23
+ this.scale = precision ?? DEFAULT_PRECISION;
24
+ return;
25
+ }
26
+ if (value instanceof _BigDecimal) {
27
+ if (precision !== void 0 && precision !== value.scale) {
28
+ const adjusted = value.setScale(precision);
29
+ this.unscaledValue = adjusted.unscaledValue;
30
+ this.scale = adjusted.scale;
31
+ } else {
32
+ this.unscaledValue = value.unscaledValue;
33
+ this.scale = value.scale;
34
+ }
35
+ return;
36
+ }
37
+ const parsed = _BigDecimal.parse(value, precision);
38
+ this.unscaledValue = parsed.unscaledValue;
39
+ this.scale = parsed.scale;
40
+ }
41
+ /**
42
+ * Parse a value into unscaled bigint and scale
43
+ */
44
+ static parse(value, precision) {
45
+ if (typeof value === "bigint") {
46
+ const scale = precision ?? DEFAULT_PRECISION;
47
+ return {
48
+ unscaledValue: value * _BigDecimal.powerOf10(scale),
49
+ scale
50
+ };
51
+ }
52
+ let str = typeof value === "number" ? value.toString() : value;
53
+ if (/[eE]/.test(str)) {
54
+ str = _BigDecimal.scientificToPlain(str);
55
+ }
56
+ const isNegative = str.startsWith("-");
57
+ if (isNegative || str.startsWith("+")) {
58
+ str = str.slice(1);
59
+ }
60
+ const [intPart, decPart = ""] = str.split(/[.,]/);
61
+ const detectedScale = decPart.length;
62
+ const targetScale = precision ?? Math.max(detectedScale, DEFAULT_PRECISION);
63
+ let cleanIntPart = intPart.replace(/^0+/, "") || "0";
64
+ let adjustedDecPart;
65
+ if (decPart.length < targetScale) {
66
+ adjustedDecPart = decPart.padEnd(targetScale, "0");
67
+ } else if (decPart.length > targetScale) {
68
+ adjustedDecPart = decPart.slice(0, targetScale);
69
+ const nextDigit = parseInt(decPart[targetScale] || "0", 10);
70
+ if (nextDigit >= 5) {
71
+ if (targetScale === 0) {
72
+ cleanIntPart = (BigInt(cleanIntPart) + 1n).toString();
73
+ } else {
74
+ const rounded = BigInt(adjustedDecPart || "0") + 1n;
75
+ const roundedStr = rounded.toString();
76
+ if (roundedStr.length > targetScale) {
77
+ cleanIntPart = (BigInt(cleanIntPart) + 1n).toString();
78
+ adjustedDecPart = "0".repeat(targetScale);
79
+ } else {
80
+ adjustedDecPart = roundedStr.padStart(targetScale, "0");
81
+ }
82
+ }
83
+ }
84
+ } else {
85
+ adjustedDecPart = decPart;
86
+ }
87
+ const combined = targetScale > 0 ? cleanIntPart + adjustedDecPart : cleanIntPart;
88
+ let unscaledValue = BigInt(combined);
89
+ if (isNegative) {
90
+ unscaledValue = -unscaledValue;
91
+ }
92
+ return { unscaledValue, scale: targetScale };
93
+ }
94
+ /**
95
+ * Convert scientific notation to plain decimal string
96
+ */
97
+ static scientificToPlain(sci) {
98
+ if (!/[eE]/.test(sci)) return sci;
99
+ const [coefficient, expPart] = sci.toLowerCase().split("e");
100
+ const exponent = parseInt(expPart, 10);
101
+ const isNegative = coefficient.startsWith("-");
102
+ const cleanCoef = coefficient.replace(/^[-+]/, "");
103
+ const [intPart, fracPart = ""] = cleanCoef.split(".");
104
+ const digits = intPart + fracPart;
105
+ const sign = isNegative ? "-" : "";
106
+ if (exponent >= 0) {
107
+ const totalIntDigits = intPart.length + exponent;
108
+ if (totalIntDigits >= digits.length) {
109
+ return sign + digits + "0".repeat(totalIntDigits - digits.length);
110
+ }
111
+ return sign + digits.slice(0, totalIntDigits) + "." + digits.slice(totalIntDigits);
112
+ }
113
+ const zerosNeeded = Math.abs(exponent) - intPart.length;
114
+ if (zerosNeeded >= 0) {
115
+ return sign + "0." + "0".repeat(zerosNeeded) + digits;
116
+ }
117
+ const splitPoint = intPart.length + exponent;
118
+ return sign + digits.slice(0, splitPoint) + "." + digits.slice(splitPoint);
119
+ }
120
+ /**
121
+ * Calculate 10^n as bigint
122
+ */
123
+ static powerOf10(n) {
124
+ if (n < 0) throw new Error("Power must be non-negative");
125
+ return 10n ** BigInt(n);
126
+ }
127
+ // ============================================
128
+ // ARITHMETIC OPERATIONS (Chainable)
129
+ // ============================================
130
+ /**
131
+ * Add another value to this BigDecimal
132
+ * @returns A new BigDecimal with the result
133
+ */
134
+ add(other) {
135
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
136
+ const [a, b] = _BigDecimal.alignScales(this, otherBd);
137
+ return _BigDecimal.fromUnscaled(a.unscaledValue + b.unscaledValue, a.scale);
138
+ }
139
+ /**
140
+ * Subtract another value from this BigDecimal
141
+ * @returns A new BigDecimal with the result
142
+ */
143
+ subtract(other) {
144
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
145
+ const [a, b] = _BigDecimal.alignScales(this, otherBd);
146
+ return _BigDecimal.fromUnscaled(a.unscaledValue - b.unscaledValue, a.scale);
147
+ }
148
+ /**
149
+ * Alias for subtract
150
+ */
151
+ minus(other) {
152
+ return this.subtract(other);
153
+ }
154
+ /**
155
+ * Alias for add
156
+ */
157
+ plus(other) {
158
+ return this.add(other);
159
+ }
160
+ /**
161
+ * Multiply this BigDecimal by another value
162
+ * @returns A new BigDecimal with the result
163
+ */
164
+ multiply(other) {
165
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
166
+ const rawResult = this.unscaledValue * otherBd.unscaledValue;
167
+ const rawScale = this.scale + otherBd.scale;
168
+ const targetScale = Math.max(this.scale, otherBd.scale);
169
+ const scaleDiff = rawScale - targetScale;
170
+ const divisor = _BigDecimal.powerOf10(scaleDiff);
171
+ const rounded = _BigDecimal.roundDivision(rawResult, divisor, DEFAULT_ROUNDING_MODE);
172
+ return _BigDecimal.fromUnscaled(rounded, targetScale);
173
+ }
174
+ /**
175
+ * Alias for multiply
176
+ */
177
+ times(other) {
178
+ return this.multiply(other);
179
+ }
180
+ /**
181
+ * Divide this BigDecimal by another value
182
+ * @param other - The divisor
183
+ * @param precision - Precision for the result (default: this.scale)
184
+ * @param roundingMode - How to round (default: HALF_UP)
185
+ * @returns A new BigDecimal with the result
186
+ */
187
+ divide(other, precision, roundingMode = DEFAULT_ROUNDING_MODE) {
188
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
189
+ if (otherBd.unscaledValue === 0n) {
190
+ throw new Error("Division by zero");
191
+ }
192
+ const targetScale = precision ?? this.scale;
193
+ const scaledDividend = this.unscaledValue * _BigDecimal.powerOf10(otherBd.scale + targetScale);
194
+ const divisor = otherBd.unscaledValue * _BigDecimal.powerOf10(this.scale);
195
+ const result = _BigDecimal.roundDivision(scaledDividend, divisor, roundingMode);
196
+ return _BigDecimal.fromUnscaled(result, targetScale);
197
+ }
198
+ /**
199
+ * Alias for divide
200
+ */
201
+ dividedBy(other, precision, roundingMode) {
202
+ return this.divide(other, precision, roundingMode);
203
+ }
204
+ /**
205
+ * Get the remainder of division
206
+ */
207
+ mod(other) {
208
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
209
+ const [a, b] = _BigDecimal.alignScales(this, otherBd);
210
+ if (b.unscaledValue === 0n) {
211
+ throw new Error("Division by zero");
212
+ }
213
+ const remainder = a.unscaledValue % b.unscaledValue;
214
+ return _BigDecimal.fromUnscaled(remainder, a.scale);
215
+ }
216
+ // ============================================
217
+ // COMPARISON OPERATIONS
218
+ // ============================================
219
+ /**
220
+ * Compare this BigDecimal to another
221
+ * @returns -1 if this < other, 0 if equal, 1 if this > other
222
+ */
223
+ compareTo(other) {
224
+ const otherBd = other instanceof _BigDecimal ? other : new _BigDecimal(other, this.scale);
225
+ const [a, b] = _BigDecimal.alignScales(this, otherBd);
226
+ if (a.unscaledValue < b.unscaledValue) return -1;
227
+ if (a.unscaledValue > b.unscaledValue) return 1;
228
+ return 0;
229
+ }
230
+ /**
231
+ * Check if this BigDecimal equals another value
232
+ */
233
+ equals(other) {
234
+ return this.compareTo(other) === 0;
235
+ }
236
+ /**
237
+ * Alias for equals
238
+ */
239
+ eq(other) {
240
+ return this.equals(other);
241
+ }
242
+ /**
243
+ * Check if this BigDecimal is less than another
244
+ */
245
+ lessThan(other) {
246
+ return this.compareTo(other) < 0;
247
+ }
248
+ /**
249
+ * Alias for lessThan
250
+ */
251
+ lt(other) {
252
+ return this.lessThan(other);
253
+ }
254
+ /**
255
+ * Check if this BigDecimal is less than or equal to another
256
+ */
257
+ lessThanOrEqual(other) {
258
+ return this.compareTo(other) <= 0;
259
+ }
260
+ /**
261
+ * Alias for lessThanOrEqual
262
+ */
263
+ lte(other) {
264
+ return this.lessThanOrEqual(other);
265
+ }
266
+ /**
267
+ * Check if this BigDecimal is greater than another
268
+ */
269
+ greaterThan(other) {
270
+ return this.compareTo(other) > 0;
271
+ }
272
+ /**
273
+ * Alias for greaterThan
274
+ */
275
+ gt(other) {
276
+ return this.greaterThan(other);
277
+ }
278
+ /**
279
+ * Check if this BigDecimal is greater than or equal to another
280
+ */
281
+ greaterThanOrEqual(other) {
282
+ return this.compareTo(other) >= 0;
283
+ }
284
+ /**
285
+ * Alias for greaterThanOrEqual
286
+ */
287
+ gte(other) {
288
+ return this.greaterThanOrEqual(other);
289
+ }
290
+ // ============================================
291
+ // UTILITY METHODS
292
+ // ============================================
293
+ /**
294
+ * Check if this BigDecimal is zero
295
+ */
296
+ isZero() {
297
+ return this.unscaledValue === 0n;
298
+ }
299
+ /**
300
+ * Check if this BigDecimal is positive (> 0)
301
+ */
302
+ isPositive() {
303
+ return this.unscaledValue > 0n;
304
+ }
305
+ /**
306
+ * Check if this BigDecimal is negative (< 0)
307
+ */
308
+ isNegative() {
309
+ return this.unscaledValue < 0n;
310
+ }
311
+ /**
312
+ * Get the absolute value
313
+ * @returns A new BigDecimal with the absolute value
314
+ */
315
+ abs() {
316
+ if (this.unscaledValue >= 0n) {
317
+ return this;
318
+ }
319
+ return _BigDecimal.fromUnscaled(-this.unscaledValue, this.scale);
320
+ }
321
+ /**
322
+ * Negate this BigDecimal
323
+ * @returns A new BigDecimal with the opposite sign
324
+ */
325
+ negate() {
326
+ return _BigDecimal.fromUnscaled(-this.unscaledValue, this.scale);
327
+ }
328
+ /**
329
+ * Get the sign of this BigDecimal
330
+ * @returns -1, 0, or 1
331
+ */
332
+ sign() {
333
+ if (this.unscaledValue < 0n) return -1;
334
+ if (this.unscaledValue > 0n) return 1;
335
+ return 0;
336
+ }
337
+ /**
338
+ * Change the scale (number of decimal places)
339
+ */
340
+ setScale(newScale, roundingMode = DEFAULT_ROUNDING_MODE) {
341
+ if (newScale === this.scale) {
342
+ return this;
343
+ }
344
+ if (newScale > this.scale) {
345
+ const multiplier = _BigDecimal.powerOf10(newScale - this.scale);
346
+ return _BigDecimal.fromUnscaled(this.unscaledValue * multiplier, newScale);
347
+ }
348
+ const divisor = _BigDecimal.powerOf10(this.scale - newScale);
349
+ const rounded = _BigDecimal.roundDivision(this.unscaledValue, divisor, roundingMode);
350
+ return _BigDecimal.fromUnscaled(rounded, newScale);
351
+ }
352
+ /**
353
+ * Get the precision (number of decimal places)
354
+ */
355
+ getPrecision() {
356
+ return this.scale;
357
+ }
358
+ // ============================================
359
+ // CONVERSION METHODS
360
+ // ============================================
361
+ /**
362
+ * Convert to string representation
363
+ * @param options - Formatting options
364
+ * @param options.prettify - Add thousand separators (commas)
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * bd("1234567.89").toString() // "1234567.89"
369
+ * bd("1234567.89").toString({ prettify: true }) // "1,234,567.89"
370
+ * bd("1e15").toString() // "1000000000000000.00"
371
+ * bd("1e15").toString({ prettify: true }) // "1,000,000,000,000,000.00"
372
+ * ```
373
+ */
374
+ toString(options) {
375
+ const isNegative = this.unscaledValue < 0n;
376
+ const absValue = isNegative ? -this.unscaledValue : this.unscaledValue;
377
+ const sign = isNegative ? "-" : "";
378
+ let intPart;
379
+ let decPart;
380
+ if (this.scale === 0) {
381
+ intPart = absValue.toString();
382
+ decPart = "";
383
+ } else {
384
+ const str = absValue.toString().padStart(this.scale + 1, "0");
385
+ intPart = str.slice(0, -this.scale) || "0";
386
+ decPart = str.slice(-this.scale);
387
+ }
388
+ if (options?.prettify) {
389
+ intPart = _BigDecimal.addThousandSeparators(intPart);
390
+ }
391
+ if (this.scale === 0 || !decPart) {
392
+ return `${sign}${intPart}`;
393
+ }
394
+ return `${sign}${intPart}.${decPart}`;
395
+ }
396
+ /**
397
+ * Format as a display string with thousand separators
398
+ * Shorthand for toString({ prettify: true })
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * bd("1234567.89").toFormat() // "1,234,567.89"
403
+ * bd("1e12").toFormat() // "1,000,000,000,000.00"
404
+ * ```
405
+ */
406
+ toFormat() {
407
+ return this.toString({ prettify: true });
408
+ }
409
+ /**
410
+ * Add thousand separators to an integer string
411
+ */
412
+ static addThousandSeparators(intPart) {
413
+ return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
414
+ }
415
+ /**
416
+ * Convert to number (may lose precision for large values)
417
+ * @warning Use with caution - JavaScript numbers have limited precision
418
+ */
419
+ toNumber() {
420
+ return parseFloat(this.toString());
421
+ }
422
+ /**
423
+ * Format with fixed decimal places
424
+ * @param decimals - Number of decimal places
425
+ * @param options - Formatting options
426
+ */
427
+ toFixed(decimals, options) {
428
+ return this.setScale(decimals).toString(options);
429
+ }
430
+ /**
431
+ * Get the integer part only
432
+ */
433
+ toInteger() {
434
+ const divisor = _BigDecimal.powerOf10(this.scale);
435
+ return this.unscaledValue / divisor;
436
+ }
437
+ /**
438
+ * valueOf for implicit conversions
439
+ */
440
+ valueOf() {
441
+ return this.toNumber();
442
+ }
443
+ // ============================================
444
+ // STATIC FACTORY METHODS
445
+ // ============================================
446
+ /**
447
+ * Create from unscaled value and scale
448
+ */
449
+ static fromUnscaled(unscaledValue, scale) {
450
+ const bd2 = Object.create(_BigDecimal.prototype);
451
+ bd2.unscaledValue = unscaledValue;
452
+ bd2.scale = scale;
453
+ return bd2;
454
+ }
455
+ /**
456
+ * Create a BigDecimal with value zero
457
+ */
458
+ static zero(precision = DEFAULT_PRECISION) {
459
+ return new _BigDecimal(0, precision);
460
+ }
461
+ /**
462
+ * Create a BigDecimal with value one
463
+ */
464
+ static one(precision = DEFAULT_PRECISION) {
465
+ return new _BigDecimal(1, precision);
466
+ }
467
+ /**
468
+ * Sum multiple values
469
+ */
470
+ static sum(...values) {
471
+ if (values.length === 0) {
472
+ return _BigDecimal.zero();
473
+ }
474
+ return values.reduce((acc, val) => {
475
+ return acc.add(val);
476
+ }, _BigDecimal.zero());
477
+ }
478
+ /**
479
+ * Get the maximum value
480
+ */
481
+ static max(...values) {
482
+ if (values.length === 0) {
483
+ throw new Error("max requires at least one value");
484
+ }
485
+ return values.reduce((max, val) => {
486
+ const bd2 = val instanceof _BigDecimal ? val : new _BigDecimal(val);
487
+ return bd2.gt(max) ? bd2 : max;
488
+ }, new _BigDecimal(values[0]));
489
+ }
490
+ /**
491
+ * Get the minimum value
492
+ */
493
+ static min(...values) {
494
+ if (values.length === 0) {
495
+ throw new Error("min requires at least one value");
496
+ }
497
+ return values.reduce((min, val) => {
498
+ const bd2 = val instanceof _BigDecimal ? val : new _BigDecimal(val);
499
+ return bd2.lt(min) ? bd2 : min;
500
+ }, new _BigDecimal(values[0]));
501
+ }
502
+ // ============================================
503
+ // INTERNAL HELPERS
504
+ // ============================================
505
+ /**
506
+ * Align two BigDecimals to the same scale
507
+ */
508
+ static alignScales(a, b) {
509
+ if (a.scale === b.scale) {
510
+ return [a, b];
511
+ }
512
+ const targetScale = Math.max(a.scale, b.scale);
513
+ return [a.setScale(targetScale), b.setScale(targetScale)];
514
+ }
515
+ /**
516
+ * Perform division with rounding
517
+ */
518
+ static roundDivision(dividend, divisor, mode) {
519
+ if (divisor === 0n) {
520
+ throw new Error("Division by zero");
521
+ }
522
+ const quotient = dividend / divisor;
523
+ const remainder = dividend % divisor;
524
+ if (remainder === 0n) {
525
+ return quotient;
526
+ }
527
+ const isNegative = dividend < 0n !== divisor < 0n;
528
+ const absRemainder = remainder < 0n ? -remainder : remainder;
529
+ const absDivisor = divisor < 0n ? -divisor : divisor;
530
+ const shouldRoundUp = (() => {
531
+ switch (mode) {
532
+ case "UP" /* UP */:
533
+ return true;
534
+ case "DOWN" /* DOWN */:
535
+ return false;
536
+ case "CEILING" /* CEILING */:
537
+ return !isNegative;
538
+ case "FLOOR" /* FLOOR */:
539
+ return isNegative;
540
+ case "HALF_UP" /* HALF_UP */: {
541
+ const doubled = absRemainder * 2n;
542
+ return doubled >= absDivisor;
543
+ }
544
+ case "HALF_DOWN" /* HALF_DOWN */: {
545
+ const doubled = absRemainder * 2n;
546
+ return doubled > absDivisor;
547
+ }
548
+ case "HALF_EVEN" /* HALF_EVEN */: {
549
+ const doubled = absRemainder * 2n;
550
+ if (doubled > absDivisor) return true;
551
+ if (doubled < absDivisor) return false;
552
+ const absQuotient = quotient < 0n ? -quotient : quotient;
553
+ return absQuotient % 2n !== 0n;
554
+ }
555
+ default:
556
+ return false;
557
+ }
558
+ })();
559
+ if (shouldRoundUp) {
560
+ return isNegative ? quotient - 1n : quotient + 1n;
561
+ }
562
+ return quotient;
563
+ }
564
+ };
565
+ function bd(value, precision) {
566
+ return new BigDecimal(value, precision);
567
+ }
568
+ var index_default = BigDecimal;
569
+ export {
570
+ BigDecimal,
571
+ RoundingMode,
572
+ bd,
573
+ index_default as default
574
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "bigdecimal-string",
3
+ "version": "1.0.0",
4
+ "description": "Precise decimal arithmetic for JavaScript using BigInt. Avoids floating-point errors with a chainable, immutable API.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "bigdecimal",
26
+ "decimal",
27
+ "precision",
28
+ "arithmetic",
29
+ "bigint",
30
+ "financial",
31
+ "money",
32
+ "currency",
33
+ "floating-point",
34
+ "math"
35
+ ],
36
+ "author": "ozJSey",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/ozJSey/big_decimal_string.git"
41
+ },
42
+ "homepage": "https://github.com/ozJSey/big_decimal_string#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/ozJSey/big_decimal_string/issues"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.0.0",
49
+ "vitest": "^2.0.0"
50
+ }
51
+ }