ts-data-forge 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/LICENSE +201 -0
- package/README.md +534 -0
- package/package.json +101 -0
- package/src/array/array-utils-creation.test.mts +443 -0
- package/src/array/array-utils-modification.test.mts +197 -0
- package/src/array/array-utils-overload-type-error.test.mts +149 -0
- package/src/array/array-utils-reducing-value.test.mts +425 -0
- package/src/array/array-utils-search.test.mts +169 -0
- package/src/array/array-utils-set-op.test.mts +335 -0
- package/src/array/array-utils-slice-clamped.test.mts +113 -0
- package/src/array/array-utils-slicing.test.mts +316 -0
- package/src/array/array-utils-transformation.test.mts +790 -0
- package/src/array/array-utils-validation.test.mts +492 -0
- package/src/array/array-utils.mts +4000 -0
- package/src/array/array.test.mts +146 -0
- package/src/array/index.mts +2 -0
- package/src/array/tuple-utils.mts +519 -0
- package/src/array/tuple-utils.test.mts +518 -0
- package/src/collections/imap-mapped.mts +801 -0
- package/src/collections/imap-mapped.test.mts +860 -0
- package/src/collections/imap.mts +651 -0
- package/src/collections/imap.test.mts +932 -0
- package/src/collections/index.mts +6 -0
- package/src/collections/iset-mapped.mts +889 -0
- package/src/collections/iset-mapped.test.mts +1187 -0
- package/src/collections/iset.mts +682 -0
- package/src/collections/iset.test.mts +1084 -0
- package/src/collections/queue.mts +390 -0
- package/src/collections/queue.test.mts +282 -0
- package/src/collections/stack.mts +423 -0
- package/src/collections/stack.test.mts +225 -0
- package/src/expect-type.mts +206 -0
- package/src/functional/index.mts +4 -0
- package/src/functional/match.mts +300 -0
- package/src/functional/match.test.mts +177 -0
- package/src/functional/optional.mts +733 -0
- package/src/functional/optional.test.mts +619 -0
- package/src/functional/pipe.mts +212 -0
- package/src/functional/pipe.test.mts +85 -0
- package/src/functional/result.mts +1134 -0
- package/src/functional/result.test.mts +777 -0
- package/src/globals.d.mts +38 -0
- package/src/guard/has-key.mts +119 -0
- package/src/guard/has-key.test.mts +219 -0
- package/src/guard/index.mts +7 -0
- package/src/guard/is-non-empty-string.mts +108 -0
- package/src/guard/is-non-empty-string.test.mts +91 -0
- package/src/guard/is-non-null-object.mts +106 -0
- package/src/guard/is-non-null-object.test.mts +90 -0
- package/src/guard/is-primitive.mts +165 -0
- package/src/guard/is-primitive.test.mts +102 -0
- package/src/guard/is-record.mts +153 -0
- package/src/guard/is-record.test.mts +112 -0
- package/src/guard/is-type.mts +450 -0
- package/src/guard/is-type.test.mts +496 -0
- package/src/guard/key-is-in.mts +163 -0
- package/src/guard/key-is-in.test.mts +19 -0
- package/src/index.mts +10 -0
- package/src/iterator/index.mts +1 -0
- package/src/iterator/range.mts +120 -0
- package/src/iterator/range.test.mts +33 -0
- package/src/json/index.mts +1 -0
- package/src/json/json.mts +711 -0
- package/src/json/json.test.mts +628 -0
- package/src/number/branded-types/finite-number.mts +354 -0
- package/src/number/branded-types/finite-number.test.mts +135 -0
- package/src/number/branded-types/index.mts +26 -0
- package/src/number/branded-types/int.mts +278 -0
- package/src/number/branded-types/int.test.mts +140 -0
- package/src/number/branded-types/int16.mts +192 -0
- package/src/number/branded-types/int16.test.mts +170 -0
- package/src/number/branded-types/int32.mts +193 -0
- package/src/number/branded-types/int32.test.mts +170 -0
- package/src/number/branded-types/non-negative-finite-number.mts +223 -0
- package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
- package/src/number/branded-types/non-negative-int16.mts +187 -0
- package/src/number/branded-types/non-negative-int16.test.mts +201 -0
- package/src/number/branded-types/non-negative-int32.mts +187 -0
- package/src/number/branded-types/non-negative-int32.test.mts +204 -0
- package/src/number/branded-types/non-zero-finite-number.mts +229 -0
- package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
- package/src/number/branded-types/non-zero-int.mts +167 -0
- package/src/number/branded-types/non-zero-int.test.mts +177 -0
- package/src/number/branded-types/non-zero-int16.mts +196 -0
- package/src/number/branded-types/non-zero-int16.test.mts +195 -0
- package/src/number/branded-types/non-zero-int32.mts +196 -0
- package/src/number/branded-types/non-zero-int32.test.mts +197 -0
- package/src/number/branded-types/non-zero-safe-int.mts +196 -0
- package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
- package/src/number/branded-types/non-zero-uint16.mts +189 -0
- package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
- package/src/number/branded-types/non-zero-uint32.mts +189 -0
- package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
- package/src/number/branded-types/positive-finite-number.mts +241 -0
- package/src/number/branded-types/positive-finite-number.test.mts +204 -0
- package/src/number/branded-types/positive-int.mts +304 -0
- package/src/number/branded-types/positive-int.test.mts +176 -0
- package/src/number/branded-types/positive-int16.mts +188 -0
- package/src/number/branded-types/positive-int16.test.mts +197 -0
- package/src/number/branded-types/positive-int32.mts +188 -0
- package/src/number/branded-types/positive-int32.test.mts +197 -0
- package/src/number/branded-types/positive-safe-int.mts +187 -0
- package/src/number/branded-types/positive-safe-int.test.mts +210 -0
- package/src/number/branded-types/positive-uint16.mts +188 -0
- package/src/number/branded-types/positive-uint16.test.mts +203 -0
- package/src/number/branded-types/positive-uint32.mts +188 -0
- package/src/number/branded-types/positive-uint32.test.mts +203 -0
- package/src/number/branded-types/safe-int.mts +291 -0
- package/src/number/branded-types/safe-int.test.mts +170 -0
- package/src/number/branded-types/safe-uint.mts +187 -0
- package/src/number/branded-types/safe-uint.test.mts +176 -0
- package/src/number/branded-types/uint.mts +179 -0
- package/src/number/branded-types/uint.test.mts +158 -0
- package/src/number/branded-types/uint16.mts +186 -0
- package/src/number/branded-types/uint16.test.mts +170 -0
- package/src/number/branded-types/uint32.mts +218 -0
- package/src/number/branded-types/uint32.test.mts +170 -0
- package/src/number/enum/index.mts +2 -0
- package/src/number/enum/int8.mts +344 -0
- package/src/number/enum/int8.test.mts +180 -0
- package/src/number/enum/uint8.mts +293 -0
- package/src/number/enum/uint8.test.mts +164 -0
- package/src/number/index.mts +4 -0
- package/src/number/num.mts +604 -0
- package/src/number/num.test.mts +242 -0
- package/src/number/refined-number-utils.mts +566 -0
- package/src/object/index.mts +1 -0
- package/src/object/object.mts +447 -0
- package/src/object/object.test.mts +124 -0
- package/src/others/cast-mutable.mts +113 -0
- package/src/others/cast-readonly.mts +192 -0
- package/src/others/cast-readonly.test.mts +89 -0
- package/src/others/if-then.mts +98 -0
- package/src/others/if-then.test.mts +75 -0
- package/src/others/index.mts +7 -0
- package/src/others/map-nullable.mts +172 -0
- package/src/others/map-nullable.test.mts +297 -0
- package/src/others/memoize-function.mts +196 -0
- package/src/others/memoize-function.test.mts +168 -0
- package/src/others/tuple.mts +160 -0
- package/src/others/tuple.test.mts +11 -0
- package/src/others/unknown-to-string.mts +215 -0
- package/src/others/unknown-to-string.test.mts +114 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { expectType } from '../../expect-type.mjs';
|
|
2
|
+
import {
|
|
3
|
+
asPositiveUint32,
|
|
4
|
+
isPositiveUint32,
|
|
5
|
+
PositiveUint32,
|
|
6
|
+
} from './positive-uint32.mjs';
|
|
7
|
+
|
|
8
|
+
describe('PositiveUint32', () => {
|
|
9
|
+
describe('asPositiveUint32', () => {
|
|
10
|
+
test('accepts valid positive uint32 values', () => {
|
|
11
|
+
expect(() => asPositiveUint32(1)).not.toThrow();
|
|
12
|
+
expect(() => asPositiveUint32(1000)).not.toThrow();
|
|
13
|
+
expect(() => asPositiveUint32(4294967295)).not.toThrow(); // 2^32 - 1
|
|
14
|
+
expect(() => asPositiveUint32(2147483648)).not.toThrow(); // 2^31
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('rejects zero', () => {
|
|
18
|
+
expect(() => asPositiveUint32(0)).toThrow(TypeError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('rejects values outside uint32 range', () => {
|
|
22
|
+
expect(() => asPositiveUint32(4294967296)).toThrow(TypeError); // 2^32
|
|
23
|
+
expect(() => asPositiveUint32(10000000000)).toThrow(TypeError);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('rejects negative integers', () => {
|
|
27
|
+
expect(() => asPositiveUint32(-1)).toThrow(TypeError);
|
|
28
|
+
expect(() => asPositiveUint32(-42)).toThrow(TypeError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('rejects non-integers', () => {
|
|
32
|
+
expect(() => asPositiveUint32(Number.NaN)).toThrow(TypeError);
|
|
33
|
+
expect(() => asPositiveUint32(Number.POSITIVE_INFINITY)).toThrow(
|
|
34
|
+
TypeError,
|
|
35
|
+
);
|
|
36
|
+
expect(() => asPositiveUint32(Number.NEGATIVE_INFINITY)).toThrow(
|
|
37
|
+
TypeError,
|
|
38
|
+
);
|
|
39
|
+
expect(() => asPositiveUint32(1.2)).toThrow(TypeError);
|
|
40
|
+
expect(() => asPositiveUint32(-3.4)).toThrow(TypeError);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('returns the same value for valid inputs', () => {
|
|
44
|
+
expect(asPositiveUint32(5)).toBe(5);
|
|
45
|
+
expect(asPositiveUint32(1)).toBe(1);
|
|
46
|
+
expect(asPositiveUint32(4294967295)).toBe(4294967295);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test.each([
|
|
50
|
+
{ name: 'Number.NaN', value: Number.NaN },
|
|
51
|
+
{ name: 'Number.POSITIVE_INFINITY', value: Number.POSITIVE_INFINITY },
|
|
52
|
+
{ name: 'Number.NEGATIVE_INFINITY', value: Number.NEGATIVE_INFINITY },
|
|
53
|
+
{ name: '1.2', value: 1.2 },
|
|
54
|
+
{ name: '-3.4', value: -3.4 },
|
|
55
|
+
{ name: '0', value: 0 },
|
|
56
|
+
{ name: '-1', value: -1 },
|
|
57
|
+
{ name: '4294967296', value: 4294967296 },
|
|
58
|
+
] as const)(
|
|
59
|
+
`asPositiveUint32($name) should throw a TypeError`,
|
|
60
|
+
({ value }) => {
|
|
61
|
+
expect(() => asPositiveUint32(value)).toThrow(
|
|
62
|
+
new TypeError(
|
|
63
|
+
`Expected a positive integer in [1, 2^32), got: ${value}`,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('isPositiveUint32', () => {
|
|
71
|
+
test('correctly identifies positive uint32 values', () => {
|
|
72
|
+
expect(isPositiveUint32(1)).toBe(true);
|
|
73
|
+
expect(isPositiveUint32(1000)).toBe(true);
|
|
74
|
+
expect(isPositiveUint32(4294967295)).toBe(true);
|
|
75
|
+
expect(isPositiveUint32(2147483648)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('correctly identifies zero', () => {
|
|
79
|
+
expect(isPositiveUint32(0)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('correctly identifies values outside uint32 range', () => {
|
|
83
|
+
expect(isPositiveUint32(4294967296)).toBe(false);
|
|
84
|
+
expect(isPositiveUint32(10000000000)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('correctly identifies negative integers', () => {
|
|
88
|
+
expect(isPositiveUint32(-1)).toBe(false);
|
|
89
|
+
expect(isPositiveUint32(-42)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('correctly identifies non-integers', () => {
|
|
93
|
+
expect(isPositiveUint32(Number.NaN)).toBe(false);
|
|
94
|
+
expect(isPositiveUint32(Number.POSITIVE_INFINITY)).toBe(false);
|
|
95
|
+
expect(isPositiveUint32(Number.NEGATIVE_INFINITY)).toBe(false);
|
|
96
|
+
expect(isPositiveUint32(1.2)).toBe(false);
|
|
97
|
+
expect(isPositiveUint32(-3.4)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('PositiveUint32.is', () => {
|
|
102
|
+
test('same as isPositiveUint32 function', () => {
|
|
103
|
+
expect(PositiveUint32.is(5)).toBe(isPositiveUint32(5));
|
|
104
|
+
expect(PositiveUint32.is(0)).toBe(isPositiveUint32(0));
|
|
105
|
+
expect(PositiveUint32.is(-1)).toBe(isPositiveUint32(-1));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('constants', () => {
|
|
110
|
+
test('MIN_VALUE and MAX_VALUE', () => {
|
|
111
|
+
expect(PositiveUint32.MIN_VALUE).toBe(1);
|
|
112
|
+
expect(PositiveUint32.MAX_VALUE).toBe(4294967295);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('mathematical operations', () => {
|
|
117
|
+
const a = asPositiveUint32(1000000);
|
|
118
|
+
const b = asPositiveUint32(500000);
|
|
119
|
+
|
|
120
|
+
test('min and max', () => {
|
|
121
|
+
expect(PositiveUint32.min(a, b)).toBe(500000);
|
|
122
|
+
expect(PositiveUint32.max(a, b)).toBe(1000000);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('add (with clamping to positive uint32 range)', () => {
|
|
126
|
+
const result = PositiveUint32.add(
|
|
127
|
+
asPositiveUint32(4294967000),
|
|
128
|
+
asPositiveUint32(1000),
|
|
129
|
+
);
|
|
130
|
+
expect(result).toBe(4294967295); // clamped to max
|
|
131
|
+
expect(PositiveUint32.add(a, b)).toBe(1500000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('sub (never goes below 1)', () => {
|
|
135
|
+
expect(PositiveUint32.sub(a, b)).toBe(500000);
|
|
136
|
+
expect(PositiveUint32.sub(b, a)).toBe(1); // clamped to 1
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('mul (with clamping to positive uint32 range)', () => {
|
|
140
|
+
const result = PositiveUint32.mul(
|
|
141
|
+
asPositiveUint32(100000),
|
|
142
|
+
asPositiveUint32(100000),
|
|
143
|
+
);
|
|
144
|
+
expect(result).toBe(4294967295); // clamped to max
|
|
145
|
+
expect(
|
|
146
|
+
PositiveUint32.mul(asPositiveUint32(1000), asPositiveUint32(5)),
|
|
147
|
+
).toBe(5000);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('div (floor division, never goes below 1)', () => {
|
|
151
|
+
expect(PositiveUint32.div(a, asPositiveUint32(500000))).toBe(2);
|
|
152
|
+
expect(PositiveUint32.div(asPositiveUint32(7), asPositiveUint32(3))).toBe(
|
|
153
|
+
2,
|
|
154
|
+
);
|
|
155
|
+
expect(
|
|
156
|
+
PositiveUint32.div(asPositiveUint32(500000), asPositiveUint32(1000000)),
|
|
157
|
+
).toBe(1); // floor(500000/1000000) = 0, clamped to 1
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('pow (with clamping to positive uint32 range)', () => {
|
|
161
|
+
const result = PositiveUint32.pow(
|
|
162
|
+
asPositiveUint32(10000),
|
|
163
|
+
asPositiveUint32(3),
|
|
164
|
+
);
|
|
165
|
+
expect(result).toBe(4294967295); // clamped to max
|
|
166
|
+
expect(PositiveUint32.pow(asPositiveUint32(2), asPositiveUint32(3))).toBe(
|
|
167
|
+
8,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('random', () => {
|
|
173
|
+
test('generates positive uint32 values within specified range', () => {
|
|
174
|
+
const min = 1;
|
|
175
|
+
const max = 20;
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < 10; i++) {
|
|
178
|
+
const result = PositiveUint32.random(min, max);
|
|
179
|
+
expect(result).toBeGreaterThanOrEqual(min);
|
|
180
|
+
expect(result).toBeLessThanOrEqual(max);
|
|
181
|
+
expect(PositiveUint32.is(result)).toBe(true);
|
|
182
|
+
expect(Number.isInteger(result)).toBe(true);
|
|
183
|
+
expect(result).toBeGreaterThan(0);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('generates values within PositiveUint32 range', () => {
|
|
188
|
+
for (let i = 0; i < 10; i++) {
|
|
189
|
+
const result = PositiveUint32.random(1, 30);
|
|
190
|
+
expect(result).toBeGreaterThanOrEqual(1);
|
|
191
|
+
expect(result).toBeLessThanOrEqual(4294967295);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('type assertions', () => {
|
|
197
|
+
test('type relationships', () => {
|
|
198
|
+
expectType<PositiveUint32, number>('<=');
|
|
199
|
+
|
|
200
|
+
expectTypeOf(asPositiveUint32(1000000)).toExtend<PositiveUint32>();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { expectType } from '../../expect-type.mjs';
|
|
2
|
+
import { TsVerifiedInternals } from '../refined-number-utils.mjs';
|
|
3
|
+
|
|
4
|
+
type ElementType = SafeInt;
|
|
5
|
+
|
|
6
|
+
const typeNameInMessage = 'a safe integer';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
MIN_VALUE,
|
|
10
|
+
MAX_VALUE,
|
|
11
|
+
abs,
|
|
12
|
+
min: min_,
|
|
13
|
+
max: max_,
|
|
14
|
+
pow,
|
|
15
|
+
add,
|
|
16
|
+
sub,
|
|
17
|
+
mul,
|
|
18
|
+
div,
|
|
19
|
+
random,
|
|
20
|
+
is,
|
|
21
|
+
castType,
|
|
22
|
+
clamp,
|
|
23
|
+
} = TsVerifiedInternals.RefinedNumberUtils.operatorsForInteger<
|
|
24
|
+
ElementType,
|
|
25
|
+
SafeInt,
|
|
26
|
+
SafeUint
|
|
27
|
+
>({
|
|
28
|
+
integerOrSafeInteger: 'SafeInteger',
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
30
|
+
MIN_VALUE: Number.MIN_SAFE_INTEGER as SafeInt,
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
32
|
+
MAX_VALUE: Number.MAX_SAFE_INTEGER as SafeUint,
|
|
33
|
+
typeNameInMessage,
|
|
34
|
+
} as const);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Type guard that checks if a value is a safe integer.
|
|
38
|
+
*
|
|
39
|
+
* A safe integer is an integer that can be exactly represented in JavaScript
|
|
40
|
+
* without precision loss. The range is [±(2^53 - 1)].
|
|
41
|
+
*
|
|
42
|
+
* @param value - The value to check
|
|
43
|
+
* @returns `true` if the value is a safe integer, `false` otherwise
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* isSafeInt(42); // true
|
|
48
|
+
* isSafeInt(Number.MAX_SAFE_INTEGER); // true
|
|
49
|
+
* isSafeInt(Number.MAX_SAFE_INTEGER + 1); // false
|
|
50
|
+
* isSafeInt(3.14); // false
|
|
51
|
+
* isSafeInt(NaN); // false
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const isSafeInt = is;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Casts a number to a SafeInt branded type.
|
|
58
|
+
*
|
|
59
|
+
* This function validates that the input is a safe integer (within ±(2^53 - 1))
|
|
60
|
+
* and returns it with the SafeInt brand. This ensures type safety for operations
|
|
61
|
+
* that require precise integer arithmetic.
|
|
62
|
+
*
|
|
63
|
+
* @param value - The value to cast
|
|
64
|
+
* @returns The value as a SafeInt branded type
|
|
65
|
+
* @throws {TypeError} If the value is not a safe integer
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const x = asSafeInt(5); // SafeInt
|
|
70
|
+
* const y = asSafeInt(-1000); // SafeInt
|
|
71
|
+
* const z = asSafeInt(2**50); // SafeInt (within range)
|
|
72
|
+
*
|
|
73
|
+
* // These throw TypeError:
|
|
74
|
+
* // asSafeInt(1.5); // Not an integer
|
|
75
|
+
* // asSafeInt(Number.MAX_SAFE_INTEGER + 1); // Exceeds safe range
|
|
76
|
+
* // asSafeInt(2**53); // Loss of precision
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const asSafeInt = castType;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Namespace providing type-safe operations for SafeInt branded types.
|
|
83
|
+
*
|
|
84
|
+
* SafeInt represents integers that can be exactly represented in JavaScript's
|
|
85
|
+
* number type without precision loss. The range is [±(2^53 - 1)], which covers
|
|
86
|
+
* approximately ±9 quadrillion.
|
|
87
|
+
*
|
|
88
|
+
* All operations automatically clamp results to stay within the safe range,
|
|
89
|
+
* preventing precision loss that occurs with larger integers. This makes SafeInt
|
|
90
|
+
* ideal for:
|
|
91
|
+
* - Financial calculations requiring exact cents
|
|
92
|
+
* - Database IDs and counters
|
|
93
|
+
* - Array indices and sizes
|
|
94
|
+
* - Any integer arithmetic requiring precision guarantees
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* // Near the boundary
|
|
99
|
+
* const nearMax = asSafeInt(9007199254740990);
|
|
100
|
+
* const increment = asSafeInt(10);
|
|
101
|
+
*
|
|
102
|
+
* // Automatic clamping prevents precision loss
|
|
103
|
+
* const sum = SafeInt.add(nearMax, increment); // Clamped to MAX_SAFE_INTEGER
|
|
104
|
+
* const product = SafeInt.mul(nearMax, increment); // Clamped to MAX_SAFE_INTEGER
|
|
105
|
+
*
|
|
106
|
+
* // Safe operations
|
|
107
|
+
* const a = asSafeInt(1000000);
|
|
108
|
+
* const b = asSafeInt(500);
|
|
109
|
+
*
|
|
110
|
+
* const diff = SafeInt.sub(a, b); // SafeInt (999500)
|
|
111
|
+
* const quotient = SafeInt.div(a, b); // SafeInt (2000)
|
|
112
|
+
* const power = SafeInt.pow(b, asSafeInt(2)); // SafeInt (250000)
|
|
113
|
+
*
|
|
114
|
+
* // Utility operations
|
|
115
|
+
* const absolute = SafeInt.abs(asSafeInt(-42)); // SafeInt (42)
|
|
116
|
+
* const clamped = SafeInt.clamp(2**60); // SafeInt (MAX_SAFE_INTEGER)
|
|
117
|
+
*
|
|
118
|
+
* // Random generation
|
|
119
|
+
* const die = SafeInt.random(asSafeInt(1), asSafeInt(6)); // Random 1-6
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export const SafeInt = {
|
|
123
|
+
/**
|
|
124
|
+
* Type guard that checks if a value is a safe integer.
|
|
125
|
+
*
|
|
126
|
+
* @param value - The value to check
|
|
127
|
+
* @returns `true` if the value is a safe integer, `false` otherwise
|
|
128
|
+
*
|
|
129
|
+
* @see {@link isSafeInt} for usage examples
|
|
130
|
+
*/
|
|
131
|
+
is,
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The minimum safe integer value (-(2^53 - 1)).
|
|
135
|
+
* @readonly
|
|
136
|
+
*/
|
|
137
|
+
MIN_VALUE,
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The maximum safe integer value (2^53 - 1).
|
|
141
|
+
* @readonly
|
|
142
|
+
*/
|
|
143
|
+
MAX_VALUE,
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns the absolute value of a safe integer.
|
|
147
|
+
*
|
|
148
|
+
* Note: `Math.abs(MIN_SAFE_INTEGER)` would exceed `MAX_SAFE_INTEGER`,
|
|
149
|
+
* so this function clamps the result to maintain the safe integer guarantee.
|
|
150
|
+
*
|
|
151
|
+
* @param a - The safe integer value
|
|
152
|
+
* @returns The absolute value as a SafeInt, clamped if necessary
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* SafeInt.abs(asSafeInt(-42)); // SafeInt (42)
|
|
157
|
+
* SafeInt.abs(asSafeInt(42)); // SafeInt (42)
|
|
158
|
+
* SafeInt.abs(SafeInt.MIN_VALUE); // SafeInt (MAX_SAFE_INTEGER)
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
abs,
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns the minimum value from a list of safe integers.
|
|
165
|
+
*
|
|
166
|
+
* @param values - The safe integers to compare (at least one required)
|
|
167
|
+
* @returns The smallest value as a SafeInt
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* SafeInt.min(asSafeInt(5), asSafeInt(3)); // SafeInt (3)
|
|
172
|
+
* SafeInt.min(asSafeInt(-10), asSafeInt(0), asSafeInt(10)); // SafeInt (-10)
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
min: min_,
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns the maximum value from a list of safe integers.
|
|
179
|
+
*
|
|
180
|
+
* @param values - The safe integers to compare (at least one required)
|
|
181
|
+
* @returns The largest value as a SafeInt
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* SafeInt.max(asSafeInt(5), asSafeInt(3)); // SafeInt (5)
|
|
186
|
+
* SafeInt.max(asSafeInt(-10), asSafeInt(0), asSafeInt(10)); // SafeInt (10)
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
max: max_,
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clamps a number to the safe integer range.
|
|
193
|
+
* @param value The number to clamp.
|
|
194
|
+
* @returns The value clamped to [MIN_SAFE_INTEGER, MAX_SAFE_INTEGER] as a SafeInt.
|
|
195
|
+
*/
|
|
196
|
+
clamp,
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generates a random safe integer within the specified range (inclusive).
|
|
200
|
+
*
|
|
201
|
+
* The range is inclusive on both ends. If min > max, they are automatically swapped.
|
|
202
|
+
*
|
|
203
|
+
* @param min - The minimum value (inclusive)
|
|
204
|
+
* @param max - The maximum value (inclusive)
|
|
205
|
+
* @returns A random SafeInt in the range [min, max]
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* // Dice roll
|
|
210
|
+
* const d20 = SafeInt.random(asSafeInt(1), asSafeInt(20));
|
|
211
|
+
*
|
|
212
|
+
* // Random index for large array
|
|
213
|
+
* const index = SafeInt.random(asSafeInt(0), asSafeInt(1000000));
|
|
214
|
+
*
|
|
215
|
+
* // Can use full safe range
|
|
216
|
+
* const any = SafeInt.random(SafeInt.MIN_VALUE, SafeInt.MAX_VALUE);
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
random,
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Raises a SafeInt to the power of another SafeInt.
|
|
223
|
+
* @param a The base SafeInt.
|
|
224
|
+
* @param b The exponent SafeInt.
|
|
225
|
+
* @returns `a ** b` clamped to safe integer range as a SafeInt.
|
|
226
|
+
*/
|
|
227
|
+
pow,
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Adds two SafeInt values.
|
|
231
|
+
* @param a The first SafeInt.
|
|
232
|
+
* @param b The second SafeInt.
|
|
233
|
+
* @returns `a + b` clamped to safe integer range as a SafeInt.
|
|
234
|
+
*/
|
|
235
|
+
add,
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Subtracts one SafeInt from another.
|
|
239
|
+
* @param a The minuend SafeInt.
|
|
240
|
+
* @param b The subtrahend SafeInt.
|
|
241
|
+
* @returns `a - b` clamped to safe integer range as a SafeInt.
|
|
242
|
+
*/
|
|
243
|
+
sub,
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Multiplies two SafeInt values.
|
|
247
|
+
* @param a The first SafeInt.
|
|
248
|
+
* @param b The second SafeInt.
|
|
249
|
+
* @returns `a * b` clamped to safe integer range as a SafeInt.
|
|
250
|
+
*/
|
|
251
|
+
mul,
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Divides one SafeInt by another using floor division.
|
|
255
|
+
*
|
|
256
|
+
* Performs mathematical floor division: `⌊a / b⌋`.
|
|
257
|
+
* The divisor must be non-zero (enforced by type constraints).
|
|
258
|
+
*
|
|
259
|
+
* @param a - The dividend
|
|
260
|
+
* @param b - The divisor (must be non-zero)
|
|
261
|
+
* @returns The integer quotient as a SafeInt
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* SafeInt.div(asSafeInt(10), asSafeInt(3)); // SafeInt (3)
|
|
266
|
+
* SafeInt.div(asSafeInt(-10), asSafeInt(3)); // SafeInt (-4)
|
|
267
|
+
*
|
|
268
|
+
* // Large number division
|
|
269
|
+
* const large = asSafeInt(1000000000000);
|
|
270
|
+
* const divisor = asSafeInt(1000000);
|
|
271
|
+
* SafeInt.div(large, divisor); // SafeInt (1000000)
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
div,
|
|
275
|
+
} as const;
|
|
276
|
+
|
|
277
|
+
expectType<
|
|
278
|
+
keyof typeof SafeInt,
|
|
279
|
+
keyof TsVerifiedInternals.RefinedNumberUtils.NumberClass<
|
|
280
|
+
ElementType,
|
|
281
|
+
'int' | 'range'
|
|
282
|
+
>
|
|
283
|
+
>('=');
|
|
284
|
+
|
|
285
|
+
expectType<
|
|
286
|
+
typeof SafeInt,
|
|
287
|
+
TsVerifiedInternals.RefinedNumberUtils.NumberClass<
|
|
288
|
+
ElementType,
|
|
289
|
+
'int' | 'range'
|
|
290
|
+
>
|
|
291
|
+
>('<=');
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { expectType } from '../../expect-type.mjs';
|
|
2
|
+
import { asSafeInt, SafeInt } from './safe-int.mjs';
|
|
3
|
+
|
|
4
|
+
describe('SafeInt', () => {
|
|
5
|
+
describe('asSafeInt', () => {
|
|
6
|
+
test('accepts valid safe integers', () => {
|
|
7
|
+
expect(() => asSafeInt(0)).not.toThrow();
|
|
8
|
+
expect(() => asSafeInt(1)).not.toThrow();
|
|
9
|
+
expect(() => asSafeInt(-1)).not.toThrow();
|
|
10
|
+
expect(() => asSafeInt(42)).not.toThrow();
|
|
11
|
+
expect(() => asSafeInt(-42)).not.toThrow();
|
|
12
|
+
expect(() => asSafeInt(Number.MAX_SAFE_INTEGER)).not.toThrow();
|
|
13
|
+
expect(() => asSafeInt(Number.MIN_SAFE_INTEGER)).not.toThrow();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('rejects values outside safe integer range', () => {
|
|
17
|
+
expect(() => asSafeInt(Number.MAX_SAFE_INTEGER + 1)).toThrow(TypeError);
|
|
18
|
+
expect(() => asSafeInt(Number.MIN_SAFE_INTEGER - 1)).toThrow(TypeError);
|
|
19
|
+
expect(() => asSafeInt(Number.MAX_VALUE)).toThrow(TypeError);
|
|
20
|
+
expect(() => asSafeInt(-Number.MAX_VALUE)).toThrow(TypeError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rejects non-integers', () => {
|
|
24
|
+
expect(() => asSafeInt(Number.NaN)).toThrow(TypeError);
|
|
25
|
+
expect(() => asSafeInt(Number.POSITIVE_INFINITY)).toThrow(TypeError);
|
|
26
|
+
expect(() => asSafeInt(Number.NEGATIVE_INFINITY)).toThrow(TypeError);
|
|
27
|
+
expect(() => asSafeInt(1.2)).toThrow(TypeError);
|
|
28
|
+
expect(() => asSafeInt(-3.4)).toThrow(TypeError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('returns the same value for valid inputs', () => {
|
|
32
|
+
expect(asSafeInt(5)).toBe(5);
|
|
33
|
+
expect(asSafeInt(-10)).toBe(-10);
|
|
34
|
+
expect(asSafeInt(0)).toBe(0);
|
|
35
|
+
expect(asSafeInt(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER);
|
|
36
|
+
expect(asSafeInt(Number.MIN_SAFE_INTEGER)).toBe(Number.MIN_SAFE_INTEGER);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test.each([
|
|
40
|
+
{ name: 'Number.NaN', value: Number.NaN },
|
|
41
|
+
{ name: 'Number.POSITIVE_INFINITY', value: Number.POSITIVE_INFINITY },
|
|
42
|
+
{ name: 'Number.NEGATIVE_INFINITY', value: Number.NEGATIVE_INFINITY },
|
|
43
|
+
{ name: '1.2', value: 1.2 },
|
|
44
|
+
{ name: '-3.4', value: -3.4 },
|
|
45
|
+
] as const)(`asSafeInt($name) should throw a TypeError`, ({ value }) => {
|
|
46
|
+
expect(() => asSafeInt(value)).toThrow(
|
|
47
|
+
new TypeError(`Expected a safe integer, got: ${value}`),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('SafeInt.is', () => {
|
|
53
|
+
test('correctly identifies safe integers', () => {
|
|
54
|
+
expect(SafeInt.is(0)).toBe(true);
|
|
55
|
+
expect(SafeInt.is(1)).toBe(true);
|
|
56
|
+
expect(SafeInt.is(-1)).toBe(true);
|
|
57
|
+
expect(SafeInt.is(42)).toBe(true);
|
|
58
|
+
expect(SafeInt.is(-42)).toBe(true);
|
|
59
|
+
expect(SafeInt.is(Number.MAX_SAFE_INTEGER)).toBe(true);
|
|
60
|
+
expect(SafeInt.is(Number.MIN_SAFE_INTEGER)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('correctly identifies values outside safe integer range', () => {
|
|
64
|
+
expect(SafeInt.is(Number.MAX_SAFE_INTEGER + 1)).toBe(false);
|
|
65
|
+
expect(SafeInt.is(Number.MIN_SAFE_INTEGER - 1)).toBe(false);
|
|
66
|
+
expect(SafeInt.is(Number.MAX_VALUE)).toBe(false);
|
|
67
|
+
expect(SafeInt.is(-Number.MAX_VALUE)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('correctly identifies non-integers', () => {
|
|
71
|
+
expect(SafeInt.is(Number.NaN)).toBe(false);
|
|
72
|
+
expect(SafeInt.is(Number.POSITIVE_INFINITY)).toBe(false);
|
|
73
|
+
expect(SafeInt.is(Number.NEGATIVE_INFINITY)).toBe(false);
|
|
74
|
+
expect(SafeInt.is(1.2)).toBe(false);
|
|
75
|
+
expect(SafeInt.is(-3.4)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('constants', () => {
|
|
80
|
+
test('MIN_VALUE and MAX_VALUE', () => {
|
|
81
|
+
expect(SafeInt.MIN_VALUE).toBe(Number.MIN_SAFE_INTEGER);
|
|
82
|
+
expect(SafeInt.MAX_VALUE).toBe(Number.MAX_SAFE_INTEGER);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('mathematical operations', () => {
|
|
87
|
+
const a = asSafeInt(5);
|
|
88
|
+
const b = asSafeInt(2);
|
|
89
|
+
const c = asSafeInt(-3);
|
|
90
|
+
|
|
91
|
+
test('abs', () => {
|
|
92
|
+
expect(SafeInt.abs(a)).toBe(5);
|
|
93
|
+
expect(SafeInt.abs(c)).toBe(3);
|
|
94
|
+
expect(SafeInt.abs(asSafeInt(0))).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('min and max', () => {
|
|
98
|
+
expect(SafeInt.min(a, b)).toBe(2);
|
|
99
|
+
expect(SafeInt.max(a, b)).toBe(5);
|
|
100
|
+
expect(SafeInt.min(a, c)).toBe(-3);
|
|
101
|
+
expect(SafeInt.max(a, c)).toBe(5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('add (with clamping to safe integer range)', () => {
|
|
105
|
+
const largeValue = asSafeInt(Number.MAX_SAFE_INTEGER - 1);
|
|
106
|
+
const result = SafeInt.add(largeValue, asSafeInt(10));
|
|
107
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER); // clamped to max
|
|
108
|
+
expect(SafeInt.add(a, b)).toBe(7);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('sub (with clamping to safe integer range)', () => {
|
|
112
|
+
const smallValue = asSafeInt(Number.MIN_SAFE_INTEGER + 1);
|
|
113
|
+
const result = SafeInt.sub(smallValue, asSafeInt(10));
|
|
114
|
+
expect(result).toBe(Number.MIN_SAFE_INTEGER); // clamped to min
|
|
115
|
+
expect(SafeInt.sub(a, b)).toBe(3);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('mul (with clamping to safe integer range)', () => {
|
|
119
|
+
const largeValue = asSafeInt(
|
|
120
|
+
Math.floor(Math.sqrt(Number.MAX_SAFE_INTEGER)),
|
|
121
|
+
);
|
|
122
|
+
const result = SafeInt.mul(largeValue, largeValue);
|
|
123
|
+
expect(result).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER);
|
|
124
|
+
expect(SafeInt.mul(asSafeInt(10), asSafeInt(5))).toBe(50);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('div (floor division with clamping)', () => {
|
|
128
|
+
expect(SafeInt.div(a, b)).toBe(2);
|
|
129
|
+
expect(SafeInt.div(asSafeInt(7), asSafeInt(3))).toBe(2);
|
|
130
|
+
expect(SafeInt.div(asSafeInt(-7), asSafeInt(3))).toBe(-3);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('pow (with clamping to safe integer range)', () => {
|
|
134
|
+
const result = SafeInt.pow(asSafeInt(1000), asSafeInt(10));
|
|
135
|
+
expect(result).toBe(Number.MAX_SAFE_INTEGER); // clamped to max
|
|
136
|
+
expect(SafeInt.pow(asSafeInt(2), asSafeInt(3))).toBe(8);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('random', () => {
|
|
141
|
+
test('generates safe integers within specified range', () => {
|
|
142
|
+
const min = -20;
|
|
143
|
+
const max = 20;
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < 10; i++) {
|
|
146
|
+
const result = SafeInt.random(min, max);
|
|
147
|
+
expect(result).toBeGreaterThanOrEqual(min);
|
|
148
|
+
expect(result).toBeLessThanOrEqual(max);
|
|
149
|
+
expect(SafeInt.is(result)).toBe(true);
|
|
150
|
+
expect(Number.isInteger(result)).toBe(true);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('generates values within safe integer range', () => {
|
|
155
|
+
for (let i = 0; i < 10; i++) {
|
|
156
|
+
const result = SafeInt.random(-30, 30);
|
|
157
|
+
expect(result).toBeGreaterThanOrEqual(Number.MIN_SAFE_INTEGER);
|
|
158
|
+
expect(result).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('type assertions', () => {
|
|
164
|
+
test('type relationships', () => {
|
|
165
|
+
expectType<SafeInt, number>('<=');
|
|
166
|
+
|
|
167
|
+
expectTypeOf(asSafeInt(5)).toExtend<SafeInt>();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|