@helpers4/object 2.0.0-beta.0 → 2.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/lib/index.js CHANGED
@@ -32,27 +32,109 @@ function deepClone(obj) {
32
32
  return cloned;
33
33
  }
34
34
  //#endregion
35
- //#region helpers/array/shallowEquals.ts
35
+ //#region helpers/type/isPlainObject.ts
36
+ /**
37
+ * This file is part of helpers4.
38
+ * Copyright (C) 2025 baxyz
39
+ * SPDX-License-Identifier: LGPL-3.0-or-later
40
+ */
41
+ /**
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
36
90
  /**
37
91
  * This file is part of helpers4.
38
92
  * Copyright (C) 2025 baxyz
39
93
  * SPDX-License-Identifier: LGPL-3.0-or-later
40
94
  */
41
95
  /**
42
- * Quick comparison of two arrays using JSON.stringify.
43
- * This is a fast but simple comparison that may not work for all edge cases.
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}.
44
109
  *
45
110
  * @param arrA - First array to compare
46
111
  * @param arrB - Second array to compare
47
- * @returns `true` if arrays are identical according to JSON.stringify, `false` otherwise
112
+ * @returns `true` if arrays are deeply equal, `false` otherwise.
48
113
  * @since 2.0.0
49
114
  */
50
- function shallowEquals$1(arrA, arrB) {
51
- try {
52
- return JSON.stringify(arrA) === JSON.stringify(arrB);
53
- } catch {
54
- 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;
55
136
  }
137
+ return false;
56
138
  }
57
139
  //#endregion
58
140
  //#region helpers/date/timestamp.ts
@@ -207,104 +289,158 @@ function isSpecialObject(value) {
207
289
  return false;
208
290
  }
209
291
  //#endregion
210
- //#region helpers/object/deepCompare.ts
292
+ //#region helpers/object/diff.ts
211
293
  /**
212
294
  * This file is part of helpers4.
213
295
  * Copyright (C) 2025 baxyz
214
296
  * SPDX-License-Identifier: LGPL-3.0-or-later
215
297
  */
216
298
  /**
217
- * Deep comparison of two objects that returns detailed information about 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.
218
315
  *
219
- * @param objA - First object to compare (can be object, undefined, or null)
220
- * @param objB - Second object to compare (can be object, undefined, or null)
221
- * @returns `true` if objects are identical, `false` if incompatible types, or a `DeepCompareResult` object detailing differences
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.
222
320
  * @since 2.0.0
223
321
  */
224
- function deepCompare(objA, objB) {
322
+ function diff(objA, objB, options = {}) {
323
+ const depth = options.depth ?? Infinity;
225
324
  if (objA === objB) return true;
226
325
  if (objA == null || objB == null) return false;
227
326
  if (objA instanceof Date && objB instanceof Date) return compare(objA, objB);
228
- if (Array.isArray(objA) && Array.isArray(objB)) return shallowEquals$1(objA, objB);
327
+ if (Array.isArray(objA) && Array.isArray(objB)) return equalsDeep$1(objA, objB);
229
328
  if (Array.isArray(objA) || Array.isArray(objB)) return false;
230
329
  if (isSpecialObject(objA) || isSpecialObject(objB)) return objA === objB;
330
+ return diffPlainObjects(objA, objB, depth);
331
+ }
332
+ function diffPlainObjects(objA, objB, depth) {
231
333
  const keys = Array.from(new Set([...Object.keys(objA), ...Object.keys(objB)]));
232
- const differences = {};
334
+ const differences = Object.create(null);
233
335
  for (const key of keys) {
234
- const hasA = key in objA;
235
- const hasB = key in objB;
236
- if (!hasA && hasB) differences[key] = "onlyB";
237
- else if (hasA && !hasB) differences[key] = "onlyA";
238
- else {
239
- const valueA = objA[key];
240
- const valueB = objB[key];
241
- if (Array.isArray(valueA) && Array.isArray(valueB)) {
242
- if (!shallowEquals$1(valueA, valueB)) differences[key] = false;
243
- } else if (valueA instanceof Date && valueB instanceof Date) {
244
- if (!compare(valueA, valueB)) differences[key] = false;
245
- } else if (isSpecialObject(valueA) || isSpecialObject(valueB)) {
246
- if (valueA !== valueB) differences[key] = false;
247
- } else if (valueA !== null && valueB !== null && valueA !== void 0 && valueB !== void 0 && typeof valueA === "object" && typeof valueB === "object" && !isSpecialObject(valueA) && !isSpecialObject(valueB)) {
248
- const nestedResult = deepCompare(valueA, valueB);
249
- if (nestedResult !== true) differences[key] = nestedResult === false ? false : nestedResult;
250
- } 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;
356
+ }
357
+ if (valueA instanceof Date && valueB instanceof Date) {
358
+ if (!compare(valueA, valueB)) differences[key] = false;
359
+ continue;
251
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;
252
379
  }
253
380
  return Object.keys(differences).length > 0 ? differences : true;
254
381
  }
255
382
  //#endregion
256
- //#region helpers/type/isPlainObject.ts
383
+ //#region helpers/object/equalsDeep.ts
257
384
  /**
258
385
  * This file is part of helpers4.
259
386
  * Copyright (C) 2025 baxyz
260
387
  * SPDX-License-Identifier: LGPL-3.0-or-later
261
388
  */
262
389
  /**
263
- * Checks if a value is a plain object.
390
+ * Recursive structural object equality.
264
391
  *
265
- * A plain object is created by `{}`, `new Object()`, or `Object.create(null)`.
266
- * Returns `false` for arrays, Date, Map, Set, RegExp, class instances, etc.
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.
267
396
  *
268
- * @param value - The value to check
269
- * @returns True if value is a plain object
270
- * @example
271
- * isPlainObject({}) // => true
272
- * isPlainObject({ a: 1 }) // => true
273
- * isPlainObject(new Date()) // => false
274
- * isPlainObject([]) // => false
275
- * isPlainObject(null) // => false
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.
276
402
  * @since 2.0.0
277
403
  */
278
- function isPlainObject(value) {
279
- if (value === null || typeof value !== "object") return false;
280
- const proto = Object.getPrototypeOf(value);
281
- return proto === Object.prototype || proto === null;
404
+ function equalsDeep(objA, objB) {
405
+ return diff(objA, objB) === true;
282
406
  }
283
407
  //#endregion
284
- //#region helpers/object/deepMerge.ts
408
+ //#region helpers/object/equalsShallow.ts
285
409
  /**
286
410
  * This file is part of helpers4.
287
411
  * Copyright (C) 2025 baxyz
288
412
  * SPDX-License-Identifier: LGPL-3.0-or-later
289
413
  */
290
- var UNSAFE_KEYS$1 = new Set([
291
- "__proto__",
292
- "constructor",
293
- "prototype"
294
- ]);
295
- function deepMerge(target, ...sources) {
296
- if (target === void 0 || target === null) return target;
297
- if (!sources.length) return target;
298
- const source = sources.shift();
299
- if (!source) return deepMerge(target, ...sources);
300
- for (const key in source) {
301
- if (!Object.hasOwn(source, key) || UNSAFE_KEYS$1.has(key)) continue;
302
- const targetValue = target[key];
303
- const sourceValue = source[key];
304
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
305
- else if (sourceValue !== void 0) target[key] = sourceValue;
306
- }
307
- return deepMerge(target, ...sources);
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;
308
444
  }
309
445
  //#endregion
310
446
  //#region helpers/object/get.ts
@@ -331,6 +467,105 @@ function get(obj, path, defaultValue) {
331
467
  return result !== void 0 ? result : defaultValue;
332
468
  }
333
469
  //#endregion
470
+ //#region helpers/object/groupBy.ts
471
+ /**
472
+ * This file is part of helpers4.
473
+ * Copyright (C) 2025 baxyz
474
+ * SPDX-License-Identifier: LGPL-3.0-or-later
475
+ */
476
+ /**
477
+ * Groups an array of items by a key derived from each item.
478
+ *
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 2.0.0
489
+ */
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 2.0.0
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 2.0.0
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;
565
+ }
566
+ return result;
567
+ }
568
+ //#endregion
334
569
  //#region helpers/object/omit.ts
335
570
  function omit(obj, keys) {
336
571
  if (obj === void 0 || obj === null) return obj;
@@ -432,31 +667,6 @@ function set(obj, path, value) {
432
667
  return obj;
433
668
  }
434
669
  //#endregion
435
- //#region helpers/object/shallowEquals.ts
436
- /**
437
- * This file is part of helpers4.
438
- * Copyright (C) 2025 baxyz
439
- * SPDX-License-Identifier: LGPL-3.0-or-later
440
- */
441
- /**
442
- * Quick comparison of two objects using JSON.stringify.
443
- * This is a fast but simple comparison that may not work for all edge cases.
444
- *
445
- * @param objA - First object to compare
446
- * @param objB - Second object to compare
447
- * @returns `true` if objects are identical according to JSON.stringify, `false` otherwise
448
- * @since 2.0.0
449
- */
450
- function shallowEquals(objA, objB) {
451
- if (objA === objB) return true;
452
- if (typeof objA === "function" || typeof objB === "function") return objA === objB;
453
- try {
454
- return JSON.stringify(objA) === JSON.stringify(objB);
455
- } catch {
456
- return objA === objB;
457
- }
458
- }
459
- //#endregion
460
- export { compact, deepClone, deepCompare, deepMerge, get, omit, pick, removeUndefinedNull, safeJsonParse, set, shallowEquals };
670
+ export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
461
671
 
462
672
  //# sourceMappingURL=index.js.map