@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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "arbitrary",
7
7
  "bigint"
8
8
  ],
9
- "version": "0.1.0-alpha.1",
9
+ "version": "0.1.0-alpha.3",
10
10
  "type": "module",
11
11
  "main": "./src/index.ts",
12
12
  "author": "kikuchan <kikuchan98@gmail.com>",
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.digits <= 0n || this.coeff === 0n) return 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
- this.coeff = -this.coeff;
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
  }
@@ -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('rescale preserves explicit zero scales without stripping digits', () => {
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(5n);
1103
- expect(compressed.toString()).toBe('0.00000');
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