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,297 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { pipe } from '../functional/index.mjs';
|
|
3
|
+
import { mapNullable } from './map-nullable.mjs';
|
|
4
|
+
|
|
5
|
+
describe('mapNullable', () => {
|
|
6
|
+
describe('regular usage', () => {
|
|
7
|
+
test('should apply function to non-null value', () => {
|
|
8
|
+
const result = mapNullable('hello', (s) => s.toUpperCase());
|
|
9
|
+
expect(result).toBe('HELLO');
|
|
10
|
+
expectType<typeof result, string | undefined>('=');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('should apply function to non-undefined value', () => {
|
|
14
|
+
const result = mapNullable(42, (n) => n * 2);
|
|
15
|
+
expect(result).toBe(84);
|
|
16
|
+
expectType<typeof result, number | undefined>('=');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should return undefined for null input', () => {
|
|
20
|
+
const result = mapNullable(null, (s: string) => s.toUpperCase());
|
|
21
|
+
expect(result).toBeUndefined();
|
|
22
|
+
expectType<typeof result, string | undefined>('=');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should return undefined for undefined input', () => {
|
|
26
|
+
const result = mapNullable(undefined, (s: string) => s.toUpperCase());
|
|
27
|
+
expect(result).toBeUndefined();
|
|
28
|
+
expectType<typeof result, string | undefined>('=');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should work with complex transformations', () => {
|
|
32
|
+
const user = { name: 'Alice', age: 30 };
|
|
33
|
+
const result = mapNullable(
|
|
34
|
+
user,
|
|
35
|
+
(u) => `${u.name} is ${u.age} years old`,
|
|
36
|
+
);
|
|
37
|
+
expect(result).toBe('Alice is 30 years old');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should work with nullable object properties', () => {
|
|
41
|
+
const user: { name?: string } = { name: 'Bob' };
|
|
42
|
+
const result = mapNullable(user.name, (name) => name.toUpperCase());
|
|
43
|
+
expect(result).toBe('BOB');
|
|
44
|
+
|
|
45
|
+
const userWithoutName: { name?: string } = {};
|
|
46
|
+
const resultEmpty = mapNullable(userWithoutName.name, (name) =>
|
|
47
|
+
name.toUpperCase(),
|
|
48
|
+
);
|
|
49
|
+
expect(resultEmpty).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('curried usage', () => {
|
|
54
|
+
test('should support curried form with string transformation', () => {
|
|
55
|
+
const toUpperCase = mapNullable((s: string) => s.toUpperCase());
|
|
56
|
+
|
|
57
|
+
const result1 = toUpperCase('hello');
|
|
58
|
+
expect(result1).toBe('HELLO');
|
|
59
|
+
|
|
60
|
+
const result2 = toUpperCase(null);
|
|
61
|
+
expect(result2).toBeUndefined();
|
|
62
|
+
|
|
63
|
+
const result3 = toUpperCase(undefined);
|
|
64
|
+
expect(result3).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should support curried form with number transformation', () => {
|
|
68
|
+
const double = mapNullable((n: number) => n * 2);
|
|
69
|
+
|
|
70
|
+
const result1 = double(21);
|
|
71
|
+
expect(result1).toBe(42);
|
|
72
|
+
|
|
73
|
+
const result2 = double(null);
|
|
74
|
+
expect(result2).toBeUndefined();
|
|
75
|
+
|
|
76
|
+
const result3 = double(undefined);
|
|
77
|
+
expect(result3).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should support curried form with object transformation', () => {
|
|
81
|
+
const getName = mapNullable(
|
|
82
|
+
(u: Readonly<{ name: string; age: number }>) => u.name,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const user = { name: 'Charlie', age: 25 };
|
|
86
|
+
const result1 = getName(user);
|
|
87
|
+
expect(result1).toBe('Charlie');
|
|
88
|
+
|
|
89
|
+
const result2 = getName(null);
|
|
90
|
+
expect(result2).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should work with pipe composition', () => {
|
|
94
|
+
const toUpperCase = mapNullable((s: string) => s.toUpperCase());
|
|
95
|
+
const addGreeting = mapNullable((s: string) => `Hello, ${s}!`);
|
|
96
|
+
|
|
97
|
+
const result = pipe('world').map(toUpperCase).map(addGreeting).value;
|
|
98
|
+
|
|
99
|
+
expect(result).toBe('Hello, WORLD!');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should handle null values in pipe composition', () => {
|
|
103
|
+
const toUpperCase = mapNullable((s: string) => s.toUpperCase());
|
|
104
|
+
const addGreeting = mapNullable((s: string) => `Hello, ${s}!`);
|
|
105
|
+
|
|
106
|
+
const result = pipe(null as string | null)
|
|
107
|
+
.map(toUpperCase)
|
|
108
|
+
.map(addGreeting).value;
|
|
109
|
+
|
|
110
|
+
expect(result).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should work with multiple transformations in pipe', () => {
|
|
114
|
+
const toStr = mapNullable((n: number) => n.toString());
|
|
115
|
+
const addPrefix = mapNullable((s: string) => `Number: ${s}`);
|
|
116
|
+
const toUpperCase = mapNullable((s: string) => s.toUpperCase());
|
|
117
|
+
|
|
118
|
+
const result = pipe(42 as number | null)
|
|
119
|
+
.map(toStr)
|
|
120
|
+
.map(addPrefix)
|
|
121
|
+
.map(toUpperCase).value;
|
|
122
|
+
|
|
123
|
+
expect(result).toBe('NUMBER: 42');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('edge cases and type safety', () => {
|
|
128
|
+
test('should preserve type information', () => {
|
|
129
|
+
expectTypeOf(mapNullable('test', (s) => s.length)).toEqualTypeOf<
|
|
130
|
+
number | undefined
|
|
131
|
+
>();
|
|
132
|
+
|
|
133
|
+
expectTypeOf(mapNullable(42, (n) => n.toString())).toEqualTypeOf<
|
|
134
|
+
string | undefined
|
|
135
|
+
>();
|
|
136
|
+
|
|
137
|
+
expectTypeOf(mapNullable(true, (b) => !b)).toEqualTypeOf<
|
|
138
|
+
boolean | undefined
|
|
139
|
+
>();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('should work with zero values', () => {
|
|
143
|
+
const result = mapNullable(0, (n) => n + 1);
|
|
144
|
+
expect(result).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should work with empty string', () => {
|
|
148
|
+
const result = mapNullable('', (s) => s.length);
|
|
149
|
+
expect(result).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should work with false boolean', () => {
|
|
153
|
+
const result = mapNullable(false, (b) => !b);
|
|
154
|
+
expect(result).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should work with arrays', () => {
|
|
158
|
+
const result = mapNullable([1, 2, 3], (arr) => arr.length);
|
|
159
|
+
expect(result).toBe(3);
|
|
160
|
+
|
|
161
|
+
const nullResult = mapNullable(
|
|
162
|
+
null as number[] | null,
|
|
163
|
+
(arr) => arr.length,
|
|
164
|
+
);
|
|
165
|
+
expect(nullResult).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should work with nested objects', () => {
|
|
169
|
+
const data = {
|
|
170
|
+
user: { profile: { name: 'Alice' } },
|
|
171
|
+
settings: { theme: 'dark' },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = mapNullable(data, (d) => d.user.profile.name);
|
|
175
|
+
expect(result).toBe('Alice');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('chaining operations', () => {
|
|
180
|
+
test('should chain multiple mapNullable operations', () => {
|
|
181
|
+
const getValue = (): string | null => 'hello';
|
|
182
|
+
|
|
183
|
+
const step1 = mapNullable(getValue(), (s) => s.toUpperCase());
|
|
184
|
+
const step2 = mapNullable(step1, (s) => s.length);
|
|
185
|
+
const step3 = mapNullable(step2, (n) => n * 2);
|
|
186
|
+
|
|
187
|
+
expect(step3).toBe(10); // 'HELLO'.length * 2 = 5 * 2 = 10
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('should short-circuit on null in chain', () => {
|
|
191
|
+
const getValue = (): string | null => null;
|
|
192
|
+
|
|
193
|
+
const step1 = mapNullable(getValue(), (s) => s.toUpperCase());
|
|
194
|
+
const step2 = mapNullable(step1, (s) => s.length);
|
|
195
|
+
const step3 = mapNullable(step2, (n) => n * 2);
|
|
196
|
+
|
|
197
|
+
expect(step1).toBeUndefined();
|
|
198
|
+
expect(step2).toBeUndefined();
|
|
199
|
+
expect(step3).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should work with curried functions in chain', () => {
|
|
203
|
+
const toUpperCase = mapNullable((s: string) => s.toUpperCase());
|
|
204
|
+
const getLength = mapNullable((s: string) => s.length);
|
|
205
|
+
const double = mapNullable((n: number) => n * 2);
|
|
206
|
+
|
|
207
|
+
const input1 = 'hello';
|
|
208
|
+
const result1 = double(getLength(toUpperCase(input1)));
|
|
209
|
+
expect(result1).toBe(10);
|
|
210
|
+
|
|
211
|
+
const input2: string | null = null;
|
|
212
|
+
const result2 = double(getLength(toUpperCase(input2)));
|
|
213
|
+
expect(result2).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('practical use cases', () => {
|
|
218
|
+
test('should work with API response handling', () => {
|
|
219
|
+
type ApiResponse = DeepReadonly<{
|
|
220
|
+
data?: {
|
|
221
|
+
user?: {
|
|
222
|
+
name?: string;
|
|
223
|
+
email?: string;
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
}>;
|
|
227
|
+
|
|
228
|
+
const response: ApiResponse = {
|
|
229
|
+
data: {
|
|
230
|
+
user: {
|
|
231
|
+
name: 'John Doe',
|
|
232
|
+
email: 'john@example.com',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const extractUserName = mapNullable(
|
|
238
|
+
(r: ApiResponse) => r.data?.user?.name,
|
|
239
|
+
);
|
|
240
|
+
const formatName = mapNullable((name: string) => `Mr. ${name}`);
|
|
241
|
+
|
|
242
|
+
const userName = extractUserName(response);
|
|
243
|
+
const formattedName = formatName(userName);
|
|
244
|
+
|
|
245
|
+
expect(formattedName).toBe('Mr. John Doe');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('should work with form data processing', () => {
|
|
249
|
+
type FormData = Readonly<{
|
|
250
|
+
email?: string;
|
|
251
|
+
age?: string;
|
|
252
|
+
}>;
|
|
253
|
+
|
|
254
|
+
const formData: FormData = {
|
|
255
|
+
email: 'test@example.com',
|
|
256
|
+
age: '25',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const extractAge = mapNullable((data: FormData) => data.age);
|
|
260
|
+
const parseAge = mapNullable((ageStr: string) =>
|
|
261
|
+
Number.parseInt(ageStr, 10),
|
|
262
|
+
);
|
|
263
|
+
const validateAge = mapNullable((age: number) =>
|
|
264
|
+
age >= 18 ? age : null,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const extractedAge = extractAge(formData);
|
|
268
|
+
const parsedAge = parseAge(extractedAge);
|
|
269
|
+
const validAge = validateAge(parsedAge);
|
|
270
|
+
|
|
271
|
+
expect(validAge).toBe(25);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('should handle missing data gracefully', () => {
|
|
275
|
+
type FormData = Readonly<{
|
|
276
|
+
email?: string;
|
|
277
|
+
age?: string;
|
|
278
|
+
}>;
|
|
279
|
+
|
|
280
|
+
const incompleteFormData: FormData = {
|
|
281
|
+
email: 'test@example.com',
|
|
282
|
+
// age is missing
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const extractAge = mapNullable((data: FormData) => data.age);
|
|
286
|
+
const parseAge = mapNullable((ageStr: string) =>
|
|
287
|
+
Number.parseInt(ageStr, 10),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const extractedAge = extractAge(incompleteFormData);
|
|
291
|
+
const parsedAge = parseAge(extractedAge);
|
|
292
|
+
|
|
293
|
+
expect(extractedAge).toBeUndefined();
|
|
294
|
+
expect(parsedAge).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a memoized version of a function that caches results based on input arguments.
|
|
3
|
+
*
|
|
4
|
+
* The memoized function stores results in an internal Map and returns cached values
|
|
5
|
+
* for repeated calls with the same arguments. This can significantly improve performance
|
|
6
|
+
* for expensive computations or I/O operations.
|
|
7
|
+
*
|
|
8
|
+
* **Important considerations:**
|
|
9
|
+
* - The cache grows unbounded - consider memory implications for long-running applications
|
|
10
|
+
* - Cache keys must be primitives (string, number, boolean, symbol, null, undefined, bigint)
|
|
11
|
+
* - Object arguments require careful key generation to ensure uniqueness
|
|
12
|
+
* - Pure functions only - memoizing functions with side effects can lead to bugs
|
|
13
|
+
*
|
|
14
|
+
* @template A - The tuple type of the function arguments
|
|
15
|
+
* @template R - The return type of the function
|
|
16
|
+
* @template K - The primitive type used as the cache key (must be valid Map key)
|
|
17
|
+
* @param fn - The pure function to memoize
|
|
18
|
+
* @param argsToCacheKey - Function that converts arguments to a unique cache key
|
|
19
|
+
* @returns A memoized version of the input function with the same signature
|
|
20
|
+
*
|
|
21
|
+
* @example Basic memoization for expensive calculations
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Fibonacci calculation (exponential time complexity)
|
|
24
|
+
* const fibonacci = (n: number): number => {
|
|
25
|
+
* console.log(`Computing fib(${n})`);
|
|
26
|
+
* if (n <= 1) return n;
|
|
27
|
+
* return fibonacci(n - 1) + fibonacci(n - 2);
|
|
28
|
+
* };
|
|
29
|
+
*
|
|
30
|
+
* const memoizedFib = memoizeFunction(
|
|
31
|
+
* fibonacci,
|
|
32
|
+
* (n) => n // Number itself as key
|
|
33
|
+
* );
|
|
34
|
+
*
|
|
35
|
+
* memoizedFib(40); // Much faster than unmemoized version
|
|
36
|
+
* memoizedFib(40); // Returns instantly from cache
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example Multi-argument functions with composite keys
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // Grid calculation with x,y coordinates
|
|
42
|
+
* const calculateGridValue = (x: number, y: number, scale: number): number => {
|
|
43
|
+
* console.log(`Computing grid(${x},${y},${scale})`);
|
|
44
|
+
* // Expensive computation...
|
|
45
|
+
* return Math.sin(x * scale) * Math.cos(y * scale);
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* const memoizedGrid = memoizeFunction(
|
|
49
|
+
* calculateGridValue,
|
|
50
|
+
* (x, y, scale) => `${x},${y},${scale}` // String concatenation for composite key
|
|
51
|
+
* );
|
|
52
|
+
*
|
|
53
|
+
* // Alternative: Using bit manipulation for integer coordinates
|
|
54
|
+
* const memoizedGrid2 = memoizeFunction(
|
|
55
|
+
* calculateGridValue,
|
|
56
|
+
* (x, y, scale) => (x << 20) | (y << 10) | scale // Assuming small positive integers
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example Object arguments with selective memoization
|
|
61
|
+
* ```typescript
|
|
62
|
+
* interface User {
|
|
63
|
+
* id: number;
|
|
64
|
+
* name: string;
|
|
65
|
+
* email: string;
|
|
66
|
+
* metadata?: Record<string, unknown>;
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* const fetchUserPermissions = async (user: User): Promise<string[]> => {
|
|
70
|
+
* console.log(`Fetching permissions for user ${user.id}`);
|
|
71
|
+
* const response = await api.get(`/permissions/${user.id}`);
|
|
72
|
+
* return response.data;
|
|
73
|
+
* };
|
|
74
|
+
*
|
|
75
|
+
* // Memoize based only on user ID, ignoring other fields
|
|
76
|
+
* const memoizedFetchPermissions = memoizeFunction(
|
|
77
|
+
* fetchUserPermissions,
|
|
78
|
+
* (user) => user.id // Only cache by ID
|
|
79
|
+
* );
|
|
80
|
+
*
|
|
81
|
+
* // For multiple identifying fields
|
|
82
|
+
* const processUserData = (user: User, orgId: number): ProcessedData => {
|
|
83
|
+
* // Complex processing...
|
|
84
|
+
* };
|
|
85
|
+
*
|
|
86
|
+
* const memoizedProcess = memoizeFunction(
|
|
87
|
+
* processUserData,
|
|
88
|
+
* (user, orgId) => `${user.id}:${orgId}` // Composite key with separator
|
|
89
|
+
* );
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example Memoizing recursive functions
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Recursive path finding
|
|
95
|
+
* const findPaths = (start: string, end: string, visited: Set<string> = new Set()): string[][] => {
|
|
96
|
+
* if (start === end) return [[end]];
|
|
97
|
+
* // ... complex recursive logic
|
|
98
|
+
* };
|
|
99
|
+
*
|
|
100
|
+
* // Use sorted, serialized visited set for consistent keys
|
|
101
|
+
* const memoizedFindPaths = memoizeFunction(
|
|
102
|
+
* findPaths,
|
|
103
|
+
* (start, end, visited = new Set()) =>
|
|
104
|
+
* `${start}->${end}:[${[...visited].sort().join(',')}]`
|
|
105
|
+
* );
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @example Cache key strategies
|
|
109
|
+
* ```typescript
|
|
110
|
+
* // 1. Simple primitive argument
|
|
111
|
+
* memoizeFunction(fn, (x: number) => x);
|
|
112
|
+
*
|
|
113
|
+
* // 2. Multiple arguments with separator
|
|
114
|
+
* memoizeFunction(fn, (a: string, b: number) => `${a}|${b}`);
|
|
115
|
+
*
|
|
116
|
+
* // 3. Object with specific fields
|
|
117
|
+
* memoizeFunction(fn, (obj: { id: number; version: number }) =>
|
|
118
|
+
* `${obj.id}:v${obj.version}`
|
|
119
|
+
* );
|
|
120
|
+
*
|
|
121
|
+
* // 4. Array argument with JSON serialization
|
|
122
|
+
* memoizeFunction(fn, (arr: number[]) => JSON.stringify(arr));
|
|
123
|
+
*
|
|
124
|
+
* // 5. Boolean flags as bit field
|
|
125
|
+
* memoizeFunction(fn, (a: boolean, b: boolean, c: boolean) =>
|
|
126
|
+
* (a ? 4 : 0) | (b ? 2 : 0) | (c ? 1 : 0)
|
|
127
|
+
* );
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example Memory-conscious memoization with weak references
|
|
131
|
+
* ```typescript
|
|
132
|
+
* // For object keys, consider using WeakMap externally
|
|
133
|
+
* const cache = new WeakMap<object, Result>();
|
|
134
|
+
*
|
|
135
|
+
* function memoizeWithWeakMap<T extends object, R>(
|
|
136
|
+
* fn: (obj: T) => R
|
|
137
|
+
* ): (obj: T) => R {
|
|
138
|
+
* return (obj: T): R => {
|
|
139
|
+
* if (cache.has(obj)) {
|
|
140
|
+
* return cache.get(obj)!;
|
|
141
|
+
* }
|
|
142
|
+
* const result = fn(obj);
|
|
143
|
+
* cache.set(obj, result);
|
|
144
|
+
* return result;
|
|
145
|
+
* };
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @example Anti-patterns to avoid
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // ❌ Bad: Memoizing impure functions
|
|
152
|
+
* const memoizedRandom = memoizeFunction(
|
|
153
|
+
* () => Math.random(),
|
|
154
|
+
* () => 'key' // Always returns cached random value!
|
|
155
|
+
* );
|
|
156
|
+
*
|
|
157
|
+
* // ❌ Bad: Memoizing functions with side effects
|
|
158
|
+
* const memoizedLog = memoizeFunction(
|
|
159
|
+
* (msg: string) => { console.log(msg); return msg; },
|
|
160
|
+
* (msg) => msg // Logs only on first call!
|
|
161
|
+
* );
|
|
162
|
+
*
|
|
163
|
+
* // ❌ Bad: Non-unique cache keys
|
|
164
|
+
* const memoizedProcess = memoizeFunction(
|
|
165
|
+
* (user: User) => processUser(user),
|
|
166
|
+
* (user) => user.name // Multiple users can have same name!
|
|
167
|
+
* );
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @see https://en.wikipedia.org/wiki/Memoization
|
|
171
|
+
*/
|
|
172
|
+
export const memoizeFunction = <
|
|
173
|
+
const A extends readonly unknown[],
|
|
174
|
+
R,
|
|
175
|
+
K extends Primitive,
|
|
176
|
+
>(
|
|
177
|
+
fn: (...args: A) => R,
|
|
178
|
+
argsToCacheKey: (...args: A) => K,
|
|
179
|
+
): ((...args: A) => R) => {
|
|
180
|
+
const mut_cache = new Map<K, R>();
|
|
181
|
+
|
|
182
|
+
return (...args: A): R => {
|
|
183
|
+
const key = argsToCacheKey(...args);
|
|
184
|
+
|
|
185
|
+
if (mut_cache.has(key)) {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
187
|
+
return mut_cache.get(key)!;
|
|
188
|
+
} else {
|
|
189
|
+
const result = fn(...args);
|
|
190
|
+
|
|
191
|
+
mut_cache.set(key, result);
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
|
|
2
|
+
import { memoizeFunction } from './memoize-function.mjs';
|
|
3
|
+
|
|
4
|
+
describe('memoizeFunction', () => {
|
|
5
|
+
test('should cache results for the same arguments', () => {
|
|
6
|
+
const mockFn = vi.fn((x: number) => x * 2);
|
|
7
|
+
const memoized = memoizeFunction(mockFn, (x) => x);
|
|
8
|
+
|
|
9
|
+
// First call
|
|
10
|
+
expect(memoized(5)).toBe(10);
|
|
11
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
12
|
+
|
|
13
|
+
// Second call with same argument - should use cache
|
|
14
|
+
expect(memoized(5)).toBe(10);
|
|
15
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
16
|
+
|
|
17
|
+
// Call with different argument
|
|
18
|
+
expect(memoized(3)).toBe(6);
|
|
19
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should work with multiple arguments', () => {
|
|
23
|
+
const mockFn = vi.fn((a: number, b: number) => a + b);
|
|
24
|
+
const memoized = memoizeFunction(mockFn, (a, b) => `${a},${b}`);
|
|
25
|
+
|
|
26
|
+
expect(memoized(2, 3)).toBe(5);
|
|
27
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
28
|
+
|
|
29
|
+
expect(memoized(2, 3)).toBe(5);
|
|
30
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
31
|
+
|
|
32
|
+
expect(memoized(3, 2)).toBe(5);
|
|
33
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should handle functions that return undefined', () => {
|
|
37
|
+
const mockFn = vi.fn((_x: number) => undefined);
|
|
38
|
+
const memoized = memoizeFunction(mockFn, (x) => x);
|
|
39
|
+
|
|
40
|
+
expect(memoized(5)).toBe(undefined);
|
|
41
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
42
|
+
|
|
43
|
+
// Should use cache even for undefined
|
|
44
|
+
expect(memoized(5)).toBe(undefined);
|
|
45
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should work with object arguments using primitive cache keys', () => {
|
|
49
|
+
type User = Readonly<{ id: number; name: string }>;
|
|
50
|
+
const mockFn = vi.fn((user: User) => `Hello ${user.name}`);
|
|
51
|
+
const memoized = memoizeFunction(mockFn, (user) => user.id);
|
|
52
|
+
|
|
53
|
+
const user1 = { id: 1, name: 'Alice' };
|
|
54
|
+
const user2 = { id: 1, name: 'Bob' }; // Same id, different name
|
|
55
|
+
const user3 = { id: 2, name: 'Charlie' };
|
|
56
|
+
|
|
57
|
+
expect(memoized(user1)).toBe('Hello Alice');
|
|
58
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
59
|
+
|
|
60
|
+
// Same id, should use cache (even though name is different)
|
|
61
|
+
expect(memoized(user2)).toBe('Hello Alice');
|
|
62
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
63
|
+
|
|
64
|
+
// Different id, should call function
|
|
65
|
+
expect(memoized(user3)).toBe('Hello Charlie');
|
|
66
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should work with different cache key types', () => {
|
|
70
|
+
// Number key
|
|
71
|
+
const withNumber = memoizeFunction(
|
|
72
|
+
(x: string) => x.length,
|
|
73
|
+
(x) => x.length,
|
|
74
|
+
);
|
|
75
|
+
expect(withNumber('hello')).toBe(5);
|
|
76
|
+
expect(withNumber('world')).toBe(5); // Same length, uses cache
|
|
77
|
+
|
|
78
|
+
// Boolean key
|
|
79
|
+
const withBoolean = memoizeFunction(
|
|
80
|
+
(x: number) => x * 2,
|
|
81
|
+
(x) => x > 0,
|
|
82
|
+
);
|
|
83
|
+
expect(withBoolean(5)).toBe(10);
|
|
84
|
+
expect(withBoolean(3)).toBe(10); // Both positive, uses cache
|
|
85
|
+
expect(withBoolean(-2)).toBe(-4); // Negative, new cache entry
|
|
86
|
+
|
|
87
|
+
// Symbol key
|
|
88
|
+
const sym1 = Symbol('test');
|
|
89
|
+
const sym2 = Symbol('test');
|
|
90
|
+
const withSymbol = memoizeFunction(
|
|
91
|
+
(_s: symbol) => Math.random(),
|
|
92
|
+
(s) => s,
|
|
93
|
+
);
|
|
94
|
+
const result1 = withSymbol(sym1);
|
|
95
|
+
expect(withSymbol(sym1)).toBe(result1); // Same symbol, uses cache
|
|
96
|
+
expect(withSymbol(sym2)).not.toBe(result1); // Different symbol
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should handle null and undefined cache keys', () => {
|
|
100
|
+
const mockFn = vi.fn((x: string | null | undefined) => x ?? 'default');
|
|
101
|
+
const memoized = memoizeFunction(mockFn, (x) => x);
|
|
102
|
+
|
|
103
|
+
expect(memoized(null)).toBe('default');
|
|
104
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
105
|
+
|
|
106
|
+
expect(memoized(null)).toBe('default');
|
|
107
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
expect(memoized(undefined)).toBe('default');
|
|
110
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
111
|
+
|
|
112
|
+
expect(memoized(undefined)).toBe('default');
|
|
113
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should maintain separate caches for different memoized functions', () => {
|
|
117
|
+
const fn1 = vi.fn((x: number) => x * 2);
|
|
118
|
+
const fn2 = vi.fn((x: number) => x * 3);
|
|
119
|
+
|
|
120
|
+
const memoized1 = memoizeFunction(fn1, (x) => x);
|
|
121
|
+
const memoized2 = memoizeFunction(fn2, (x) => x);
|
|
122
|
+
|
|
123
|
+
expect(memoized1(5)).toBe(10);
|
|
124
|
+
expect(memoized2(5)).toBe(15);
|
|
125
|
+
|
|
126
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(fn2).toHaveBeenCalledTimes(1);
|
|
128
|
+
|
|
129
|
+
// Each has its own cache
|
|
130
|
+
expect(memoized1(5)).toBe(10);
|
|
131
|
+
expect(memoized2(5)).toBe(15);
|
|
132
|
+
|
|
133
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(fn2).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should work with complex cache key generation', () => {
|
|
138
|
+
type Args = Readonly<{
|
|
139
|
+
category: string;
|
|
140
|
+
subcategory: string;
|
|
141
|
+
id: number;
|
|
142
|
+
}>;
|
|
143
|
+
|
|
144
|
+
const mockFn = vi.fn(
|
|
145
|
+
(args: Args) => `${args.category}/${args.subcategory}/${args.id}`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const memoized = memoizeFunction(
|
|
149
|
+
mockFn,
|
|
150
|
+
(args) => `${args.category}:${args.subcategory}:${args.id}`,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const args1 = { category: 'books', subcategory: 'fiction', id: 123 };
|
|
154
|
+
const args2 = { category: 'books', subcategory: 'fiction', id: 123 };
|
|
155
|
+
const args3 = { category: 'books', subcategory: 'fiction', id: 124 };
|
|
156
|
+
|
|
157
|
+
expect(memoized(args1)).toBe('books/fiction/123');
|
|
158
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
// Same cache key, should use cache
|
|
161
|
+
expect(memoized(args2)).toBe('books/fiction/123');
|
|
162
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
163
|
+
|
|
164
|
+
// Different id, different cache key
|
|
165
|
+
expect(memoized(args3)).toBe('books/fiction/124');
|
|
166
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
167
|
+
});
|
|
168
|
+
});
|