@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/README.md +21 -7
- package/lib/index.d.ts +255 -24
- package/lib/index.js +476 -84
- package/lib/index.js.map +1 -1
- package/llms.txt +804 -0
- package/meta/api.json +644 -65
- package/meta/category.json +6 -0
- package/meta/examples.json +141 -11
- package/meta/licenses.json +1 -8
- package/meta/native-alternatives.json +50 -0
- package/package.json +13 -6
package/lib/index.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
|
|
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 (
|
|
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/
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
112
|
+
* @returns `true` if arrays are deeply equal, `false` otherwise.
|
|
36
113
|
* @since 2.0.0
|
|
37
114
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
59
|
-
|
|
239
|
+
const a = ensureDate(dateA);
|
|
240
|
+
const b = ensureDate(dateB);
|
|
241
|
+
if (a === null || b === null) return false;
|
|
60
242
|
switch (precision) {
|
|
61
|
-
case "
|
|
62
|
-
case "
|
|
63
|
-
case "
|
|
64
|
-
case "
|
|
65
|
-
|
|
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/
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
119
|
-
*
|
|
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
|
|
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
|
|
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
|
|
133
|
-
const hasB = key
|
|
134
|
-
if (!hasA && hasB)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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/
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @
|
|
164
|
-
*
|
|
165
|
-
* @
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* @
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 {
|
|
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
|