ts-data-forge 6.13.1 → 7.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.
Files changed (174) hide show
  1. package/README.md +2 -2
  2. package/dist/array/impl/array-utils-iterators.mjs +6 -0
  3. package/dist/array/impl/array-utils-iterators.mjs.map +1 -1
  4. package/dist/array/impl/array-utils-modification.mjs +6 -0
  5. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  6. package/dist/array/impl/array-utils-reducing-value.mjs +6 -0
  7. package/dist/array/impl/array-utils-reducing-value.mjs.map +1 -1
  8. package/dist/array/impl/array-utils-search.mjs +6 -0
  9. package/dist/array/impl/array-utils-search.mjs.map +1 -1
  10. package/dist/array/impl/array-utils-slice-clamped.mjs +6 -0
  11. package/dist/array/impl/array-utils-slice-clamped.mjs.map +1 -1
  12. package/dist/array/impl/array-utils-slicing.mjs +6 -0
  13. package/dist/array/impl/array-utils-slicing.mjs.map +1 -1
  14. package/dist/array/impl/array-utils-transformation.mjs +6 -0
  15. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  16. package/dist/array/impl/array-utils-validation.mjs +6 -0
  17. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  18. package/dist/collections/imap-mapped.mjs +6 -0
  19. package/dist/collections/imap-mapped.mjs.map +1 -1
  20. package/dist/collections/imap.mjs +6 -0
  21. package/dist/collections/imap.mjs.map +1 -1
  22. package/dist/collections/iset-mapped.mjs +6 -0
  23. package/dist/collections/iset-mapped.mjs.map +1 -1
  24. package/dist/collections/iset.mjs +6 -0
  25. package/dist/collections/iset.mjs.map +1 -1
  26. package/dist/collections/queue.mjs +6 -0
  27. package/dist/collections/queue.mjs.map +1 -1
  28. package/dist/collections/stack.mjs +6 -0
  29. package/dist/collections/stack.mjs.map +1 -1
  30. package/dist/entry-point.mjs +6 -0
  31. package/dist/entry-point.mjs.map +1 -1
  32. package/dist/index.mjs +6 -0
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/iterator/range.mjs +6 -0
  35. package/dist/iterator/range.mjs.map +1 -1
  36. package/dist/json/json.mjs +6 -0
  37. package/dist/json/json.mjs.map +1 -1
  38. package/dist/number/branded-types/finite-number.d.mts +1 -1
  39. package/dist/number/branded-types/index.d.mts +6 -0
  40. package/dist/number/branded-types/index.d.mts.map +1 -1
  41. package/dist/number/branded-types/index.mjs +6 -0
  42. package/dist/number/branded-types/index.mjs.map +1 -1
  43. package/dist/number/branded-types/int.d.mts +1 -2
  44. package/dist/number/branded-types/int.d.mts.map +1 -1
  45. package/dist/number/branded-types/int16.d.mts +2 -3
  46. package/dist/number/branded-types/int16.d.mts.map +1 -1
  47. package/dist/number/branded-types/int32.d.mts +2 -3
  48. package/dist/number/branded-types/int32.d.mts.map +1 -1
  49. package/dist/number/branded-types/negative-finite-number.d.mts +143 -0
  50. package/dist/number/branded-types/negative-finite-number.d.mts.map +1 -0
  51. package/dist/number/branded-types/negative-finite-number.mjs +156 -0
  52. package/dist/number/branded-types/negative-finite-number.mjs.map +1 -0
  53. package/dist/number/branded-types/negative-int.d.mts +120 -0
  54. package/dist/number/branded-types/negative-int.d.mts.map +1 -0
  55. package/dist/number/branded-types/negative-int.mjs +125 -0
  56. package/dist/number/branded-types/negative-int.mjs.map +1 -0
  57. package/dist/number/branded-types/negative-safe-int.d.mts +132 -0
  58. package/dist/number/branded-types/negative-safe-int.d.mts.map +1 -0
  59. package/dist/number/branded-types/negative-safe-int.mjs +136 -0
  60. package/dist/number/branded-types/negative-safe-int.mjs.map +1 -0
  61. package/dist/number/branded-types/non-negative-finite-number.d.mts +1 -1
  62. package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
  63. package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
  64. package/dist/number/branded-types/non-positive-finite-number.d.mts +130 -0
  65. package/dist/number/branded-types/non-positive-finite-number.d.mts.map +1 -0
  66. package/dist/number/branded-types/non-positive-finite-number.mjs +134 -0
  67. package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -0
  68. package/dist/number/branded-types/non-positive-int.d.mts +224 -0
  69. package/dist/number/branded-types/non-positive-int.d.mts.map +1 -0
  70. package/dist/number/branded-types/non-positive-int.mjs +222 -0
  71. package/dist/number/branded-types/non-positive-int.mjs.map +1 -0
  72. package/dist/number/branded-types/non-positive-safe-int.d.mts +119 -0
  73. package/dist/number/branded-types/non-positive-safe-int.d.mts.map +1 -0
  74. package/dist/number/branded-types/non-positive-safe-int.mjs +116 -0
  75. package/dist/number/branded-types/non-positive-safe-int.mjs.map +1 -0
  76. package/dist/number/branded-types/non-zero-finite-number.d.mts +5 -17
  77. package/dist/number/branded-types/non-zero-finite-number.d.mts.map +1 -1
  78. package/dist/number/branded-types/non-zero-finite-number.mjs +5 -17
  79. package/dist/number/branded-types/non-zero-finite-number.mjs.map +1 -1
  80. package/dist/number/branded-types/non-zero-int.d.mts +5 -33
  81. package/dist/number/branded-types/non-zero-int.d.mts.map +1 -1
  82. package/dist/number/branded-types/non-zero-int.mjs +5 -25
  83. package/dist/number/branded-types/non-zero-int.mjs.map +1 -1
  84. package/dist/number/branded-types/non-zero-int16.d.mts +6 -43
  85. package/dist/number/branded-types/non-zero-int16.d.mts.map +1 -1
  86. package/dist/number/branded-types/non-zero-int16.mjs +5 -25
  87. package/dist/number/branded-types/non-zero-int16.mjs.map +1 -1
  88. package/dist/number/branded-types/non-zero-int32.d.mts +6 -39
  89. package/dist/number/branded-types/non-zero-int32.d.mts.map +1 -1
  90. package/dist/number/branded-types/non-zero-int32.mjs +5 -25
  91. package/dist/number/branded-types/non-zero-int32.mjs.map +1 -1
  92. package/dist/number/branded-types/non-zero-safe-int.d.mts +5 -37
  93. package/dist/number/branded-types/non-zero-safe-int.d.mts.map +1 -1
  94. package/dist/number/branded-types/non-zero-safe-int.mjs +5 -28
  95. package/dist/number/branded-types/non-zero-safe-int.mjs.map +1 -1
  96. package/dist/number/branded-types/non-zero-uint16.d.mts +6 -33
  97. package/dist/number/branded-types/non-zero-uint16.d.mts.map +1 -1
  98. package/dist/number/branded-types/non-zero-uint16.mjs +5 -17
  99. package/dist/number/branded-types/non-zero-uint16.mjs.map +1 -1
  100. package/dist/number/branded-types/non-zero-uint32.d.mts +6 -31
  101. package/dist/number/branded-types/non-zero-uint32.d.mts.map +1 -1
  102. package/dist/number/branded-types/non-zero-uint32.mjs +5 -17
  103. package/dist/number/branded-types/non-zero-uint32.mjs.map +1 -1
  104. package/dist/number/branded-types/positive-finite-number.d.mts +4 -2
  105. package/dist/number/branded-types/positive-finite-number.d.mts.map +1 -1
  106. package/dist/number/branded-types/positive-int.d.mts +4 -2
  107. package/dist/number/branded-types/positive-int.d.mts.map +1 -1
  108. package/dist/number/branded-types/positive-int16.d.mts +4 -2
  109. package/dist/number/branded-types/positive-int16.d.mts.map +1 -1
  110. package/dist/number/branded-types/positive-int32.d.mts +4 -2
  111. package/dist/number/branded-types/positive-int32.d.mts.map +1 -1
  112. package/dist/number/branded-types/positive-safe-int.d.mts +4 -2
  113. package/dist/number/branded-types/positive-safe-int.d.mts.map +1 -1
  114. package/dist/number/branded-types/positive-uint16.d.mts +4 -2
  115. package/dist/number/branded-types/positive-uint16.d.mts.map +1 -1
  116. package/dist/number/branded-types/positive-uint32.d.mts +4 -2
  117. package/dist/number/branded-types/positive-uint32.d.mts.map +1 -1
  118. package/dist/number/branded-types/safe-int.d.mts +1 -2
  119. package/dist/number/branded-types/safe-int.d.mts.map +1 -1
  120. package/dist/number/branded-types/safe-uint.d.mts +1 -1
  121. package/dist/number/branded-types/uint.d.mts +1 -1
  122. package/dist/number/branded-types/uint16.d.mts +1 -1
  123. package/dist/number/branded-types/uint32.d.mts +1 -1
  124. package/dist/number/index.d.mts +1 -0
  125. package/dist/number/index.d.mts.map +1 -1
  126. package/dist/number/index.mjs +6 -0
  127. package/dist/number/index.mjs.map +1 -1
  128. package/dist/number/num-arithmetic-types.d.mts +74 -0
  129. package/dist/number/num-arithmetic-types.d.mts.map +1 -0
  130. package/dist/number/num-arithmetic-types.mjs +2 -0
  131. package/dist/number/num-arithmetic-types.mjs.map +1 -0
  132. package/dist/number/num.d.mts +53 -9
  133. package/dist/number/num.d.mts.map +1 -1
  134. package/dist/number/num.mjs +64 -12
  135. package/dist/number/num.mjs.map +1 -1
  136. package/dist/number/refined-number-utils.d.mts +12 -9
  137. package/dist/number/refined-number-utils.d.mts.map +1 -1
  138. package/dist/number/refined-number-utils.mjs.map +1 -1
  139. package/dist/object/object.mjs +6 -0
  140. package/dist/object/object.mjs.map +1 -1
  141. package/package.json +2 -2
  142. package/src/number/branded-types/index.mts +6 -0
  143. package/src/number/branded-types/negative-finite-number.mts +210 -0
  144. package/src/number/branded-types/negative-finite-number.test.mts +172 -0
  145. package/src/number/branded-types/negative-int.mts +160 -0
  146. package/src/number/branded-types/negative-int.test.mts +146 -0
  147. package/src/number/branded-types/negative-safe-int.mts +173 -0
  148. package/src/number/branded-types/negative-safe-int.test.mts +120 -0
  149. package/src/number/branded-types/non-positive-finite-number.mts +191 -0
  150. package/src/number/branded-types/non-positive-finite-number.test.mts +286 -0
  151. package/src/number/branded-types/non-positive-int.mts +251 -0
  152. package/src/number/branded-types/non-positive-int.test.mts +197 -0
  153. package/src/number/branded-types/non-positive-safe-int.mts +147 -0
  154. package/src/number/branded-types/non-positive-safe-int.test.mts +223 -0
  155. package/src/number/branded-types/non-zero-finite-number.mts +14 -22
  156. package/src/number/branded-types/non-zero-finite-number.test.mts +0 -12
  157. package/src/number/branded-types/non-zero-int.mts +10 -32
  158. package/src/number/branded-types/non-zero-int.test.mts +0 -20
  159. package/src/number/branded-types/non-zero-int16.mts +8 -32
  160. package/src/number/branded-types/non-zero-int16.test.mts +0 -30
  161. package/src/number/branded-types/non-zero-int32.mts +8 -32
  162. package/src/number/branded-types/non-zero-int32.test.mts +0 -30
  163. package/src/number/branded-types/non-zero-safe-int.mts +8 -35
  164. package/src/number/branded-types/non-zero-safe-int.test.mts +0 -32
  165. package/src/number/branded-types/non-zero-uint16.mts +8 -22
  166. package/src/number/branded-types/non-zero-uint16.test.mts +0 -16
  167. package/src/number/branded-types/non-zero-uint32.mts +8 -22
  168. package/src/number/branded-types/non-zero-uint32.test.mts +0 -16
  169. package/src/number/index.mts +1 -0
  170. package/src/number/num-arithmetic-types.mts +269 -0
  171. package/src/number/num-arithmetic-types.test.mts +130 -0
  172. package/src/number/num-arithmetic.test.mts +102 -0
  173. package/src/number/num.mts +96 -15
  174. package/src/number/refined-number-utils.mts +19 -9
@@ -0,0 +1,130 @@
1
+ import {
2
+ type FiniteNumber,
3
+ type Int,
4
+ type NegativeFiniteNumber,
5
+ type NegativeInt,
6
+ type NonNegativeFiniteNumber,
7
+ type NonPositiveInt,
8
+ type NonZeroInt,
9
+ type PositiveFiniteNumber,
10
+ type PositiveInt,
11
+ type PositiveSafeInt,
12
+ type SafeUint,
13
+ type Uint,
14
+ } from 'ts-type-forge';
15
+ import { expectType } from '../expect-type.mjs';
16
+ import {
17
+ type AddResult,
18
+ type DivIntResult,
19
+ type DivResult,
20
+ type LevelOf,
21
+ type MulResult,
22
+ type SignOf,
23
+ type SubResult,
24
+ } from './num-arithmetic-types.mjs';
25
+
26
+ describe('NumericArithmetic type-level', () => {
27
+ test('SignOf', () => {
28
+ expectType<SignOf<PositiveInt>, 'pos'>('=');
29
+
30
+ expectType<SignOf<NegativeInt>, 'neg'>('=');
31
+
32
+ expectType<SignOf<Uint>, 'nonneg'>('=');
33
+
34
+ expectType<SignOf<NonPositiveInt>, 'nonpos'>('=');
35
+
36
+ expectType<SignOf<NonZeroInt>, 'nonzero'>('=');
37
+
38
+ expectType<SignOf<Int>, 'any'>('=');
39
+
40
+ expectType<SignOf<number>, 'any'>('=');
41
+ });
42
+
43
+ test('LevelOf', () => {
44
+ expectType<LevelOf<PositiveSafeInt>, 'safeint'>('=');
45
+
46
+ expectType<LevelOf<PositiveInt>, 'int'>('=');
47
+
48
+ expectType<LevelOf<PositiveFiniteNumber>, 'finite'>('=');
49
+
50
+ expectType<LevelOf<number>, 'number'>('=');
51
+ });
52
+
53
+ test('mul: sign flips', () => {
54
+ // <=0 * <=0 = >=0
55
+ expectType<MulResult<NonPositiveInt, NonPositiveInt>, Uint>('=');
56
+
57
+ // <0 * <0 = >0
58
+ expectType<MulResult<NegativeInt, NegativeInt>, PositiveInt>('=');
59
+
60
+ // >0 * <0 = <0
61
+ expectType<MulResult<PositiveInt, NegativeInt>, NegativeInt>('=');
62
+
63
+ // finite negatives -> positive finite
64
+ expectType<
65
+ MulResult<NegativeFiniteNumber, NegativeFiniteNumber>,
66
+ PositiveFiniteNumber
67
+ >('=');
68
+ });
69
+
70
+ test('mul: safeint widens to int', () => {
71
+ expectType<MulResult<PositiveSafeInt, PositiveSafeInt>, PositiveInt>('=');
72
+ });
73
+
74
+ test('add', () => {
75
+ expectType<AddResult<PositiveInt, PositiveInt>, PositiveInt>('=');
76
+
77
+ expectType<AddResult<Uint, Uint>, Uint>('=');
78
+
79
+ // positive + negative -> unknown sign
80
+ expectType<AddResult<PositiveInt, NegativeInt>, Int>('=');
81
+
82
+ // nonzero + nonzero -> can be 0
83
+ expectType<AddResult<NonZeroInt, NonZeroInt>, Int>('=');
84
+
85
+ // safeint widens
86
+ expectType<AddResult<SafeUint, SafeUint>, Uint>('=');
87
+ });
88
+
89
+ test('sub', () => {
90
+ // same-sign sub -> unknown sign (the saturating case lives in branded)
91
+ expectType<SubResult<Uint, Uint>, Int>('=');
92
+
93
+ expectType<SubResult<NonPositiveInt, NonPositiveInt>, Int>('=');
94
+
95
+ // >=0 - <=0 = >=0
96
+ expectType<SubResult<Uint, NonPositiveInt>, Uint>('=');
97
+ });
98
+
99
+ test('div (exact) -> finite level', () => {
100
+ expectType<DivResult<PositiveInt, PositiveInt>, PositiveFiniteNumber>('=');
101
+
102
+ expectType<
103
+ DivResult<NegativeFiniteNumber, NegativeFiniteNumber>,
104
+ PositiveFiniteNumber
105
+ >('=');
106
+
107
+ expectType<DivResult<NonPositiveInt, NegativeInt>, NonNegativeFiniteNumber>(
108
+ '=',
109
+ );
110
+ });
111
+
112
+ test('divInt (floor)', () => {
113
+ // <0 / <0 = >0 but floor can be 0 -> Uint
114
+ expectType<DivIntResult<NegativeInt, NegativeInt>, Uint>('=');
115
+
116
+ // >0 / >0 floor can be 0 -> Uint
117
+ expectType<DivIntResult<PositiveInt, PositiveInt>, Uint>('=');
118
+
119
+ // safeint preserved under floor division
120
+ expectType<DivIntResult<PositiveSafeInt, PositiveSafeInt>, SafeUint>('=');
121
+ });
122
+
123
+ test('plain number stays number', () => {
124
+ expectType<AddResult<number, number>, number>('=');
125
+
126
+ expectType<MulResult<number, PositiveInt>, number>('=');
127
+
128
+ expectType<AddResult<FiniteNumber, FiniteNumber>, FiniteNumber>('=');
129
+ });
130
+ });
@@ -0,0 +1,102 @@
1
+ import {
2
+ type Int,
3
+ type NegativeInt,
4
+ type PositiveFiniteNumber,
5
+ type PositiveInt,
6
+ type Uint,
7
+ } from 'ts-type-forge';
8
+ import { expectType } from '../expect-type.mjs';
9
+ import {
10
+ asNegativeInt,
11
+ asNonPositiveInt,
12
+ asPositiveInt,
13
+ asUint,
14
+ } from './branded-types/index.mjs';
15
+ import { Num } from './num.mjs';
16
+
17
+ describe('Num arithmetic (general / sign-aware)', () => {
18
+ describe('Num.add', () => {
19
+ test('adds and keeps the value (no saturation)', () => {
20
+ expect(Num.add(asPositiveInt(4), asPositiveInt(5))).toBe(9);
21
+ });
22
+
23
+ test('positive + negative is typed Int', () => {
24
+ const r = Num.add(asPositiveInt(4), asNegativeInt(-9));
25
+
26
+ expect(r).toBe(-5);
27
+
28
+ expectType<typeof r, Int>('=');
29
+ });
30
+
31
+ test('positive + positive stays PositiveInt', () => {
32
+ expectType<
33
+ ReturnType<typeof Num.add<PositiveInt, PositiveInt>>,
34
+ PositiveInt
35
+ >('=');
36
+ });
37
+ });
38
+
39
+ describe('Num.sub', () => {
40
+ test('does not saturate (unlike branded Uint.sub)', () => {
41
+ const r = Num.sub(asUint(3), asUint(5));
42
+
43
+ expect(r).toBe(-2);
44
+
45
+ expectType<typeof r, Int>('=');
46
+ });
47
+ });
48
+
49
+ describe('Num.mul', () => {
50
+ test('negative * negative is positive (PositiveInt)', () => {
51
+ const r = Num.mul(asNegativeInt(-5), asNegativeInt(-3));
52
+
53
+ expect(r).toBe(15);
54
+
55
+ expectType<typeof r, PositiveInt>('=');
56
+ });
57
+
58
+ test('positive * negative is negative (NegativeInt)', () => {
59
+ const r = Num.mul(asPositiveInt(5), asNegativeInt(-3));
60
+
61
+ expect(r).toBe(-15);
62
+
63
+ expectType<typeof r, NegativeInt>('=');
64
+ });
65
+
66
+ test('non-positive * non-positive is non-negative (Uint)', () => {
67
+ const r = Num.mul(asNonPositiveInt(-4), asNonPositiveInt(-2));
68
+
69
+ expect(r).toBe(8);
70
+
71
+ expectType<typeof r, Uint>('=');
72
+ });
73
+ });
74
+
75
+ describe('Num.div (exact)', () => {
76
+ test('exact division keeps the fractional value', () => {
77
+ const r = Num.div(asPositiveInt(9), asPositiveInt(2));
78
+
79
+ expect(r).toBe(4.5);
80
+
81
+ expectType<typeof r, PositiveFiniteNumber>('=');
82
+ });
83
+
84
+ test('negative / negative is positive', () => {
85
+ expect(Num.div(asNegativeInt(-10), asNegativeInt(-2))).toBe(5);
86
+ });
87
+ });
88
+
89
+ describe('Num.divInt (floor)', () => {
90
+ test('floor division of negatives is non-negative (Uint)', () => {
91
+ const r = Num.divInt(asNegativeInt(-7), asNegativeInt(-2));
92
+
93
+ expect(r).toBe(3);
94
+
95
+ expectType<typeof r, Uint>('=');
96
+ });
97
+
98
+ test('floor can reach 0', () => {
99
+ expect(Num.divInt(asNegativeInt(-2), asNegativeInt(-3))).toBe(0);
100
+ });
101
+ });
102
+ });
@@ -19,6 +19,13 @@ import {
19
19
  import { expectType } from '../expect-type.mjs';
20
20
  import { Result } from '../functional/index.mjs';
21
21
  import { type SmallPositiveInt } from '../types.mjs';
22
+ import {
23
+ type AddResult,
24
+ type DivIntResult,
25
+ type DivResult,
26
+ type MulResult,
27
+ type SubResult,
28
+ } from './num-arithmetic-types.mjs';
22
29
 
23
30
  /**
24
31
  * Namespace providing utility functions for number manipulation and validation.
@@ -504,19 +511,86 @@ export namespace Num {
504
511
  }
505
512
 
506
513
  /**
507
- * Performs type-safe division with compile-time zero-check.
514
+ * Adds two numbers, returning the tightest sound branded result type.
515
+ *
516
+ * The sign of the result is computed exactly (e.g. a non-negative plus a
517
+ * non-negative is non-negative); the integer/finite level is preserved, with
518
+ * `SafeInt` operands widening to `Int` because the sum may exceed the safe
519
+ * range. No clamping is performed — for a result that stays inside a bounded
520
+ * type, use that type's branded `add` instead.
521
+ *
522
+ * @param a - The first addend.
523
+ * @param b - The second addend.
524
+ * @returns `a + b`, typed as the tightest branded type guaranteed to contain
525
+ * it.
526
+ */
527
+ export const add = <A extends number, B extends number>(
528
+ a: A,
529
+ b: B,
530
+ ): AddResult<A, B> =>
531
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
532
+ (a + b) as AddResult<A, B>;
533
+
534
+ /**
535
+ * Subtracts `b` from `a`, returning the tightest sound branded result type.
508
536
  *
509
- * This function leverages TypeScript's type system to prevent division by
510
- * zero at compile time. The divisor must be typed as NonZeroNumber or a
511
- * non-zero numeric literal.
537
+ * The sign is computed exactly; the level is preserved (with `SafeInt`
538
+ * widening to `Int`). No clamping — for saturating subtraction that stays in a
539
+ * bounded type (e.g. `Uint.sub` clamping at `0`), use that type's branded
540
+ * `sub`.
512
541
  *
513
- * @param a - The dividend
514
- * @param b - The divisor (must be non-zero, enforced by types)
515
- * @returns The quotient of a / b
542
+ * @param a - The minuend.
543
+ * @param b - The subtrahend.
544
+ * @returns `a - b`, typed as the tightest branded type guaranteed to contain
545
+ * it.
516
546
  */
517
- export const div = (a: number, b: NonZeroNumber | SmallInt<'!=0'>): number =>
518
- // eslint-disable-next-line total-functions/no-partial-division
519
- a / b;
547
+ export const sub = <A extends number, B extends number>(
548
+ a: A,
549
+ b: B,
550
+ ): SubResult<A, B> =>
551
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
552
+ (a - b) as SubResult<A, B>;
553
+
554
+ /**
555
+ * Multiplies two numbers, returning the tightest sound branded result type.
556
+ *
557
+ * The sign follows the usual product rule (e.g. negative times negative is
558
+ * positive); the level is preserved (with `SafeInt` widening to `Int`). No
559
+ * clamping.
560
+ *
561
+ * @param a - The first factor.
562
+ * @param b - The second factor.
563
+ * @returns `a * b`, typed as the tightest branded type guaranteed to contain
564
+ * it.
565
+ */
566
+ export const mul = <A extends number, B extends number>(
567
+ a: A,
568
+ b: B,
569
+ ): MulResult<A, B> =>
570
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
571
+ (a * b) as MulResult<A, B>;
572
+
573
+ /**
574
+ * Performs type-safe exact division with a compile-time zero-check.
575
+ *
576
+ * The divisor must be typed as `NonZeroNumber` or a non-zero numeric literal.
577
+ * The result sign follows the quotient rule; because exact division can be
578
+ * fractional, the level is `finite` (or `number`).
579
+ *
580
+ * @param a - The dividend.
581
+ * @param b - The divisor (must be non-zero, enforced by types).
582
+ * @returns `a / b`, typed as the tightest branded type guaranteed to contain
583
+ * it.
584
+ */
585
+ export const div = <
586
+ A extends number,
587
+ B extends NonZeroNumber | SmallInt<'!=0'>,
588
+ >(
589
+ a: A,
590
+ b: B,
591
+ ): DivResult<A, B> =>
592
+ // eslint-disable-next-line total-functions/no-partial-division, total-functions/no-unsafe-type-assertion
593
+ (a / b) as DivResult<A, B>;
520
594
 
521
595
  /**
522
596
  * Performs integer division using floor division.
@@ -531,12 +605,19 @@ export namespace Num {
531
605
  * @param b - The divisor
532
606
  * @returns The integer quotient, or `NaN` if b is zero
533
607
  */
534
- export const divInt = (
535
- a: number,
536
- b: NonZeroNumber | SmallInt<'!=0'>,
537
- ): number =>
608
+ export const divInt = <
609
+ A extends number,
610
+ B extends NonZeroNumber | SmallInt<'!=0'>,
611
+ >(
612
+ a: A,
613
+ b: B,
614
+ ): DivIntResult<A, B> => {
538
615
  // eslint-disable-next-line total-functions/no-partial-division
539
- Math.floor(Math.floor(a) / Math.floor(b));
616
+ const quotient = Math.floor(Math.floor(a) / Math.floor(b));
617
+
618
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
619
+ return quotient as DivIntResult<A, B>;
620
+ };
540
621
 
541
622
  /**
542
623
  * Rounds a number to a specified number of decimal places.
@@ -6,6 +6,7 @@ import {
6
6
  type IntersectBrand,
7
7
  type NonNegativeNumber,
8
8
  type NonZeroNumber,
9
+ type PositiveNumber,
9
10
  type RelaxedExclude,
10
11
  type TypeEq,
11
12
  type UnknownBrand,
@@ -84,10 +85,10 @@ export namespace TsDataForgeInternals {
84
85
  * @template N - A branded number type
85
86
  * @internal
86
87
  */
87
- export type ToNonNegative<N extends UnknownNumberBrand> = IntersectBrand<
88
- N,
89
- NonNegativeNumber
90
- >;
88
+ export type ToNonNegative<N extends UnknownNumberBrand> =
89
+ '!=0' extends UnwrapBrandTrueKeys<N>
90
+ ? IntersectBrand<N, PositiveNumber>
91
+ : IntersectBrand<N, NonNegativeNumber>;
91
92
 
92
93
  /**
93
94
  * Removes the non-zero brand constraint from a branded number type. Used
@@ -99,7 +100,7 @@ export namespace TsDataForgeInternals {
99
100
  export type RemoveNonZeroBrandKey<N extends UnknownNumberBrand> = Brand<
100
101
  GetBrandValuePart<N>,
101
102
  RelaxedExclude<UnwrapBrandTrueKeys<N>, '!=0'> & string,
102
- UnwrapBrandFalseKeys<N> & string
103
+ RelaxedExclude<UnwrapBrandFalseKeys<N>, '<=0' | '>=0'> & string
103
104
  >;
104
105
 
105
106
  type CastToInt<N> = N extends Int ? N : never;
@@ -120,9 +121,13 @@ export namespace TsDataForgeInternals {
120
121
  * 'positive' | 'range'
121
122
  * @internal
122
123
  */
124
+ /** The closed binary arithmetic operations a branded namespace may expose. */
125
+ export type NumberClassBinOp = 'add' | 'div' | 'mul' | 'sub';
126
+
123
127
  export type NumberClass<
124
128
  N extends UnknownNumberBrand,
125
129
  classes extends 'int' | 'non-negative' | 'positive' | 'range',
130
+ binOps extends NumberClassBinOp = NumberClassBinOp,
126
131
  > = ('int' extends classes
127
132
  ? unknown
128
133
  : 'positive' extends classes
@@ -162,16 +167,21 @@ export namespace TsDataForgeInternals {
162
167
  clamp: (a: number) => N;
163
168
  }>
164
169
  : unknown) &
170
+ Pick<
171
+ Readonly<{
172
+ add: (x: N, y: N) => N;
173
+ sub: (x: N, y: N) => N;
174
+ mul: (x: N, y: N) => N;
175
+ div: (x: N, y: ToNonZero<N>) => N;
176
+ }>,
177
+ binOps
178
+ > &
165
179
  Readonly<{
166
180
  is: (a: number) => a is N;
167
181
  min: (...values: readonly N[]) => N;
168
182
  max: (...values: readonly N[]) => N;
169
183
  random: (min: N, max: N) => N;
170
184
  pow: (x: N, y: N) => N;
171
- add: (x: N, y: N) => N;
172
- sub: (x: N, y: N) => N;
173
- mul: (x: N, y: N) => N;
174
- div: (x: N, y: ToNonZero<N>) => N;
175
185
  }>;
176
186
 
177
187
  type BaseKeys =