@helpers4/object 2.0.3 → 2.1.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/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @helpers4/object
2
2
 
3
3
  > Tree-shakable TypeScript utility functions for the `object` domain.
4
- > Package: `@helpers4/object` — Version: 2.0.3
4
+ > Package: `@helpers4/object` — Version: 2.1.0
5
5
  > License: LGPL-3.0-or-later
6
6
 
7
7
  ## Installation
@@ -15,35 +15,74 @@ pnpm add @helpers4/object
15
15
  ## Usage
16
16
 
17
17
  ```typescript
18
- import { compact, deepClone, deepMerge, ... } from '@helpers4/object';
18
+ import { cloneDeep, compact, diff, ... } from '@helpers4/object';
19
19
  ```
20
20
 
21
21
  ## Functions
22
22
 
23
23
  | Function | Description |
24
24
  |---|---|
25
+ | `cloneDeep` | Creates a deep copy of an object or array. Recursively clones own enumerable string keys. `Date` in |
25
26
  | `compact` | Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an objec |
26
- | `deepClone` | Creates a deep copy of an object or array |
27
- | `deepMerge` | Merges two or more objects deeply |
28
27
  | `diff` | Structural object diff. Returns `true` when both inputs are deeply equal, otherwise a DiffResult de |
29
28
  | `equalsDeep` | Recursive structural object equality. Boolean wrapper around diff \u2014 returns `true` when the tw |
30
29
  | `equalsShallow` | One-level (shallow) object equality. Two objects are equal when they share the exact same set of ow |
31
- | `get` | Gets a value from an object using a dot-notated path |
30
+ | `get` | Gets a value from an object using a dot/bracket-notated path or explicit key array. **Two path form |
32
31
  | `groupBy` | Groups an array of items by a key derived from each item. A thin, typed wrapper around `Object.grou |
33
32
  | `invert` | Returns a new object with keys and values swapped. If multiple keys share the same value, the last o |
34
- | `isEmpty` | Checks if a plain object has no own enumerable string-keyed properties. Symbol-keyed properties are |
35
- | `isNonEmpty` | Checks if a plain object has at least one own enumerable string-keyed property. Symbol-keyed proper |
33
+ | `isEmpty` | Checks if a plain object has no own enumerable string-keyed properties. `null` and `undefined` are t |
34
+ | `isNonEmpty` | Checks if a plain object has at least one own enumerable string-keyed property. `null` and `undefine |
36
35
  | `map` | Transforms the values and/or keys of a plain object in a single pass. Both callbacks are optional a |
36
+ | `mergeDeep` | Merges two or more objects deeply, returning a **new** object without mutating any input. Sources a |
37
37
  | `omit` | Creates a new object without the specified keys. |
38
38
  | `pick` | Creates a new object with only the specified keys. |
39
39
  | `removeUndefinedNull` | Remove null and undefined values from an object. |
40
40
  | `safeJsonParse` | Parses a JSON string, returning `null` (or a fallback) on any parse failure. Unlike `JSON.parse`, t |
41
- | `set` | Sets a value in an object using a dot-notated path |
41
+ | `set` | Sets a value in an object at the given path, creating intermediate objects as needed. **Two path fo |
42
42
 
43
43
  ---
44
44
 
45
45
  ## API Reference
46
46
 
47
+ ### `cloneDeep`
48
+
49
+ Creates a deep copy of an object or array.
50
+
51
+ Recursively clones own enumerable string keys. `Date` instances are
52
+ reconstructed with the same epoch value. Prototype-polluting keys
53
+ (`__proto__`, `constructor`, `prototype`) are silently skipped.
54
+ Does **not** handle circular references.
55
+
56
+ **Limitation:** symbol-keyed own properties are **not** copied — only string
57
+ keys are processed. Use `structuredClone` if symbol propagation is required.
58
+
59
+ ```typescript
60
+ import { cloneDeep } from '@helpers4/object';
61
+
62
+ cloneDeep<T>(obj: T): T
63
+ ```
64
+
65
+ **Parameters:**
66
+
67
+ - `obj: T` — The value to clone
68
+
69
+ **Returns:** `T` — A deep copy of `obj`
70
+
71
+ **Examples:**
72
+
73
+ *Clone a nested object*
74
+
75
+ Creates a deep copy — modifying the clone does not affect the original.
76
+
77
+ ```typescript
78
+ const original = { a: { b: 1 } };
79
+ const cloned = cloneDeep(original);
80
+ cloned.a.b = 2;
81
+ // original.a.b is still 1
82
+ ```
83
+
84
+ ---
85
+
47
86
  ### `compact`
48
87
 
49
88
  Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an object.
@@ -106,93 +145,6 @@ compact({ name: 'Alice', email: '', age: 0, role: 'admin' })
106
145
 
107
146
  ---
108
147
 
109
- ### `deepClone`
110
-
111
- Creates a deep copy of an object or array
112
-
113
- ```typescript
114
- import { deepClone } from '@helpers4/object';
115
-
116
- deepClone<T>(obj: T): T
117
- ```
118
-
119
- **Parameters:**
120
-
121
- - `obj: T` — The object to clone
122
-
123
- **Returns:** `T` — Deep cloned object
124
-
125
- **Examples:**
126
-
127
- *Clone a nested object*
128
-
129
- Creates a deep copy — modifying the clone does not affect the original.
130
-
131
- ```typescript
132
- const original = { a: { b: 1 } };
133
- const cloned = deepClone(original);
134
- cloned.a.b = 2;
135
- // original.a.b is still 1
136
- ```
137
-
138
- ---
139
-
140
- ### `deepMerge`
141
-
142
- Merges two or more objects deeply
143
-
144
- ```typescript
145
- import { deepMerge } from '@helpers4/object';
146
-
147
- deepMerge<T extends Record<string, unknown>>(target: T, sources: Record<string, unknown>[]): T
148
- ```
149
-
150
- **Parameters:**
151
-
152
- - `target: T` — The target object
153
- - `sources: Record<string, unknown>[]` — The source objects to merge
154
-
155
- **Returns:** `T` — The merged object
156
-
157
- ```typescript
158
- import { deepMerge } from '@helpers4/object';
159
-
160
- deepMerge(target: undefined, sources: Record<string, unknown>[]): undefined
161
- ```
162
-
163
- **Parameters:**
164
-
165
- - `target: undefined` — The target object
166
- - `sources: Record<string, unknown>[]` — The source objects to merge
167
-
168
- **Returns:** `undefined` — The merged object
169
-
170
- ```typescript
171
- import { deepMerge } from '@helpers4/object';
172
-
173
- deepMerge(target: null, sources: Record<string, unknown>[]): null
174
- ```
175
-
176
- **Parameters:**
177
-
178
- - `target: null` — The target object
179
- - `sources: Record<string, unknown>[]` — The source objects to merge
180
-
181
- **Returns:** `null` — The merged object
182
-
183
- **Examples:**
184
-
185
- *Merge two objects deeply*
186
-
187
- Recursively merges source properties into the target object.
188
-
189
- ```typescript
190
- deepMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 })
191
- // => { a: 1, b: { c: 2, d: 3 }, e: 4 }
192
- ```
193
-
194
- ---
195
-
196
148
  ### `diff`
197
149
 
198
150
  Structural object diff.
@@ -338,21 +290,64 @@ equalsShallow({ a: 1, b: 2 }, { a: 1, b: 2 })
338
290
 
339
291
  ### `get`
340
292
 
341
- Gets a value from an object using a dot-notated path
293
+ Gets a value from an object using a dot/bracket-notated path or explicit key array.
294
+
295
+ **Two path forms are supported:**
296
+
297
+ 1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)
298
+ are both accepted and mixed freely.
299
+ - Dot segments are always string keys: `'a.1'` → key `'1'` (string).
300
+ - Bracket segments are always number keys: `'a[1]'` → key `1` (number).
301
+ - String literal paths give **full compile-time type inference** on the return type.
302
+ - Dynamic (non-literal) strings return `unknown`.
303
+
304
+ 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
305
+ no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
306
+ `get(obj, ['a', 'b'] as const)` infers the return type from the path.
307
+
308
+ Both forms support **all objects** (plain objects, arrays, class instances).
309
+ Symbol keys are only reachable via the key-array form.
342
310
 
343
311
  ```typescript
344
312
  import { get } from '@helpers4/object';
345
313
 
346
- get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined
314
+ get<D>(obj: null | undefined, path: string | readonly PropertyKey[], defaultValue: D): D
347
315
  ```
348
316
 
349
317
  **Parameters:**
350
318
 
351
- - `obj: unknown` — The object to get value from
352
- - `path: string` — The dot-notated path (e.g., 'a.b.c')
353
- - `defaultValue?: T` — Default value if path doesn't exist
319
+ - `obj: null | undefined` — The object to read from
320
+ - `path: string | readonly PropertyKey[]` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
321
+ - `defaultValue: D` — Returned when the path is absent or resolves to `undefined`
322
+
323
+ **Returns:** `D` — The value at the path, or `defaultValue`
324
+
325
+ ```typescript
326
+ import { get } from '@helpers4/object';
327
+
328
+ get(obj: null | undefined, path: string | readonly PropertyKey[]): undefined
329
+ ```
330
+
331
+ **Parameters:**
354
332
 
355
- **Returns:** `T | undefined` — The value at the path or default value
333
+ - `obj: null | undefined` — The object to read from
334
+ - `path: string | readonly PropertyKey[]` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
335
+
336
+ **Returns:** `undefined` — The value at the path, or `defaultValue`
337
+
338
+ ```typescript
339
+ import { get } from '@helpers4/object';
340
+
341
+ get<T extends object, P extends string | readonly PropertyKey[]>(obj: T | null | undefined, path: P, defaultValue?: DeepGet<T, ParsePath<P>>): DeepGet<T, ParsePath<P>> | undefined
342
+ ```
343
+
344
+ **Parameters:**
345
+
346
+ - `obj: T | null | undefined` — The object to read from
347
+ - `path: P` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
348
+ - `defaultValue?: DeepGet<T, ParsePath<P>>` — Returned when the path is absent or resolves to `undefined`
349
+
350
+ **Returns:** `DeepGet<T, ParsePath<P>> | undefined` — The value at the path, or `defaultValue`
356
351
 
357
352
  **Examples:**
358
353
 
@@ -374,6 +369,16 @@ get({ a: 1 }, 'b.c', 'default')
374
369
  // => 'default'
375
370
  ```
376
371
 
372
+ *Get via key array (supports symbols)*
373
+
374
+ Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.
375
+
376
+ ```typescript
377
+ const id = Symbol('id')
378
+ get({ [id]: 'alice' }, [id])
379
+ // => 'alice'
380
+ ```
381
+
377
382
  ---
378
383
 
379
384
  ### `groupBy`
@@ -382,16 +387,19 @@ Groups an array of items by a key derived from each item.
382
387
 
383
388
  A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
384
389
  older targets and provides stricter return-type inference.
390
+ `null` and `undefined` are treated as empty arrays and return `{}`.
391
+ Items whose computed key is a prototype-polluting string (`__proto__`,
392
+ `constructor`, `prototype`) are silently skipped.
385
393
 
386
394
  ```typescript
387
395
  import { groupBy } from '@helpers4/object';
388
396
 
389
- groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: function): Partial<Record<K, T[]>>
397
+ groupBy<T, K extends PropertyKey>(items: readonly T[] | null | undefined, keyFn: function): Partial<Record<K, T[]>>
390
398
  ```
391
399
 
392
400
  **Parameters:**
393
401
 
394
- - `items: readonly T[]` — The array to group
402
+ - `items: readonly T[] | null | undefined` — The array to group
395
403
  - `keyFn: function` — Function that returns the group key for each item
396
404
 
397
405
  **Returns:** `Partial<Record<K, T[]>>` — A record mapping each key to the array of items with that key
@@ -427,16 +435,19 @@ groupBy(users, u => u.role)
427
435
 
428
436
  Returns a new object with keys and values swapped.
429
437
  If multiple keys share the same value, the last one wins.
438
+ `null` and `undefined` are treated as empty objects and return `{}`.
439
+ Entries whose source key **or** value is a prototype-polluting string
440
+ (`__proto__`, `constructor`, `prototype`) are silently skipped.
430
441
 
431
442
  ```typescript
432
443
  import { invert } from '@helpers4/object';
433
444
 
434
- invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K>
445
+ invert<K extends string, V extends PropertyKey>(obj: Record<K, V> | null | undefined): Record<V, K>
435
446
  ```
436
447
 
437
448
  **Parameters:**
438
449
 
439
- - `obj: Record<K, V>` — The object whose keys and values are to be swapped
450
+ - `obj: Record<K, V> | null | undefined` — The object whose keys and values are to be swapped
440
451
 
441
452
  **Returns:** `Record<V, K>` — A new object with values as keys and original keys as values
442
453
 
@@ -467,6 +478,7 @@ LABEL_TO_CODE['OK']; // => '200'
467
478
  ### `isEmpty`
468
479
 
469
480
  Checks if a plain object has no own enumerable string-keyed properties.
481
+ `null` and `undefined` are treated as empty objects and return `true`.
470
482
 
471
483
  Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
472
484
  separately if symbol keys matter for your use case.
@@ -474,14 +486,14 @@ separately if symbol keys matter for your use case.
474
486
  ```typescript
475
487
  import { isEmpty } from '@helpers4/object';
476
488
 
477
- isEmpty(value: Record<PropertyKey, unknown>): boolean
489
+ isEmpty(value: Record<PropertyKey, unknown> | null | undefined): boolean
478
490
  ```
479
491
 
480
492
  **Parameters:**
481
493
 
482
- - `value: Record<PropertyKey, unknown>` — The object to check
494
+ - `value: Record<PropertyKey, unknown> | null | undefined` — The object to check
483
495
 
484
- **Returns:** `boolean` — `true` if the object has no own enumerable string-keyed properties
496
+ **Returns:** `boolean` — `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`
485
497
 
486
498
  **Examples:**
487
499
 
@@ -510,6 +522,7 @@ isEmpty(obj) // => true (only string keys are counted)
510
522
  ### `isNonEmpty`
511
523
 
512
524
  Checks if a plain object has at least one own enumerable string-keyed property.
525
+ `null` and `undefined` are treated as empty objects and return `false`.
513
526
 
514
527
  Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
515
528
  separately if symbol keys matter for your use case.
@@ -517,14 +530,15 @@ separately if symbol keys matter for your use case.
517
530
  ```typescript
518
531
  import { isNonEmpty } from '@helpers4/object';
519
532
 
520
- isNonEmpty(value: Record<PropertyKey, unknown>): boolean
533
+ isNonEmpty(value: Record<PropertyKey, unknown> | null | undefined): value is Record<PropertyKey, unknown>
521
534
  ```
522
535
 
523
536
  **Parameters:**
524
537
 
525
- - `value: Record<PropertyKey, unknown>` — The object to check
538
+ - `value: Record<PropertyKey, unknown> | null | undefined` — The object to check
526
539
 
527
- **Returns:** `boolean` — `true` if the object has at least one own enumerable string-keyed property
540
+ **Returns:** `value is Record<PropertyKey, unknown>` — `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
541
+ Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.
528
542
 
529
543
  **Examples:**
530
544
 
@@ -565,17 +579,18 @@ When `mapValue` is omitted the original values are preserved;
565
579
  when `mapKey` is omitted the original keys are preserved.
566
580
 
567
581
  Note: if two different keys map to the same output key the last one wins
568
- (insertion order).
582
+ (insertion order). Entries whose mapped key is a prototype-polluting string
583
+ (`__proto__`, `constructor`, `prototype`) are silently skipped.
569
584
 
570
585
  ```typescript
571
586
  import { map } from '@helpers4/object';
572
587
 
573
- map<TObj extends Record<string, unknown>, TVal = indexedAccess, TKey extends PropertyKey = keyof TObj>(obj: TObj, mapValue?: function, mapKey?: function): Record<TKey, TVal>
588
+ map<TObj extends Record<string, unknown>, TVal = indexedAccess, TKey extends PropertyKey = keyof TObj>(obj: TObj | null | undefined, mapValue?: function, mapKey?: function): Record<TKey, TVal>
574
589
  ```
575
590
 
576
591
  **Parameters:**
577
592
 
578
- - `obj: TObj` — The source object
593
+ - `obj: TObj | null | undefined` — The source object
579
594
  - `mapValue?: function` — Callback called with `(value, key)` for each entry.
580
595
  Defaults to identity.
581
596
  - `mapKey?: function` — Callback called with `(key, value)` for each entry.
@@ -618,6 +633,67 @@ map(
618
633
 
619
634
  ---
620
635
 
636
+ ### `mergeDeep`
637
+
638
+ Merges two or more objects deeply, returning a **new** object without mutating any input.
639
+
640
+ Sources are applied left to right. For each key:
641
+ - If both sides are plain objects → merged recursively into a new object.
642
+ - Otherwise → **later source wins** (the earlier value is overwritten).
643
+ - `undefined` in a source **never overwrites** an existing value.
644
+ - `null` in a source **does overwrite** and prevents a later source from
645
+ deep-merging into that key: `mergeDeep({ a: { x: 1 } }, { a: null }, { a: { y: 2 } })`
646
+ → `{ a: { y: 2 } }` — `x: 1` is permanently lost.
647
+ - Arrays, class instances, and all non-plain-object values are **replaced**, not merged.
648
+
649
+ Own enumerable string and symbol keys of each source are processed at the top level;
650
+ inherited and non-enumerable properties are skipped. Prototype-polluting keys
651
+ (`__proto__`, `constructor`, `prototype`) are silently ignored.
652
+ **Note:** symbol keys inside nested plain-object values are not preserved — they are
653
+ lost when those values are deep-cloned. Only top-level symbol keys survive.
654
+
655
+ **TypeScript return type — intersection semantics**
656
+
657
+ The return type is `A & B & C …` (intersection of all source types). This is accurate
658
+ when keys are disjoint or share the same type:
659
+ ```ts
660
+ mergeDeep({ a: 1 }, { b: 'x' }) // { a: number } & { b: string } ✓
661
+ mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: number } & { c: number } } ✓
662
+ ```
663
+ When the same key carries **incompatible types** across sources, the intersection
664
+ resolves to `never` for that key — TypeScript surfaces the conflict rather than
665
+ silently picking a type:
666
+ ```ts
667
+ mergeDeep({ a: 1 }, { a: 'x' }) // { a: never } ← type conflict detected
668
+ ```
669
+ At runtime the later source always wins (`'x'`), but the `never` type signals that
670
+ the caller should align their source types. If intentional, cast the result.
671
+
672
+ ```typescript
673
+ import { mergeDeep } from '@helpers4/object';
674
+
675
+ mergeDeep<T extends [object, rest]>(sources: [rest]): MergeResult<T>
676
+ ```
677
+
678
+ **Parameters:**
679
+
680
+ - `sources: [rest]` — Two or more objects to merge (none are mutated)
681
+
682
+ **Returns:** `MergeResult<T>` — A new object that is the deep merge of all sources
683
+
684
+ **Examples:**
685
+
686
+ *Merge two objects deeply*
687
+
688
+ Returns a new object — neither input is mutated.
689
+
690
+ ```typescript
691
+ mergeDeep({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 })
692
+ // => { a: 1, b: { c: 2, d: 3 }, e: 4 }
693
+ ```
694
+
695
+ ---
696
+
621
697
  ### `omit`
622
698
 
623
699
  Creates a new object without the specified keys.
@@ -868,31 +944,72 @@ safeJsonParse('invalid', [])
868
944
 
869
945
  ### `set`
870
946
 
871
- Sets a value in an object using a dot-notated path
947
+ Sets a value in an object at the given path, creating intermediate objects as needed.
948
+
949
+ **Two path forms are supported:**
950
+
951
+ 1. **String path** — dot notation and bracket notation, mixed freely.
952
+ - Dot segments are always string keys: `'layers.1.name'` → keys `['layers', '1', 'name']`.
953
+ - Bracket segments are always number keys: `'layers[1].name'` → keys `['layers', 1, 'name']`.
954
+ - String literal paths give **full compile-time type inference** on the return type.
955
+ - Dynamic (non-literal) strings return `T` (same object type).
956
+
957
+ 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
958
+ no parsing performed. Enables symbol-keyed access and precise return-type inference.
959
+
960
+ Both forms support **all objects** (plain objects, arrays, class instances).
961
+ Symbol keys are only reachable via the key-array form.
962
+
963
+ Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
964
+ Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
965
+ is rejected and the original object is returned unchanged (prototype-pollution guard).
872
966
 
873
967
  ```typescript
874
968
  import { set } from '@helpers4/object';
875
969
 
876
- set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>
970
+ set<T extends object, P extends string | readonly PropertyKey[], V extends unknown>(obj: T, path: P, value: V): conditional
877
971
  ```
878
972
 
879
973
  **Parameters:**
880
974
 
881
- - `obj: Record<string, unknown>` — The object to set value in
882
- - `path: string` — The dot-notated path (e.g., 'a.b.c')
883
- - `value: unknown` — The value to set
975
+ - `obj: T` — The object to mutate
976
+ - `path: P` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
977
+ - `value: V` — Value to assign at the path
884
978
 
885
- **Returns:** `Record<string, unknown>` — The modified object
979
+ **Returns:** `conditional` — The mutated object (same reference, narrowed type)
886
980
 
887
981
  **Examples:**
888
982
 
889
- *Set a nested property*
983
+ *Set a nested property (dot notation)*
890
984
 
891
- Creates intermediate objects as needed along the dot-notated path.
985
+ Creates intermediate objects as needed. All segments are string keys — including numeric-looking ones like "1".
892
986
 
893
987
  ```typescript
894
988
  set({}, 'a.b.c', 42)
895
989
  // => { a: { b: { c: 42 } } }
990
+
991
+ set({}, 'layers.1.name', 'bg')
992
+ // => { layers: { '1': { name: 'bg' } } } // '1' is a string key
993
+ ```
994
+
995
+ *Set via bracket notation*
996
+
997
+ Square-bracket indices become numeric keys. Useful when the path targets an array element.
998
+
999
+ ```typescript
1000
+ const obj = { layers: [{}, { name: 'old' }] }
1001
+ set(obj, 'layers[1].name', 'new')
1002
+ // => { layers: [{}, { name: 'new' }] }
1003
+ ```
1004
+
1005
+ *Set via key array (supports symbols)*
1006
+
1007
+ Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.
1008
+
1009
+ ```typescript
1010
+ const id = Symbol('id')
1011
+ set({}, ['user', id], 'alice')
1012
+ // => { user: { [id]: 'alice' } }
896
1013
  ```
897
1014
 
898
1015
  ---