@nlozgachev/pipelined 0.10.0 → 0.12.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 (76) hide show
  1. package/README.md +9 -3
  2. package/esm/src/Core/Option.js +2 -1
  3. package/esm/src/Core/RemoteData.js +5 -5
  4. package/esm/src/Core/TaskValidation.js +1 -3
  5. package/esm/src/Core/Tuple.js +112 -0
  6. package/esm/src/Core/index.js +4 -5
  7. package/esm/src/{Core → Utils}/Arr.js +95 -23
  8. package/esm/src/Utils/Dict.js +421 -0
  9. package/esm/src/Utils/Num.js +124 -0
  10. package/esm/src/{Core → Utils}/Rec.js +85 -11
  11. package/esm/src/Utils/Str.js +134 -0
  12. package/esm/src/Utils/Uniq.js +265 -0
  13. package/esm/src/Utils/index.js +6 -0
  14. package/package.json +11 -1
  15. package/script/src/Core/Option.js +2 -1
  16. package/script/src/Core/RemoteData.js +5 -5
  17. package/script/src/Core/TaskValidation.js +1 -3
  18. package/script/src/Core/Tuple.js +115 -0
  19. package/script/src/Core/index.js +4 -5
  20. package/script/src/{Core → Utils}/Arr.js +95 -23
  21. package/script/src/Utils/Dict.js +424 -0
  22. package/script/src/Utils/Num.js +127 -0
  23. package/script/src/{Core → Utils}/Rec.js +85 -11
  24. package/script/src/Utils/Str.js +137 -0
  25. package/script/src/Utils/Uniq.js +268 -0
  26. package/script/src/Utils/index.js +22 -0
  27. package/types/src/Composition/compose.d.ts.map +1 -1
  28. package/types/src/Composition/converge.d.ts.map +1 -1
  29. package/types/src/Composition/curry.d.ts.map +1 -1
  30. package/types/src/Composition/flow.d.ts.map +1 -1
  31. package/types/src/Composition/fn.d.ts.map +1 -1
  32. package/types/src/Composition/juxt.d.ts.map +1 -1
  33. package/types/src/Composition/memoize.d.ts.map +1 -1
  34. package/types/src/Composition/not.d.ts.map +1 -1
  35. package/types/src/Composition/on.d.ts.map +1 -1
  36. package/types/src/Composition/pipe.d.ts.map +1 -1
  37. package/types/src/Composition/uncurry.d.ts.map +1 -1
  38. package/types/src/Core/Deferred.d.ts.map +1 -1
  39. package/types/src/Core/Lens.d.ts.map +1 -1
  40. package/types/src/Core/Logged.d.ts.map +1 -1
  41. package/types/src/Core/Option.d.ts.map +1 -1
  42. package/types/src/Core/Optional.d.ts.map +1 -1
  43. package/types/src/Core/Predicate.d.ts.map +1 -1
  44. package/types/src/Core/Reader.d.ts.map +1 -1
  45. package/types/src/Core/Refinement.d.ts.map +1 -1
  46. package/types/src/Core/RemoteData.d.ts.map +1 -1
  47. package/types/src/Core/Result.d.ts.map +1 -1
  48. package/types/src/Core/State.d.ts.map +1 -1
  49. package/types/src/Core/Task.d.ts.map +1 -1
  50. package/types/src/Core/TaskOption.d.ts.map +1 -1
  51. package/types/src/Core/TaskResult.d.ts.map +1 -1
  52. package/types/src/Core/TaskValidation.d.ts.map +1 -1
  53. package/types/src/Core/These.d.ts.map +1 -1
  54. package/types/src/Core/Tuple.d.ts +129 -0
  55. package/types/src/Core/Tuple.d.ts.map +1 -0
  56. package/types/src/Core/Validation.d.ts.map +1 -1
  57. package/types/src/Core/index.d.ts +4 -5
  58. package/types/src/Core/index.d.ts.map +1 -1
  59. package/types/src/Types/Brand.d.ts.map +1 -1
  60. package/types/src/Types/NonEmptyList.d.ts.map +1 -1
  61. package/types/src/{Core → Utils}/Arr.d.ts +25 -3
  62. package/types/src/Utils/Arr.d.ts.map +1 -0
  63. package/types/src/Utils/Dict.d.ts +310 -0
  64. package/types/src/Utils/Dict.d.ts.map +1 -0
  65. package/types/src/Utils/Num.d.ts +110 -0
  66. package/types/src/Utils/Num.d.ts.map +1 -0
  67. package/types/src/{Core → Utils}/Rec.d.ts +39 -1
  68. package/types/src/Utils/Rec.d.ts.map +1 -0
  69. package/types/src/Utils/Str.d.ts +128 -0
  70. package/types/src/Utils/Str.d.ts.map +1 -0
  71. package/types/src/Utils/Uniq.d.ts +179 -0
  72. package/types/src/Utils/Uniq.d.ts.map +1 -0
  73. package/types/src/Utils/index.d.ts +7 -0
  74. package/types/src/Utils/index.d.ts.map +1 -0
  75. package/types/src/Core/Arr.d.ts.map +0 -1
  76. package/types/src/Core/Rec.d.ts.map +0 -1
@@ -0,0 +1,421 @@
1
+ import { Option } from "../Core/Option.js";
2
+ /**
3
+ * Functional utilities for key-value dictionaries (`ReadonlyMap<K, V>`). All functions are pure
4
+ * and data-last — they compose naturally with `pipe`.
5
+ *
6
+ * Unlike plain objects (`Rec`), dictionaries support any key type, preserve insertion order, and
7
+ * make membership checks explicit via `lookup` returning `Option`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { Dict } from "@nlozgachev/pipelined/utils";
12
+ * import { pipe } from "@nlozgachev/pipelined/composition";
13
+ *
14
+ * const scores = pipe(
15
+ * Dict.fromEntries([["alice", 10], ["bob", 8], ["carol", 10]] as const),
16
+ * Dict.filter(n => n >= 10),
17
+ * Dict.map(n => `${n} points`),
18
+ * );
19
+ * // ReadonlyMap { "alice" => "10 points", "carol" => "10 points" }
20
+ * ```
21
+ */
22
+ export var Dict;
23
+ (function (Dict) {
24
+ // ---------------------------------------------------------------------------
25
+ // Constructors
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Creates an empty dictionary.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * Dict.empty<string, number>(); // ReadonlyMap {}
33
+ * ```
34
+ */
35
+ Dict.empty = () => new globalThis.Map();
36
+ /**
37
+ * Creates a dictionary with a single entry.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * Dict.singleton("name", "Alice"); // ReadonlyMap { "name" => "Alice" }
42
+ * ```
43
+ */
44
+ Dict.singleton = (key, value) => new globalThis.Map([[key, value]]);
45
+ /**
46
+ * Creates a dictionary from an array of key-value pairs.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * Dict.fromEntries([["a", 1], ["b", 2]]); // ReadonlyMap { "a" => 1, "b" => 2 }
51
+ * ```
52
+ */
53
+ Dict.fromEntries = (entries) => new globalThis.Map(entries);
54
+ /**
55
+ * Creates a dictionary from a plain object. Keys are always strings.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * Dict.fromRecord({ a: 1, b: 2 }); // ReadonlyMap { "a" => 1, "b" => 2 }
60
+ * ```
61
+ */
62
+ Dict.fromRecord = (rec) => new globalThis.Map(Object.entries(rec));
63
+ /**
64
+ * Groups elements of an array into a dictionary keyed by the result of `keyFn`. Each key maps
65
+ * to the array of elements that produced it, in insertion order. Uses the native `Map.groupBy`
66
+ * when available, falling back to a manual loop in older environments.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * pipe(
71
+ * [{ name: "alice", role: "admin" }, { name: "bob", role: "viewer" }, { name: "carol", role: "admin" }],
72
+ * Dict.groupBy(user => user.role),
73
+ * );
74
+ * // ReadonlyMap { "admin" => [alice, carol], "viewer" => [bob] }
75
+ * ```
76
+ */
77
+ Dict.groupBy = (keyFn) => (items) => {
78
+ const result = new globalThis.Map();
79
+ for (const item of items) {
80
+ const key = keyFn(item);
81
+ const arr = result.get(key);
82
+ if (arr !== undefined)
83
+ arr.push(item);
84
+ else
85
+ result.set(key, [item]);
86
+ }
87
+ return result;
88
+ };
89
+ // ---------------------------------------------------------------------------
90
+ // Query
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Returns `true` if the dictionary contains the given key.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * pipe(Dict.fromEntries([["a", 1]]), Dict.has("a")); // true
98
+ * pipe(Dict.fromEntries([["a", 1]]), Dict.has("b")); // false
99
+ * ```
100
+ */
101
+ Dict.has = (key) => (m) => m.has(key);
102
+ /**
103
+ * Looks up a value by key, returning `Some(value)` if found and `None` if not.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * pipe(Dict.fromEntries([["a", 1]]), Dict.lookup("a")); // Some(1)
108
+ * pipe(Dict.fromEntries([["a", 1]]), Dict.lookup("b")); // None
109
+ * ```
110
+ */
111
+ Dict.lookup = (key) => (m) => m.has(key) ? Option.some(m.get(key)) : Option.none();
112
+ /**
113
+ * Returns the number of entries in the dictionary.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * Dict.size(Dict.fromEntries([["a", 1], ["b", 2]])); // 2
118
+ * ```
119
+ */
120
+ Dict.size = (m) => m.size;
121
+ /**
122
+ * Returns `true` if the dictionary has no entries.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * Dict.isEmpty(Dict.empty()); // true
127
+ * ```
128
+ */
129
+ Dict.isEmpty = (m) => m.size === 0;
130
+ /**
131
+ * Returns all keys as a readonly array, in insertion order.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * Dict.keys(Dict.fromEntries([["a", 1], ["b", 2]])); // ["a", "b"]
136
+ * ```
137
+ */
138
+ Dict.keys = (m) => [...m.keys()];
139
+ /**
140
+ * Returns all values as a readonly array, in insertion order.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * Dict.values(Dict.fromEntries([["a", 1], ["b", 2]])); // [1, 2]
145
+ * ```
146
+ */
147
+ Dict.values = (m) => [...m.values()];
148
+ /**
149
+ * Returns all key-value pairs as a readonly array of tuples, in insertion order.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * Dict.entries(Dict.fromEntries([["a", 1], ["b", 2]])); // [["a", 1], ["b", 2]]
154
+ * ```
155
+ */
156
+ Dict.entries = (m) => [...m.entries()];
157
+ // ---------------------------------------------------------------------------
158
+ // Modification
159
+ // ---------------------------------------------------------------------------
160
+ /**
161
+ * Returns a new dictionary with the given key set to the given value.
162
+ * If the key already exists, its value is replaced.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * pipe(Dict.fromEntries([["a", 1]]), Dict.insert("b", 2));
167
+ * // ReadonlyMap { "a" => 1, "b" => 2 }
168
+ * ```
169
+ */
170
+ Dict.insert = (key, value) => (m) => {
171
+ const result = new globalThis.Map(m);
172
+ result.set(key, value);
173
+ return result;
174
+ };
175
+ /**
176
+ * Returns a new dictionary with the given key removed.
177
+ * If the key does not exist, the dictionary is returned unchanged.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * pipe(Dict.fromEntries([["a", 1], ["b", 2]]), Dict.remove("a"));
182
+ * // ReadonlyMap { "b" => 2 }
183
+ * ```
184
+ */
185
+ Dict.remove = (key) => (m) => {
186
+ if (!m.has(key))
187
+ return m;
188
+ const result = new globalThis.Map(m);
189
+ result.delete(key);
190
+ return result;
191
+ };
192
+ /**
193
+ * Returns a new dictionary with the value at `key` set by `f`. If the key does not exist,
194
+ * `f` receives `None`. If the key exists, `f` receives `Some(currentValue)`.
195
+ *
196
+ * Useful for incrementing counters, initialising defaults, or conditional updates.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * import { Option } from "@nlozgachev/pipelined/core";
201
+ *
202
+ * const increment = (opt: Option<number>) => Option.getOrElse(() => 0)(opt) + 1;
203
+ * pipe(Dict.fromEntries([["views", 5]]), Dict.upsert("views", increment)); // { views: 6 }
204
+ * pipe(Dict.fromEntries([["views", 5]]), Dict.upsert("likes", increment)); // { views: 5, likes: 1 }
205
+ * ```
206
+ */
207
+ Dict.upsert = (key, f) => (m) => {
208
+ const result = new globalThis.Map(m);
209
+ result.set(key, f(Dict.lookup(key)(m)));
210
+ return result;
211
+ };
212
+ // ---------------------------------------------------------------------------
213
+ // Transform
214
+ // ---------------------------------------------------------------------------
215
+ /**
216
+ * Transforms each value in the dictionary.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * pipe(Dict.fromEntries([["a", 1], ["b", 2]]), Dict.map(n => n * 2));
221
+ * // ReadonlyMap { "a" => 2, "b" => 4 }
222
+ * ```
223
+ */
224
+ Dict.map = (f) => (m) => {
225
+ const result = new globalThis.Map();
226
+ for (const [k, v] of m) {
227
+ result.set(k, f(v));
228
+ }
229
+ return result;
230
+ };
231
+ /**
232
+ * Transforms each value in the dictionary, also receiving the key.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * pipe(Dict.fromEntries([["a", 1], ["b", 2]]), Dict.mapWithKey((k, v) => `${k}:${v}`));
237
+ * // ReadonlyMap { "a" => "a:1", "b" => "b:2" }
238
+ * ```
239
+ */
240
+ Dict.mapWithKey = (f) => (m) => {
241
+ const result = new globalThis.Map();
242
+ for (const [k, v] of m) {
243
+ result.set(k, f(k, v));
244
+ }
245
+ return result;
246
+ };
247
+ /**
248
+ * Returns a new dictionary containing only the entries for which the predicate returns `true`.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * pipe(Dict.fromEntries([["a", 1], ["b", 3], ["c", 0]]), Dict.filter(n => n > 0));
253
+ * // ReadonlyMap { "a" => 1, "b" => 3 }
254
+ * ```
255
+ */
256
+ Dict.filter = (predicate) => (m) => {
257
+ const result = new globalThis.Map();
258
+ for (const [k, v] of m) {
259
+ if (predicate(v))
260
+ result.set(k, v);
261
+ }
262
+ return result;
263
+ };
264
+ /**
265
+ * Returns a new dictionary containing only the entries for which the predicate returns `true`.
266
+ * The predicate also receives the key.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * pipe(Dict.fromEntries([["a", 1], ["b", 2]]), Dict.filterWithKey((k, v) => k !== "a" && v > 0));
271
+ * // ReadonlyMap { "b" => 2 }
272
+ * ```
273
+ */
274
+ Dict.filterWithKey = (predicate) => (m) => {
275
+ const result = new globalThis.Map();
276
+ for (const [k, v] of m) {
277
+ if (predicate(k, v))
278
+ result.set(k, v);
279
+ }
280
+ return result;
281
+ };
282
+ /**
283
+ * Removes all `None` values from a `ReadonlyMap<K, Option<A>>`, returning a plain
284
+ * `ReadonlyMap<K, A>`. Useful when building dictionaries from fallible lookups.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * import { Option } from "@nlozgachev/pipelined/core";
289
+ *
290
+ * Dict.compact(Dict.fromEntries([
291
+ * ["a", Option.some(1)],
292
+ * ["b", Option.none()],
293
+ * ["c", Option.some(3)],
294
+ * ]));
295
+ * // ReadonlyMap { "a" => 1, "c" => 3 }
296
+ * ```
297
+ */
298
+ Dict.compact = (m) => {
299
+ const result = new globalThis.Map();
300
+ for (const [k, v] of m) {
301
+ if (v.kind === "Some")
302
+ result.set(k, v.value);
303
+ }
304
+ return result;
305
+ };
306
+ // ---------------------------------------------------------------------------
307
+ // Combine
308
+ // ---------------------------------------------------------------------------
309
+ /**
310
+ * Merges two dictionaries. When both contain the same key, the value from `other` takes
311
+ * precedence.
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * pipe(
316
+ * Dict.fromEntries([["a", 1], ["b", 2]]),
317
+ * Dict.union(Dict.fromEntries([["b", 3], ["c", 4]])),
318
+ * );
319
+ * // ReadonlyMap { "a" => 1, "b" => 3, "c" => 4 }
320
+ * ```
321
+ */
322
+ Dict.union = (other) => (m) => {
323
+ const result = new globalThis.Map(m);
324
+ for (const [k, v] of other) {
325
+ result.set(k, v);
326
+ }
327
+ return result;
328
+ };
329
+ /**
330
+ * Returns a new dictionary containing only the entries whose keys appear in both dictionaries.
331
+ * Values are taken from the left (base) dictionary.
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * pipe(
336
+ * Dict.fromEntries([["a", 1], ["b", 2], ["c", 3]]),
337
+ * Dict.intersection(Dict.fromEntries([["b", 99], ["c", 0]])),
338
+ * );
339
+ * // ReadonlyMap { "b" => 2, "c" => 3 }
340
+ * ```
341
+ */
342
+ Dict.intersection = (other) => (m) => {
343
+ const result = new globalThis.Map();
344
+ for (const [k, v] of m) {
345
+ if (other.has(k))
346
+ result.set(k, v);
347
+ }
348
+ return result;
349
+ };
350
+ /**
351
+ * Returns a new dictionary containing only the entries whose keys do not appear in `other`.
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * pipe(
356
+ * Dict.fromEntries([["a", 1], ["b", 2], ["c", 3]]),
357
+ * Dict.difference(Dict.fromEntries([["b", 0]])),
358
+ * );
359
+ * // ReadonlyMap { "a" => 1, "c" => 3 }
360
+ * ```
361
+ */
362
+ Dict.difference = (other) => (m) => {
363
+ const result = new globalThis.Map();
364
+ for (const [k, v] of m) {
365
+ if (!other.has(k))
366
+ result.set(k, v);
367
+ }
368
+ return result;
369
+ };
370
+ // ---------------------------------------------------------------------------
371
+ // Fold
372
+ // ---------------------------------------------------------------------------
373
+ /**
374
+ * Folds the dictionary into a single value by applying `f` to each value in insertion order.
375
+ * When you also need the key, use `reduceWithKey`.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * Dict.reduce(0, (acc, value) => acc + value)(
380
+ * Dict.fromEntries([["a", 1], ["b", 2], ["c", 3]])
381
+ * ); // 6
382
+ * ```
383
+ */
384
+ Dict.reduce = (init, f) => (m) => {
385
+ let acc = init;
386
+ for (const v of m.values()) {
387
+ acc = f(acc, v);
388
+ }
389
+ return acc;
390
+ };
391
+ /**
392
+ * Folds the dictionary into a single value by applying `f` to each key-value pair in insertion
393
+ * order.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * Dict.reduceWithKey("", (acc, value, key) => acc + key + ":" + value + " ")(
398
+ * Dict.fromEntries([["a", 1], ["b", 2]])
399
+ * ); // "a:1 b:2 "
400
+ * ```
401
+ */
402
+ Dict.reduceWithKey = (init, f) => (m) => {
403
+ let acc = init;
404
+ for (const [k, v] of m) {
405
+ acc = f(acc, v, k);
406
+ }
407
+ return acc;
408
+ };
409
+ // ---------------------------------------------------------------------------
410
+ // Convert
411
+ // ---------------------------------------------------------------------------
412
+ /**
413
+ * Converts a `ReadonlyMap<string, V>` to a plain object. Only meaningful when keys are strings.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * Dict.toRecord(Dict.fromEntries([["a", 1], ["b", 2]])); // { a: 1, b: 2 }
418
+ * ```
419
+ */
420
+ Dict.toRecord = (m) => Object.fromEntries(m);
421
+ })(Dict || (Dict = {}));
@@ -0,0 +1,124 @@
1
+ import { Option } from "../Core/Option.js";
2
+ /**
3
+ * Number utilities for common operations. All transformation functions are data-last
4
+ * and curried so they compose naturally with `pipe` and `Arr.map`.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Num } from "@nlozgachev/pipelined/utils";
9
+ * import { pipe } from "@nlozgachev/pipelined/composition";
10
+ *
11
+ * pipe(
12
+ * Num.range(1, 6),
13
+ * Arr.map(Num.multiply(2)),
14
+ * Arr.filter(Num.between(4, 8))
15
+ * ); // [4, 6, 8]
16
+ * ```
17
+ */
18
+ export var Num;
19
+ (function (Num) {
20
+ /**
21
+ * Generates an array of numbers from `from` to `to` (both inclusive),
22
+ * stepping by `step` (default `1`). If `step` is negative or zero, or `from > to`,
23
+ * returns an empty array. When `step` does not land exactly on `to`, the last value
24
+ * is the largest reachable value that does not exceed `to`.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * Num.range(0, 5); // [0, 1, 2, 3, 4, 5]
29
+ * Num.range(0, 10, 2); // [0, 2, 4, 6, 8, 10]
30
+ * Num.range(0, 9, 2); // [0, 2, 4, 6, 8]
31
+ * Num.range(5, 0); // []
32
+ * Num.range(3, 3); // [3]
33
+ * ```
34
+ */
35
+ Num.range = (from, to, step = 1) => {
36
+ if (step <= 0 || from > to)
37
+ return [];
38
+ const count = Math.floor((to - from) / step) + 1;
39
+ const result = new Array(count);
40
+ for (let i = 0; i < count; i++) {
41
+ result[i] = from + i * step;
42
+ }
43
+ return result;
44
+ };
45
+ /**
46
+ * Clamps a number between `min` and `max` (both inclusive).
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * pipe(150, Num.clamp(0, 100)); // 100
51
+ * pipe(-5, Num.clamp(0, 100)); // 0
52
+ * pipe(42, Num.clamp(0, 100)); // 42
53
+ * ```
54
+ */
55
+ Num.clamp = (min, max) => (n) => Math.min(Math.max(n, min), max);
56
+ /**
57
+ * Returns `true` when the number is between `min` and `max` (both inclusive).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * pipe(5, Num.between(1, 10)); // true
62
+ * pipe(0, Num.between(1, 10)); // false
63
+ * pipe(10, Num.between(1, 10)); // true
64
+ * ```
65
+ */
66
+ Num.between = (min, max) => (n) => n >= min && n <= max;
67
+ /**
68
+ * Parses a string as a number. Returns `None` when the result is `NaN`.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * Num.parse("42"); // Some(42)
73
+ * Num.parse("3.14"); // Some(3.14)
74
+ * Num.parse("abc"); // None
75
+ * Num.parse(""); // None
76
+ * ```
77
+ */
78
+ Num.parse = (s) => {
79
+ if (s.trim() === "")
80
+ return Option.none();
81
+ const n = Number(s);
82
+ return isNaN(n) ? Option.none() : Option.some(n);
83
+ };
84
+ /**
85
+ * Adds `b` to a number. Data-last: use in `pipe` or `Arr.map`.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * pipe(5, Num.add(3)); // 8
90
+ * pipe([1, 2, 3], Arr.map(Num.add(10))); // [11, 12, 13]
91
+ * ```
92
+ */
93
+ Num.add = (b) => (a) => a + b;
94
+ /**
95
+ * Subtracts `b` from a number. Data-last: `subtract(b)(a)` = `a - b`.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * pipe(10, Num.subtract(3)); // 7
100
+ * pipe([5, 10, 15], Arr.map(Num.subtract(2))); // [3, 8, 13]
101
+ * ```
102
+ */
103
+ Num.subtract = (b) => (a) => a - b;
104
+ /**
105
+ * Multiplies a number by `b`. Data-last: use in `pipe` or `Arr.map`.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * pipe(6, Num.multiply(7)); // 42
110
+ * pipe([1, 2, 3], Arr.map(Num.multiply(100))); // [100, 200, 300]
111
+ * ```
112
+ */
113
+ Num.multiply = (b) => (a) => a * b;
114
+ /**
115
+ * Divides a number by `b`. Data-last: `divide(b)(a)` = `a / b`.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * pipe(20, Num.divide(4)); // 5
120
+ * pipe([10, 20, 30], Arr.map(Num.divide(10))); // [1, 2, 3]
121
+ * ```
122
+ */
123
+ Num.divide = (b) => (a) => a / b;
124
+ })(Num || (Num = {}));
@@ -1,4 +1,4 @@
1
- import { Option } from "./Option.js";
1
+ import { Option } from "../Core/Option.js";
2
2
  /**
3
3
  * Functional record/object utilities that compose well with pipe.
4
4
  * All functions are data-last and curried where applicable.
@@ -23,9 +23,11 @@ export var Rec;
23
23
  * ```
24
24
  */
25
25
  Rec.map = (f) => (data) => {
26
+ const keys = Object.keys(data);
27
+ const vals = Object.values(data);
26
28
  const result = {};
27
- for (const key of Object.keys(data)) {
28
- result[key] = f(data[key]);
29
+ for (let i = 0; i < keys.length; i++) {
30
+ result[keys[i]] = f(vals[i]);
29
31
  }
30
32
  return result;
31
33
  };
@@ -39,9 +41,11 @@ export var Rec;
39
41
  * ```
40
42
  */
41
43
  Rec.mapWithKey = (f) => (data) => {
44
+ const keys = Object.keys(data);
45
+ const vals = Object.values(data);
42
46
  const result = {};
43
- for (const key of Object.keys(data)) {
44
- result[key] = f(key, data[key]);
47
+ for (let i = 0; i < keys.length; i++) {
48
+ result[keys[i]] = f(keys[i], vals[i]);
45
49
  }
46
50
  return result;
47
51
  };
@@ -54,10 +58,12 @@ export var Rec;
54
58
  * ```
55
59
  */
56
60
  Rec.filter = (predicate) => (data) => {
61
+ const keys = Object.keys(data);
62
+ const vals = Object.values(data);
57
63
  const result = {};
58
- for (const key of Object.keys(data)) {
59
- if (predicate(data[key]))
60
- result[key] = data[key];
64
+ for (let i = 0; i < keys.length; i++) {
65
+ if (predicate(vals[i]))
66
+ result[keys[i]] = vals[i];
61
67
  }
62
68
  return result;
63
69
  };
@@ -71,10 +77,12 @@ export var Rec;
71
77
  * ```
72
78
  */
73
79
  Rec.filterWithKey = (predicate) => (data) => {
80
+ const keys = Object.keys(data);
81
+ const vals = Object.values(data);
74
82
  const result = {};
75
- for (const key of Object.keys(data)) {
76
- if (predicate(key, data[key]))
77
- result[key] = data[key];
83
+ for (let i = 0; i < keys.length; i++) {
84
+ if (predicate(keys[i], vals[i]))
85
+ result[keys[i]] = vals[i];
78
86
  }
79
87
  return result;
80
88
  };
@@ -109,6 +117,32 @@ export var Rec;
109
117
  * ```
110
118
  */
111
119
  Rec.fromEntries = (data) => Object.fromEntries(data);
120
+ /**
121
+ * Groups elements of an array into a record keyed by the result of `keyFn`. Each key maps to
122
+ * the array of elements that produced it, in insertion order.
123
+ *
124
+ * Unlike `Dict.groupBy`, keys are always strings. Use `Dict.groupBy` when you need non-string
125
+ * keys or want to avoid the plain-object prototype chain.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * pipe(
130
+ * ["apple", "avocado", "banana", "blueberry"],
131
+ * Rec.groupBy(s => s[0]),
132
+ * ); // { a: ["apple", "avocado"], b: ["banana", "blueberry"] }
133
+ * ```
134
+ */
135
+ Rec.groupBy = (keyFn) => (items) => {
136
+ const result = {};
137
+ for (const item of items) {
138
+ const key = keyFn(item);
139
+ if (key in result)
140
+ result[key].push(item);
141
+ else
142
+ result[key] = [item];
143
+ }
144
+ return result;
145
+ };
112
146
  /**
113
147
  * Picks specific keys from a record.
114
148
  *
@@ -164,4 +198,44 @@ export var Rec;
164
198
  * Returns the number of keys in a record.
165
199
  */
166
200
  Rec.size = (data) => Object.keys(data).length;
201
+ /**
202
+ * Transforms each key while preserving values.
203
+ * If two keys map to the same new key, the last one wins.
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * pipe({ firstName: "Alice", lastName: "Smith" }, Rec.mapKeys(k => k.toUpperCase()));
208
+ * // { FIRSTNAME: "Alice", LASTNAME: "Smith" }
209
+ * ```
210
+ */
211
+ Rec.mapKeys = (f) => (data) => {
212
+ const keys = Object.keys(data);
213
+ const vals = Object.values(data);
214
+ const result = {};
215
+ for (let i = 0; i < keys.length; i++) {
216
+ result[f(keys[i])] = vals[i];
217
+ }
218
+ return result;
219
+ };
220
+ /**
221
+ * Removes all `None` values from a `Record<string, Option<A>>`, returning a plain `Record<string, A>`.
222
+ * Useful when building records from fallible lookups.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * Rec.compact({ a: Option.some(1), b: Option.none(), c: Option.some(3) });
227
+ * // { a: 1, c: 3 }
228
+ * ```
229
+ */
230
+ Rec.compact = (data) => {
231
+ const keys = Object.keys(data);
232
+ const vals = Object.values(data);
233
+ const result = {};
234
+ for (let i = 0; i < keys.length; i++) {
235
+ const v = vals[i];
236
+ if (v.kind === "Some")
237
+ result[keys[i]] = v.value;
238
+ }
239
+ return result;
240
+ };
167
241
  })(Rec || (Rec = {}));