@helpers4/object 2.0.0-alpha.9 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,10 +1,22 @@
1
- import { isNullish } from "radashi";
1
+ //#region helpers/object/compact.ts
2
+ function compact(obj) {
3
+ if (obj === void 0 || obj === null) return obj;
4
+ const result = {};
5
+ for (const key of Object.keys(obj)) if (obj[key]) result[key] = obj[key];
6
+ return result;
7
+ }
8
+ //#endregion
2
9
  //#region helpers/object/deepClone.ts
3
10
  /**
4
11
  * This file is part of helpers4.
5
12
  * Copyright (C) 2025 baxyz
6
13
  * SPDX-License-Identifier: LGPL-3.0-or-later
7
14
  */
15
+ var UNSAFE_KEYS$2 = new Set([
16
+ "__proto__",
17
+ "constructor",
18
+ "prototype"
19
+ ]);
8
20
  /**
9
21
  * Creates a deep copy of an object or array
10
22
  * @param obj - The object to clone
@@ -16,53 +28,225 @@ function deepClone(obj) {
16
28
  if (obj instanceof Date) return new Date(obj.getTime());
17
29
  if (Array.isArray(obj)) return obj.map((item) => deepClone(item));
18
30
  const cloned = {};
19
- for (const key in obj) if (obj.hasOwnProperty(key)) cloned[key] = deepClone(obj[key]);
31
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS$2.has(key)) cloned[key] = deepClone(obj[key]);
20
32
  return cloned;
21
33
  }
22
34
  //#endregion
23
- //#region helpers/array/quickCompare.ts
35
+ //#region helpers/type/isPlainObject.ts
24
36
  /**
25
37
  * This file is part of helpers4.
26
38
  * Copyright (C) 2025 baxyz
27
39
  * SPDX-License-Identifier: LGPL-3.0-or-later
28
40
  */
29
41
  /**
30
- * Quick comparison of two arrays using JSON.stringify.
31
- * This is a fast but simple comparison that may not work for all edge cases.
32
- *
42
+ * Checks if a value is a plain object.
43
+ *
44
+ * A plain object is created by `{}`, `new Object()`, or `Object.create(null)`.
45
+ * Returns `false` for arrays, Date, Map, Set, RegExp, class instances, etc.
46
+ *
47
+ * @param value - The value to check
48
+ * @returns True if value is a plain object
49
+ * @example
50
+ * isPlainObject({}) // => true
51
+ * isPlainObject({ a: 1 }) // => true
52
+ * isPlainObject(new Date()) // => false
53
+ * isPlainObject([]) // => false
54
+ * isPlainObject(null) // => false
55
+ * @since 2.0.0
56
+ */
57
+ function isPlainObject(value) {
58
+ if (value === null || typeof value !== "object") return false;
59
+ const proto = Object.getPrototypeOf(value);
60
+ return proto === Object.prototype || proto === null;
61
+ }
62
+ //#endregion
63
+ //#region helpers/object/deepMerge.ts
64
+ /**
65
+ * This file is part of helpers4.
66
+ * Copyright (C) 2025 baxyz
67
+ * SPDX-License-Identifier: LGPL-3.0-or-later
68
+ */
69
+ var UNSAFE_KEYS$1 = new Set([
70
+ "__proto__",
71
+ "constructor",
72
+ "prototype"
73
+ ]);
74
+ function deepMerge(target, ...sources) {
75
+ if (target === void 0 || target === null) return target;
76
+ if (!sources.length) return target;
77
+ const source = sources.shift();
78
+ if (!source) return deepMerge(target, ...sources);
79
+ for (const key in source) {
80
+ if (!Object.hasOwn(source, key) || UNSAFE_KEYS$1.has(key)) continue;
81
+ const targetValue = target[key];
82
+ const sourceValue = source[key];
83
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
84
+ else if (sourceValue !== void 0) target[key] = sourceValue;
85
+ }
86
+ return deepMerge(target, ...sources);
87
+ }
88
+ //#endregion
89
+ //#region helpers/array/equalsDeep.ts
90
+ /**
91
+ * This file is part of helpers4.
92
+ * Copyright (C) 2025 baxyz
93
+ * SPDX-License-Identifier: LGPL-3.0-or-later
94
+ */
95
+ /**
96
+ * Recursive structural array equality.
97
+ *
98
+ * Two arrays are equal when they have the same length and each pair of
99
+ * elements at the same index is structurally equal:
100
+ * - Arrays recurse with `equalsDeep`.
101
+ * - Plain objects recurse key-by-key with structural comparison.
102
+ * - `Date` instances are compared by their epoch value.
103
+ * - All other values use strict equality (`===`), which means `NaN !== NaN`
104
+ * and special objects (Map, Set, RegExp, Promise, class instances\u2026) are
105
+ * compared by reference.
106
+ *
107
+ * For positional one-level comparison use {@link equalsShallow}. For
108
+ * order-independent comparison use {@link equalsUnordered}.
109
+ *
33
110
  * @param arrA - First array to compare
34
111
  * @param arrB - Second array to compare
35
- * @returns `true` if arrays are identical according to JSON.stringify, `false` otherwise
112
+ * @returns `true` if arrays are deeply equal, `false` otherwise.
36
113
  * @since 2.0.0
37
114
  */
38
- function quickCompare$1(arrA, arrB) {
39
- try {
40
- return JSON.stringify(arrA) === JSON.stringify(arrB);
41
- } catch {
42
- return arrA === arrB;
115
+ function equalsDeep$1(arrA, arrB) {
116
+ if (arrA === arrB) return true;
117
+ if (!Array.isArray(arrA) || !Array.isArray(arrB)) return false;
118
+ if (arrA.length !== arrB.length) return false;
119
+ for (let i = 0; i < arrA.length; i++) if (!valuesEqual(arrA[i], arrB[i])) return false;
120
+ return true;
121
+ }
122
+ function valuesEqual(a, b) {
123
+ if (a === b) return true;
124
+ if (Array.isArray(a) && Array.isArray(b)) return equalsDeep$1(a, b);
125
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
126
+ if (isPlainObject(a) && isPlainObject(b)) {
127
+ const ao = a;
128
+ const bo = b;
129
+ const keysA = Object.keys(ao);
130
+ if (keysA.length !== Object.keys(bo).length) return false;
131
+ for (const key of keysA) {
132
+ if (!Object.prototype.hasOwnProperty.call(bo, key)) return false;
133
+ if (!valuesEqual(ao[key], bo[key])) return false;
134
+ }
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ //#endregion
140
+ //#region helpers/date/timestamp.ts
141
+ /**
142
+ * Checks if a timestamp is likely in seconds (Java/Unix style) vs milliseconds (JavaScript style)
143
+ * @param timestamp - The timestamp to check
144
+ * @returns True if timestamp appears to be in seconds
145
+ * @since 1.9.0
146
+ */
147
+ function isTimestampInSeconds(timestamp) {
148
+ return Math.abs(timestamp) < 1e10;
149
+ }
150
+ /**
151
+ * Converts a timestamp to JavaScript milliseconds format
152
+ * @param timestamp - The timestamp (in seconds or milliseconds)
153
+ * @returns Timestamp in milliseconds
154
+ * @since 1.9.0
155
+ */
156
+ function normalizeTimestamp(timestamp) {
157
+ return isTimestampInSeconds(timestamp) ? timestamp * 1e3 : timestamp;
158
+ }
159
+ //#endregion
160
+ //#region helpers/date/ensureDate.ts
161
+ /**
162
+ * This file is part of helpers4.
163
+ * Copyright (C) 2025 baxyz
164
+ * SPDX-License-Identifier: LGPL-3.0-or-later
165
+ */
166
+ /**
167
+ * Type guard that checks whether a value satisfies the {@link EpochMilliseconds}
168
+ * structural interface (e.g. `Temporal.Instant`, `Temporal.ZonedDateTime`).
169
+ */
170
+ function hasEpochMilliseconds(value) {
171
+ return typeof value === "object" && value !== null && "epochMilliseconds" in value && typeof value.epochMilliseconds === "number";
172
+ }
173
+ /**
174
+ * Safely converts a date-like value to a valid `Date` object, or returns `null`.
175
+ *
176
+ * Accepts `Date`, timestamps (seconds or milliseconds, auto-detected), date strings,
177
+ * and objects with an `epochMilliseconds` property (e.g. `Temporal.Instant`,
178
+ * `Temporal.ZonedDateTime`).
179
+ * Returns `null` for `null`, `undefined`, empty strings, `0`, and any value that
180
+ * produces an invalid `Date`.
181
+ *
182
+ * This is the date equivalent of {@link ensureArray} — it normalizes flexible
183
+ * input into a guaranteed type (or a safe fallback).
184
+ *
185
+ * @param input - A date-like value to convert
186
+ * @returns A valid `Date` object, or `null` if the input is invalid
187
+ * @example
188
+ * ensureDate('2025-01-19T12:00:00Z') // => Date
189
+ * ensureDate(1737290400) // => Date (from Unix seconds)
190
+ * ensureDate(1737290400000) // => Date (from milliseconds)
191
+ * ensureDate(new Date()) // => Date (same reference)
192
+ * ensureDate(null) // => null
193
+ * ensureDate('invalid') // => null
194
+ *
195
+ * @see {@link DateLike} for the accepted input types
196
+ * @since 2.0.0
197
+ */
198
+ function ensureDate(input) {
199
+ if (input === null || input === void 0 || input === "" || input === 0) return null;
200
+ if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
201
+ if (hasEpochMilliseconds(input)) {
202
+ const date = new Date(input.epochMilliseconds);
203
+ return isNaN(date.getTime()) ? null : date;
43
204
  }
205
+ if (typeof input === "number") {
206
+ const normalized = normalizeTimestamp(input);
207
+ const date = new Date(normalized);
208
+ return isNaN(date.getTime()) ? null : date;
209
+ }
210
+ if (typeof input === "string") {
211
+ const date = new Date(input);
212
+ return isNaN(date.getTime()) ? null : date;
213
+ }
214
+ return null;
44
215
  }
45
216
  //#endregion
46
217
  //#region helpers/date/compare.ts
47
218
  /**
219
+ * This file is part of helpers4.
220
+ * Copyright (C) 2025 baxyz
221
+ * SPDX-License-Identifier: LGPL-3.0-or-later
222
+ */
223
+ /**
48
224
  * Comparison of two dates.
49
- *
225
+ *
226
+ * Accepts any {@link DateLike} input (Date, timestamp, or date string).
227
+ *
50
228
  * @param dateA - First date to compare
51
229
  * @param dateB - Second date to compare
52
230
  * @param options - Comparison options
53
231
  * @returns `true` if dates are identical according to the specified precision, `false` otherwise
232
+ * @example
233
+ * compare('2025-01-19', new Date('2025-01-19T08:00:00Z'), { precision: 'days' })
234
+ * // => true
54
235
  * @since 2.0.0
55
236
  */
56
237
  function compare(dateA, dateB, options = {}) {
57
238
  const { precision = "milliseconds" } = options;
58
- if (!(dateA instanceof Date) || !(dateB instanceof Date)) return dateA === dateB;
59
- if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) return isNaN(dateA.getTime()) && isNaN(dateB.getTime());
239
+ const a = ensureDate(dateA);
240
+ const b = ensureDate(dateB);
241
+ if (a === null || b === null) return false;
60
242
  switch (precision) {
61
- case "days": return dateA.toDateString() === dateB.toDateString();
62
- case "hours": return Math.floor(dateA.getTime() / (1e3 * 60 * 60)) === Math.floor(dateB.getTime() / (1e3 * 60 * 60));
63
- case "minutes": return Math.floor(dateA.getTime() / (1e3 * 60)) === Math.floor(dateB.getTime() / (1e3 * 60));
64
- case "seconds": return Math.floor(dateA.getTime() / 1e3) === Math.floor(dateB.getTime() / 1e3);
65
- default: return dateA.getTime() === dateB.getTime();
243
+ case "years": return a.getFullYear() === b.getFullYear();
244
+ case "months": return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
245
+ case "days": return a.toDateString() === b.toDateString();
246
+ case "hours": return Math.floor(a.getTime() / (1e3 * 60 * 60)) === Math.floor(b.getTime() / (1e3 * 60 * 60));
247
+ case "minutes": return Math.floor(a.getTime() / (1e3 * 60)) === Math.floor(b.getTime() / (1e3 * 60));
248
+ case "seconds": return Math.floor(a.getTime() / 1e3) === Math.floor(b.getTime() / 1e3);
249
+ default: return a.getTime() === b.getTime();
66
250
  }
67
251
  }
68
252
  //#endregion
@@ -105,79 +289,158 @@ function isSpecialObject(value) {
105
289
  return false;
106
290
  }
107
291
  //#endregion
108
- //#region helpers/object/deepCompare.ts
292
+ //#region helpers/object/diff.ts
109
293
  /**
110
294
  * This file is part of helpers4.
111
295
  * Copyright (C) 2025 baxyz
112
296
  * SPDX-License-Identifier: LGPL-3.0-or-later
113
297
  */
114
298
  /**
115
- * Deep comparison of two objects that returns detailed information about differences.
116
- *
117
- * @param objA - First object to compare (can be object, undefined, or null)
118
- * @param objB - Second object to compare (can be object, undefined, or null)
119
- * @returns `true` if objects are identical, `false` if incompatible types, or a `DeepCompareResult` object detailing differences
299
+ * Structural object diff.
300
+ *
301
+ * Returns `true` when both inputs are deeply equal, otherwise a
302
+ * {@link DiffResult} describing the differences key by key.
303
+ *
304
+ * Comparison rules:
305
+ * - Same reference \u2192 `true`.
306
+ * - Either side is `null`/`undefined` (and not both) \u2192 `false`.
307
+ * - Both `Date` \u2192 epoch comparison.
308
+ * - Both arrays \u2192 compared with `array/equalsDeep` (leaf, no diff drill-down).
309
+ * - Special objects (Map, Set, RegExp, Promise, class instances\u2026) \u2192 reference equality.
310
+ * - Plain objects \u2192 key-by-key, recursing up to `options.depth` levels.
311
+ * - Mixed types (e.g. array vs object, Date vs object) \u2192 `false`.
312
+ *
313
+ * For a boolean wrapper see {@link equalsDeep} from this category.
314
+ * For a one-level boolean check see {@link equalsShallow} from this category.
315
+ *
316
+ * @param objA - First value (object, `null`, or `undefined`).
317
+ * @param objB - Second value (object, `null`, or `undefined`).
318
+ * @param options - See {@link DiffOptions}.
319
+ * @returns `true` when equal, otherwise a {@link DiffResult}, or `false` for incompatible types.
120
320
  * @since 2.0.0
121
321
  */
122
- function deepCompare(objA, objB) {
322
+ function diff(objA, objB, options = {}) {
323
+ const depth = options.depth ?? Infinity;
123
324
  if (objA === objB) return true;
124
325
  if (objA == null || objB == null) return false;
125
326
  if (objA instanceof Date && objB instanceof Date) return compare(objA, objB);
126
- if (Array.isArray(objA) && Array.isArray(objB)) return quickCompare$1(objA, objB);
327
+ if (Array.isArray(objA) && Array.isArray(objB)) return equalsDeep$1(objA, objB);
127
328
  if (Array.isArray(objA) || Array.isArray(objB)) return false;
128
329
  if (isSpecialObject(objA) || isSpecialObject(objB)) return objA === objB;
330
+ return diffPlainObjects(objA, objB, depth);
331
+ }
332
+ function diffPlainObjects(objA, objB, depth) {
129
333
  const keys = Array.from(new Set([...Object.keys(objA), ...Object.keys(objB)]));
130
- const differences = {};
334
+ const differences = Object.create(null);
131
335
  for (const key of keys) {
132
- const hasA = key in objA;
133
- const hasB = key in objB;
134
- if (!hasA && hasB) differences[key] = "onlyB";
135
- else if (hasA && !hasB) differences[key] = "onlyA";
136
- else {
137
- const valueA = objA[key];
138
- const valueB = objB[key];
139
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
140
- if (!quickCompare$1(valueA, valueB)) differences[key] = false;
141
- } else if (valueA instanceof Date && valueB instanceof Date) {
142
- if (!compare(valueA, valueB)) differences[key] = false;
143
- } else if (isSpecialObject(valueA) || isSpecialObject(valueB)) {
144
- if (valueA !== valueB) differences[key] = false;
145
- } else if (valueA !== null && valueB !== null && valueA !== void 0 && valueB !== void 0 && typeof valueA === "object" && typeof valueB === "object" && !isSpecialObject(valueA) && !isSpecialObject(valueB)) {
146
- const nestedResult = deepCompare(valueA, valueB);
147
- if (nestedResult !== true) differences[key] = nestedResult === false ? false : nestedResult;
148
- } else if (valueA !== valueB) differences[key] = false;
336
+ const hasA = Object.prototype.hasOwnProperty.call(objA, key);
337
+ const hasB = Object.prototype.hasOwnProperty.call(objB, key);
338
+ if (!hasA && hasB) {
339
+ differences[key] = "onlyB";
340
+ continue;
341
+ }
342
+ if (hasA && !hasB) {
343
+ differences[key] = "onlyA";
344
+ continue;
345
+ }
346
+ const valueA = objA[key];
347
+ const valueB = objB[key];
348
+ if (valueA === valueB) continue;
349
+ if (Array.isArray(valueA) && Array.isArray(valueB)) {
350
+ if (!equalsDeep$1(valueA, valueB)) differences[key] = false;
351
+ continue;
352
+ }
353
+ if (Array.isArray(valueA) || Array.isArray(valueB)) {
354
+ differences[key] = false;
355
+ continue;
149
356
  }
357
+ if (valueA instanceof Date && valueB instanceof Date) {
358
+ if (!compare(valueA, valueB)) differences[key] = false;
359
+ continue;
360
+ }
361
+ if (valueA instanceof Date || valueB instanceof Date) {
362
+ differences[key] = false;
363
+ continue;
364
+ }
365
+ if (isSpecialObject(valueA) || isSpecialObject(valueB)) {
366
+ differences[key] = false;
367
+ continue;
368
+ }
369
+ if (!(valueA !== null && valueB !== null && typeof valueA === "object" && typeof valueB === "object")) {
370
+ differences[key] = false;
371
+ continue;
372
+ }
373
+ if (depth <= 0) {
374
+ differences[key] = false;
375
+ continue;
376
+ }
377
+ const nested = diffPlainObjects(valueA, valueB, depth - 1);
378
+ if (nested !== true) differences[key] = nested;
150
379
  }
151
380
  return Object.keys(differences).length > 0 ? differences : true;
152
381
  }
153
382
  //#endregion
154
- //#region helpers/object/deepMerge.ts
383
+ //#region helpers/object/equalsDeep.ts
155
384
  /**
156
385
  * This file is part of helpers4.
157
386
  * Copyright (C) 2025 baxyz
158
387
  * SPDX-License-Identifier: LGPL-3.0-or-later
159
388
  */
160
389
  /**
161
- * Merges two or more objects deeply
162
- * @param target - The target object
163
- * @param sources - The source objects to merge
164
- * @returns The merged object
165
- * @since 1.9.0
390
+ * Recursive structural object equality.
391
+ *
392
+ * Boolean wrapper around {@link diff} \u2014 returns `true` when the two values
393
+ * are deeply equal according to the same rules. Use this when you only
394
+ * need a yes/no answer; use {@link diff} when you also need to know
395
+ * *what* differs.
396
+ *
397
+ * For a one-level boolean check use {@link equalsShallow}.
398
+ *
399
+ * @param objA - First value (object, `null`, or `undefined`).
400
+ * @param objB - Second value (object, `null`, or `undefined`).
401
+ * @returns `true` if both inputs are deeply equal, `false` otherwise.
402
+ * @since 2.0.0
166
403
  */
167
- function deepMerge(target, ...sources) {
168
- if (!sources.length) return target;
169
- const source = sources.shift();
170
- if (!source) return deepMerge(target, ...sources);
171
- for (const key in source) {
172
- const targetValue = target[key];
173
- const sourceValue = source[key];
174
- if (isObject(targetValue) && isObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
175
- else if (sourceValue !== void 0) target[key] = sourceValue;
176
- }
177
- return deepMerge(target, ...sources);
404
+ function equalsDeep(objA, objB) {
405
+ return diff(objA, objB) === true;
178
406
  }
179
- function isObject(item) {
180
- return item && typeof item === "object" && !Array.isArray(item);
407
+ //#endregion
408
+ //#region helpers/object/equalsShallow.ts
409
+ /**
410
+ * This file is part of helpers4.
411
+ * Copyright (C) 2025 baxyz
412
+ * SPDX-License-Identifier: LGPL-3.0-or-later
413
+ */
414
+ /**
415
+ * One-level (shallow) object equality.
416
+ *
417
+ * Two objects are equal when they share the exact same set of own
418
+ * enumerable string keys and each pair of values satisfies strict equality
419
+ * (`===`). No recursion: nested objects/arrays are compared by reference.
420
+ *
421
+ * Falls back to strict equality when either input is `null`, `undefined`
422
+ * or not an object \u2014 so primitives match if and only if they are `===`.
423
+ * Arrays are not supported; they always return `false` (unless identical
424
+ * references). Use `array/equalsShallow` instead.
425
+ *
426
+ * For recursive structural comparison use {@link equalsDeep}. For a diff
427
+ * structure use {@link diff}.
428
+ *
429
+ * @param objA - First value to compare
430
+ * @param objB - Second value to compare
431
+ * @returns `true` if values are shallowly equal, `false` otherwise.
432
+ * @since 2.0.0
433
+ */
434
+ function equalsShallow(objA, objB) {
435
+ if (objA === objB) return true;
436
+ if (objA === null || objB === null || typeof objA !== "object" || typeof objB !== "object") return false;
437
+ const a = objA;
438
+ const b = objB;
439
+ if (Array.isArray(objA) || Array.isArray(objB)) return false;
440
+ const keysA = Object.keys(a);
441
+ if (keysA.length !== Object.keys(b).length) return false;
442
+ for (const key of keysA) if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) return false;
443
+ return true;
181
444
  }
182
445
  //#endregion
183
446
  //#region helpers/object/get.ts
@@ -204,29 +467,143 @@ function get(obj, path, defaultValue) {
204
467
  return result !== void 0 ? result : defaultValue;
205
468
  }
206
469
  //#endregion
207
- //#region helpers/object/quickCompare.ts
470
+ //#region helpers/object/groupBy.ts
208
471
  /**
209
472
  * This file is part of helpers4.
210
473
  * Copyright (C) 2025 baxyz
211
474
  * SPDX-License-Identifier: LGPL-3.0-or-later
212
475
  */
213
476
  /**
214
- * Quick comparison of two objects using JSON.stringify.
215
- * This is a fast but simple comparison that may not work for all edge cases.
477
+ * Groups an array of items by a key derived from each item.
216
478
  *
217
- * @param objA - First object to compare
218
- * @param objB - Second object to compare
219
- * @returns `true` if objects are identical according to JSON.stringify, `false` otherwise
220
- * @since 2.0.0
479
+ * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
480
+ * older targets and provides stricter return-type inference.
481
+ *
482
+ * @param items - The array to group
483
+ * @param keyFn - Function that returns the group key for each item
484
+ * @returns A record mapping each key to the array of items with that key
485
+ * @example
486
+ * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
487
+ * // => { odd: [1, 3], even: [2, 4] }
488
+ * @since next
221
489
  */
222
- function quickCompare(objA, objB) {
223
- if (objA === objB) return true;
224
- if (typeof objA === "function" || typeof objB === "function") return objA === objB;
225
- try {
226
- return JSON.stringify(objA) === JSON.stringify(objB);
227
- } catch {
228
- return objA === objB;
490
+ function groupBy(items, keyFn) {
491
+ const result = {};
492
+ for (const item of items) {
493
+ const key = keyFn(item);
494
+ if (result[key] === void 0) result[key] = [];
495
+ result[key].push(item);
496
+ }
497
+ return result;
498
+ }
499
+ //#endregion
500
+ //#region helpers/object/invert.ts
501
+ /**
502
+ * This file is part of helpers4.
503
+ * Copyright (C) 2025 baxyz
504
+ * SPDX-License-Identifier: LGPL-3.0-or-later
505
+ */
506
+ /**
507
+ * Returns a new object with keys and values swapped.
508
+ * If multiple keys share the same value, the last one wins.
509
+ *
510
+ * @param obj - The object whose keys and values are to be swapped
511
+ * @returns A new object with values as keys and original keys as values
512
+ * @example
513
+ * invert({ a: 'x', b: 'y', c: 'z' })
514
+ * // => { x: 'a', y: 'b', z: 'c' }
515
+ *
516
+ * invert({ one: 1, two: 2 })
517
+ * // => { 1: 'one', 2: 'two' }
518
+ * @since next
519
+ */
520
+ function invert(obj) {
521
+ const result = {};
522
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) result[obj[key]] = key;
523
+ return result;
524
+ }
525
+ //#endregion
526
+ //#region helpers/object/map.ts
527
+ /**
528
+ * This file is part of helpers4.
529
+ * Copyright (C) 2025 baxyz
530
+ * SPDX-License-Identifier: LGPL-3.0-or-later
531
+ */
532
+ /**
533
+ * Transforms the values and/or keys of a plain object in a single pass.
534
+ *
535
+ * Both callbacks are optional and default to identity (no transformation).
536
+ * When `mapValue` is omitted the original values are preserved;
537
+ * when `mapKey` is omitted the original keys are preserved.
538
+ *
539
+ * Note: if two different keys map to the same output key the last one wins
540
+ * (insertion order).
541
+ *
542
+ * @param obj - The source object
543
+ * @param mapValue - Callback called with `(value, key)` for each entry.
544
+ * Defaults to identity.
545
+ * @param mapKey - Callback called with `(key, value)` for each entry.
546
+ * Defaults to identity.
547
+ * @returns A new object with transformed keys and/or values
548
+ * @example
549
+ * map({ a: 1, b: 2 }, v => v * 10)
550
+ * // => { a: 10, b: 20 }
551
+ *
552
+ * map({ a: 1, b: 2 }, undefined, k => k.toUpperCase())
553
+ * // => { A: 1, B: 2 }
554
+ *
555
+ * map({ a: 1, b: 2 }, v => v * 10, k => k.toUpperCase())
556
+ * // => { A: 10, B: 20 }
557
+ * @since next
558
+ */
559
+ function map(obj, mapValue, mapKey) {
560
+ const result = {};
561
+ for (const key of Object.keys(obj)) {
562
+ const value = obj[key];
563
+ const newKey = mapKey ? mapKey(key, value) : key;
564
+ result[newKey] = mapValue ? mapValue(value, key) : value;
229
565
  }
566
+ return result;
567
+ }
568
+ //#endregion
569
+ //#region helpers/object/omit.ts
570
+ function omit(obj, keys) {
571
+ if (obj === void 0 || obj === null) return obj;
572
+ const result = { ...obj };
573
+ for (const key of keys) delete result[key];
574
+ return result;
575
+ }
576
+ //#endregion
577
+ //#region helpers/object/pick.ts
578
+ function pick(obj, keys) {
579
+ if (obj === void 0 || obj === null) return obj;
580
+ const result = {};
581
+ for (const key of keys) if (Object.prototype.hasOwnProperty.call(obj, key)) result[key] = obj[key];
582
+ return result;
583
+ }
584
+ //#endregion
585
+ //#region helpers/type/isNullish.ts
586
+ /**
587
+ * This file is part of helpers4.
588
+ * Copyright (C) 2025 baxyz
589
+ * SPDX-License-Identifier: LGPL-3.0-or-later
590
+ */
591
+ /**
592
+ * Checks if a value is null or undefined (nullish).
593
+ * @param value - The value to check
594
+ * @returns True if value is null or undefined
595
+ * @example
596
+ * isNullish(null) // => true
597
+ * isNullish(undefined) // => true
598
+ * isNullish(0) // => false
599
+ * isNullish('') // => false
600
+ *
601
+ * @see {@link isDefined} for the inverse check (`!isNullish`)
602
+ * @see {@link isFalsy} to also catch `0`, `""`, `false`, and `NaN`
603
+ * @since 2.0.0
604
+ */
605
+ function isNullish(value) {
606
+ return value === null || value === void 0;
230
607
  }
231
608
  //#endregion
232
609
  //#region helpers/object/removeUndefinedNull.ts
@@ -243,10 +620,19 @@ function quickCompare(objA, objB) {
243
620
  * @since 1.0.0
244
621
  */
245
622
  function removeUndefinedNull(obj) {
246
- return obj ? Object.entries(obj).filter(([_, v]) => !isNullish(v)).reduce((acc, [k, v]) => ({
247
- ...acc,
248
- [k]: v
249
- }), {}) : obj;
623
+ if (!obj) return obj;
624
+ const result = {};
625
+ for (const [k, v] of Object.entries(obj)) if (!isNullish(v)) result[k] = v;
626
+ return result;
627
+ }
628
+ //#endregion
629
+ //#region helpers/object/safeJsonParse.ts
630
+ function safeJsonParse(input, fallback) {
631
+ try {
632
+ return JSON.parse(input);
633
+ } catch {
634
+ return arguments.length < 2 ? null : fallback;
635
+ }
250
636
  }
251
637
  //#endregion
252
638
  //#region helpers/object/set.ts
@@ -255,6 +641,11 @@ function removeUndefinedNull(obj) {
255
641
  * Copyright (C) 2025 baxyz
256
642
  * SPDX-License-Identifier: LGPL-3.0-or-later
257
643
  */
644
+ var UNSAFE_KEYS = new Set([
645
+ "__proto__",
646
+ "constructor",
647
+ "prototype"
648
+ ]);
258
649
  /**
259
650
  * Sets a value in an object using a dot-notated path
260
651
  * @param obj - The object to set value in
@@ -265,6 +656,7 @@ function removeUndefinedNull(obj) {
265
656
  */
266
657
  function set(obj, path, value) {
267
658
  const keys = path.split(".");
659
+ if (keys.some((k) => UNSAFE_KEYS.has(k))) return obj;
268
660
  let current = obj;
269
661
  for (let i = 0; i < keys.length - 1; i++) {
270
662
  const key = keys[i];
@@ -275,6 +667,6 @@ function set(obj, path, value) {
275
667
  return obj;
276
668
  }
277
669
  //#endregion
278
- export { deepClone, deepCompare, deepMerge, get, quickCompare, removeUndefinedNull, set };
670
+ export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
279
671
 
280
672
  //# sourceMappingURL=index.js.map