@kikuchan/decimal 0.1.0-alpha.1 → 0.1.0-alpha.3
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/package.json +1 -1
- package/src/index.ts +26 -8
- package/tests/decimal.spec.ts +101 -4
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -41,10 +41,12 @@ export interface Decimal {
|
|
|
41
41
|
splitBy(step: DecimalLike, mode?: RoundingMode): [Decimal, Decimal];
|
|
42
42
|
|
|
43
43
|
// Sign and absolute
|
|
44
|
-
neg$(): this;
|
|
45
|
-
neg(): Decimal;
|
|
44
|
+
neg$(flag?: boolean): this;
|
|
45
|
+
neg(flag?: boolean): Decimal;
|
|
46
46
|
abs$(): this;
|
|
47
47
|
abs(): Decimal;
|
|
48
|
+
sign$(): this;
|
|
49
|
+
sign(): Decimal;
|
|
48
50
|
isZero(): boolean;
|
|
49
51
|
isPositive(): boolean;
|
|
50
52
|
isNegative(): boolean;
|
|
@@ -405,7 +407,11 @@ class DecimalImpl implements Decimal {
|
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
#stripTrailingZeros$(): this {
|
|
408
|
-
if (this.
|
|
410
|
+
if (this.coeff === 0n) {
|
|
411
|
+
this.digits = 0n;
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
if (this.digits <= 0n) return this;
|
|
409
415
|
while (this.digits > 0n && this.coeff % 10n === 0n) {
|
|
410
416
|
this.coeff /= 10n;
|
|
411
417
|
this.digits -= 1n;
|
|
@@ -519,13 +525,15 @@ class DecimalImpl implements Decimal {
|
|
|
519
525
|
return this.clone().splitBy$(step, mode);
|
|
520
526
|
}
|
|
521
527
|
|
|
522
|
-
neg$(): this {
|
|
523
|
-
|
|
528
|
+
neg$(flag?: boolean): this {
|
|
529
|
+
if (flag !== false) {
|
|
530
|
+
this.coeff = -this.coeff;
|
|
531
|
+
}
|
|
524
532
|
return this;
|
|
525
533
|
}
|
|
526
534
|
|
|
527
|
-
neg(): DecimalImpl {
|
|
528
|
-
return this.clone().neg$();
|
|
535
|
+
neg(flag?: boolean): DecimalImpl {
|
|
536
|
+
return this.clone().neg$(flag);
|
|
529
537
|
}
|
|
530
538
|
|
|
531
539
|
isZero(): boolean {
|
|
@@ -864,6 +872,17 @@ class DecimalImpl implements Decimal {
|
|
|
864
872
|
return this.clone().log$(base, digits);
|
|
865
873
|
}
|
|
866
874
|
|
|
875
|
+
sign$() {
|
|
876
|
+
if (this.isZero()) return this;
|
|
877
|
+
this.coeff = this.coeff < 0n ? -1n : 1n;
|
|
878
|
+
this.digits = 0n;
|
|
879
|
+
return this;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
sign() {
|
|
883
|
+
return this.clone().sign$();
|
|
884
|
+
}
|
|
885
|
+
|
|
867
886
|
order(): bigint {
|
|
868
887
|
if (this.isZero()) throw new RangeError('order undefined for 0');
|
|
869
888
|
return BigInt(abs(this.coeff).toString().length) - 1n - this.digits;
|
|
@@ -986,7 +1005,6 @@ export namespace Decimal {
|
|
|
986
1005
|
return minmax(...values)[0];
|
|
987
1006
|
}
|
|
988
1007
|
|
|
989
|
-
/* c8 ignore next */
|
|
990
1008
|
export function max(...values: (DecimalLike | null | undefined)[]): Decimal | null {
|
|
991
1009
|
return minmax(...values)[1];
|
|
992
1010
|
}
|
package/tests/decimal.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { RoundingMode } from '../src/index.ts';
|
|
3
3
|
import { Decimal, isDecimal, max, min, minmax, pow10 } from '../src/index.ts';
|
|
4
4
|
|
|
@@ -32,6 +32,11 @@ describe('Decimal construction', () => {
|
|
|
32
32
|
expect(value.digits).toBe(5n);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it('returns existing decimal instances unchanged', () => {
|
|
36
|
+
const value = Decimal(42);
|
|
37
|
+
expect(Decimal(value)).toBe(value);
|
|
38
|
+
});
|
|
39
|
+
|
|
35
40
|
it('returns nullish inputs unchanged', () => {
|
|
36
41
|
expect(Decimal(null)).toBeNull();
|
|
37
42
|
expect(Decimal(undefined)).toBeUndefined();
|
|
@@ -261,6 +266,12 @@ describe('Decimal transforms', () => {
|
|
|
261
266
|
});
|
|
262
267
|
|
|
263
268
|
describe('Decimal type guards', () => {
|
|
269
|
+
it('detects decimals using isDecimal', () => {
|
|
270
|
+
const value = Decimal(7);
|
|
271
|
+
expect(Decimal.isDecimal(value)).toBe(true);
|
|
272
|
+
expect(Decimal.isDecimal(7)).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
264
275
|
it('recognizes supported literal inputs', () => {
|
|
265
276
|
expect(Decimal.isDecimalLike('42.00')).toBe(true);
|
|
266
277
|
expect(Decimal.isDecimalLike(42)).toBe(true);
|
|
@@ -367,6 +378,32 @@ describe('Decimal sign helpers', () => {
|
|
|
367
378
|
expect(negative.isPositive()).toBe(false);
|
|
368
379
|
expect(negative.isNegative()).toBe(true);
|
|
369
380
|
});
|
|
381
|
+
|
|
382
|
+
describe('sign', () => {
|
|
383
|
+
it('returns a 0', () => {
|
|
384
|
+
const value = Decimal(0);
|
|
385
|
+
expect(value.sign().toString()).toBe('0');
|
|
386
|
+
});
|
|
387
|
+
it('returns a -1', () => {
|
|
388
|
+
const value = Decimal(-1.987);
|
|
389
|
+
expect(value.sign().toString()).toBe('-1');
|
|
390
|
+
});
|
|
391
|
+
it('returns a 1', () => {
|
|
392
|
+
const value = Decimal(1.987);
|
|
393
|
+
expect(value.sign().toString()).toBe('1');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('neg', () => {
|
|
398
|
+
it('returns negated value', () => {
|
|
399
|
+
const value = Decimal(1.987);
|
|
400
|
+
expect(value.neg(true).toString()).toBe('-1.987');
|
|
401
|
+
});
|
|
402
|
+
it('returns original value', () => {
|
|
403
|
+
const value = Decimal(1.987);
|
|
404
|
+
expect(value.neg(false).toString()).toBe('1.987');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
370
407
|
});
|
|
371
408
|
|
|
372
409
|
describe('Decimal truncation', () => {
|
|
@@ -647,6 +684,14 @@ describe('Decimal roots', () => {
|
|
|
647
684
|
expect(root.toString()).toBe('-3.00000000');
|
|
648
685
|
});
|
|
649
686
|
|
|
687
|
+
it('uses floating approximations for moderate roots', () => {
|
|
688
|
+
const powSpy = vi.spyOn(Math, 'pow');
|
|
689
|
+
const root = Decimal(256).root(4n, 6n);
|
|
690
|
+
expect(powSpy).toHaveBeenCalled();
|
|
691
|
+
expect(root.eq(Decimal(4))).toBe(true);
|
|
692
|
+
powSpy.mockRestore();
|
|
693
|
+
});
|
|
694
|
+
|
|
650
695
|
it('throws on even roots of negative numbers', () => {
|
|
651
696
|
expect(() => Decimal(-16).root(2n, 8n)).toThrowError();
|
|
652
697
|
});
|
|
@@ -714,6 +759,46 @@ describe('Decimal roots', () => {
|
|
|
714
759
|
const tolerance = pow10(-(digits - 4n));
|
|
715
760
|
expect(recomposed.isCloseTo(value.rescale(digits), tolerance)).toBe(true);
|
|
716
761
|
});
|
|
762
|
+
|
|
763
|
+
it('falls back to order-based estimates when floating guesses overflow', () => {
|
|
764
|
+
const powSpy = vi.spyOn(Math, 'pow');
|
|
765
|
+
const huge = Decimal({ coeff: 1n, digits: -5000n });
|
|
766
|
+
const root = huge.root(2n, 4n);
|
|
767
|
+
expect(powSpy).not.toHaveBeenCalled();
|
|
768
|
+
expect(root.digits).toBe(4n);
|
|
769
|
+
powSpy.mockRestore();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('recovers when floating approximation yields a non-finite guess', () => {
|
|
773
|
+
const powSpy = vi.spyOn(Math, 'pow').mockReturnValueOnce(Number.NaN);
|
|
774
|
+
const root = Decimal(64).root(3n, 8n);
|
|
775
|
+
expect(powSpy).toHaveBeenCalled();
|
|
776
|
+
expect(root.eq(Decimal(4))).toBe(true);
|
|
777
|
+
powSpy.mockRestore();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('treats non-positive numeric degree hints as fallbacks while keeping bigint logic', () => {
|
|
781
|
+
const originalNumber = Number;
|
|
782
|
+
const targetDegree = 2n;
|
|
783
|
+
const mockNumber = function (value: unknown) {
|
|
784
|
+
if (value === targetDegree) return 0;
|
|
785
|
+
return originalNumber(value as never);
|
|
786
|
+
} as NumberConstructor;
|
|
787
|
+
for (const key of Object.getOwnPropertyNames(originalNumber)) {
|
|
788
|
+
const descriptor = Object.getOwnPropertyDescriptor(originalNumber, key);
|
|
789
|
+
if (descriptor) {
|
|
790
|
+
Object.defineProperty(mockNumber, key, descriptor);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
(globalThis as { Number: NumberConstructor }).Number = mockNumber;
|
|
795
|
+
try {
|
|
796
|
+
const root = Decimal(16).root(targetDegree, 6n);
|
|
797
|
+
expect(root.eq(Decimal(4))).toBe(true);
|
|
798
|
+
} finally {
|
|
799
|
+
(globalThis as { Number: NumberConstructor }).Number = originalNumber;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
717
802
|
});
|
|
718
803
|
|
|
719
804
|
describe('Decimal modular helpers', () => {
|
|
@@ -726,6 +811,11 @@ describe('Decimal modular helpers', () => {
|
|
|
726
811
|
expect(Decimal(-17).modPositive(5).toString()).toBe(Decimal(3).toString());
|
|
727
812
|
});
|
|
728
813
|
|
|
814
|
+
it('leaves positive remainders unchanged in modPositive', () => {
|
|
815
|
+
const result = Decimal(17).modPositive(5);
|
|
816
|
+
expect(result.toString()).toBe(Decimal(2).toString());
|
|
817
|
+
});
|
|
818
|
+
|
|
729
819
|
it('rejects negative divisors for positive modulo', () => {
|
|
730
820
|
expect(() => Decimal(1).modPositive(-5)).toThrow('Modulo divisor must be positive');
|
|
731
821
|
});
|
|
@@ -1096,11 +1186,18 @@ describe('Decimal boundaries', () => {
|
|
|
1096
1186
|
expect(clamped.eq(original)).toBe(true);
|
|
1097
1187
|
});
|
|
1098
1188
|
|
|
1099
|
-
it('
|
|
1189
|
+
it('compresses zero to standard form with rescale', () => {
|
|
1100
1190
|
const value = Decimal({ coeff: 0n, digits: 5n });
|
|
1101
1191
|
const compressed = value.rescale();
|
|
1102
|
-
expect(compressed.digits).toBe(
|
|
1103
|
-
expect(compressed.toString()).toBe('0
|
|
1192
|
+
expect(compressed.digits).toBe(0n);
|
|
1193
|
+
expect(compressed.toString()).toBe('0');
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it('compresses zero with negative exponent to standard form with rescale', () => {
|
|
1197
|
+
const value = Decimal({ coeff: 0n, digits: -5n });
|
|
1198
|
+
const compressed = value.rescale();
|
|
1199
|
+
expect(compressed.digits).toBe(0n);
|
|
1200
|
+
expect(compressed.toString()).toBe('0');
|
|
1104
1201
|
});
|
|
1105
1202
|
});
|
|
1106
1203
|
|