@simplysm/core-common 13.0.29 → 13.0.30

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 CHANGED
@@ -52,8 +52,10 @@ To use extension methods (`getOrCreate()`, `toggle()`, etc.), you must import th
52
52
  - [`objMerge`](docs/utils.md#objmerge) - Deep merge (source + target)
53
53
  - [`objMerge3`](docs/utils.md#objmerge3) - 3-way merge with conflict detection
54
54
  - [`objOmit`](docs/utils.md#objomit) - Exclude specific keys
55
+ - [`objOmitByFilter`](docs/utils.md#objomitbyfilter) - Exclude keys matching a predicate
55
56
  - [`objPick`](docs/utils.md#objpick) - Select specific keys
56
57
  - [`objGetChainValue`](docs/utils.md#objgetchainvalue) - Query value by chain path
58
+ - [`objGetChainValueByDepth`](docs/utils.md#objgetchainvaluebydepth) - Query value by descending the same key repeatedly
57
59
  - [`objSetChainValue`](docs/utils.md#objsetchainvalue) - Set value by chain path
58
60
  - [`objDeleteChainValue`](docs/utils.md#objdeletechainvalue) - Delete value by chain path
59
61
  - [`objKeys`](docs/utils.md#objkeys) - Type-safe `Object.keys`
@@ -94,6 +96,7 @@ To use extension methods (`getOrCreate()`, `toggle()`, etc.), you must import th
94
96
 
95
97
  - [`formatDate`](docs/utils.md#formatdate) - Convert date/time to formatted string
96
98
  - [`normalizeMonth`](docs/utils.md#normalizemonth) - Normalize year/month/day when setting month
99
+ - [`convert12To24`](docs/utils.md#convert12to24) - Convert 12-hour to 24-hour format
97
100
 
98
101
  ### Byte Utilities
99
102
 
@@ -208,9 +211,15 @@ To use extension methods (`getOrCreate()`, `toggle()`, etc.), you must import th
208
211
  - [`Type`](docs/types.md#type) - Constructor type
209
212
  - [`ObjUndefToOptional`](docs/types.md#objundeftooptional) - Convert `undefined` properties to optional
210
213
  - [`ObjOptionalToUndef`](docs/types.md#objoptionaltoundef) - Convert optional properties to `required + undefined`
214
+ - [`EqualOptions`](docs/types.md#equaloptions) - Options for `objEqual`
215
+ - [`ObjMergeOptions`](docs/types.md#objmergeoptions) - Options for `objMerge`
216
+ - [`ObjMerge3KeyOptions`](docs/types.md#objmerge3keyoptions) - Per-key options for `objMerge3`
217
+ - [`DtNormalizedMonth`](docs/types.md#dtnormalizedmonth) - Return type of `normalizeMonth`
218
+ - [`ZipArchiveProgress`](docs/types.md#ziparchiveprogress) - Progress info for `ZipArchive.extractAll`
211
219
  - [`ArrayDiffsResult`](docs/types.md#arraydiffsresult) - Result type of `Array.diffs()`
212
220
  - [`ArrayDiffs2Result`](docs/types.md#arraydiffs2result) - Result type of `Array.oneWayDiffs()`
213
221
  - [`TreeArray`](docs/types.md#treearray) - Result type of `Array.toTree()`
222
+ - [`ComparableType`](docs/types.md#comparabletype) - Union of types usable for sorting/comparison
214
223
 
215
224
  ## Caveats
216
225
 
@@ -15,6 +15,7 @@ import "@simplysm/core-common";
15
15
 
16
16
  const users = [{ id: 1, name: "Alice" }];
17
17
  users.single((u) => u.id === 1); // { id: 1, name: "Alice" }
18
+ users.single(); // Returns the only element (throws if 2+)
18
19
  ```
19
20
 
20
21
  #### first
@@ -22,7 +23,8 @@ users.single((u) => u.id === 1); // { id: 1, name: "Alice" }
22
23
  Return first element.
23
24
 
24
25
  ```typescript
25
- users.first(); // { id: 1, name: "Alice" }
26
+ users.first(); // First element (or undefined)
27
+ users.first((u) => u.id > 0); // First matching element
26
28
  ```
27
29
 
28
30
  #### last
@@ -30,7 +32,8 @@ users.first(); // { id: 1, name: "Alice" }
30
32
  Return last element.
31
33
 
32
34
  ```typescript
33
- users.last(); // Last user
35
+ users.last(); // Last element (or undefined)
36
+ users.last((u) => u.id > 0); // Last matching element
34
37
  ```
35
38
 
36
39
  ---
@@ -51,14 +54,23 @@ Filter by type (`PrimitiveTypeStr` or constructor).
51
54
 
52
55
  ```typescript
53
56
  import "@simplysm/core-common";
57
+
58
+ // Filter by PrimitiveTypeStr
59
+ mixed.ofType("string"); // string[]
60
+ mixed.ofType("DateTime"); // DateTime[]
61
+
62
+ // Filter by constructor
63
+ mixed.ofType(MyClass); // MyClass[]
54
64
  ```
55
65
 
56
66
  #### filterAsync
57
67
 
58
- Async filter.
68
+ Async filter (sequential execution).
59
69
 
60
70
  ```typescript
61
71
  import "@simplysm/core-common";
72
+
73
+ const activeUsers = await users.filterAsync(async (u) => await isActive(u.id));
62
74
  ```
63
75
 
64
76
  ---
@@ -75,18 +87,26 @@ await ids.mapAsync(async (id) => await fetchUser(id));
75
87
 
76
88
  #### mapMany
77
89
 
78
- flat + filterExists.
90
+ Flatten nested arrays and remove null/undefined.
79
91
 
80
92
  ```typescript
81
93
  import "@simplysm/core-common";
94
+
95
+ // Flatten existing nested array
96
+ [[1, 2], [3, 4]].mapMany(); // [1, 2, 3, 4]
97
+
98
+ // Map then flatten
99
+ users.mapMany((u) => u.tags); // all tags from all users
82
100
  ```
83
101
 
84
102
  #### mapManyAsync
85
103
 
86
- Async mapMany.
104
+ Async mapMany (sequential execution).
87
105
 
88
106
  ```typescript
89
107
  import "@simplysm/core-common";
108
+
109
+ await groups.mapManyAsync(async (g) => await fetchMembers(g.id));
90
110
  ```
91
111
 
92
112
  #### parallelAsync
@@ -103,7 +123,7 @@ await ids.parallelAsync(async (id) => await fetchUser(id));
103
123
 
104
124
  #### groupBy
105
125
 
106
- Group by key.
126
+ Group by key. Supports primitive keys (O(n)) and object keys (O(n²)).
107
127
 
108
128
  ```typescript
109
129
  const users = [
@@ -114,22 +134,29 @@ const users = [
114
134
 
115
135
  users.groupBy((u) => u.dept);
116
136
  // [{ key: "dev", values: [...] }, { key: "hr", values: [...] }]
137
+
138
+ // With value selector
139
+ users.groupBy((u) => u.dept, (u) => u.name);
140
+ // [{ key: "dev", values: ["Alice", "Bob"] }, { key: "hr", values: ["Charlie"] }]
117
141
  ```
118
142
 
119
143
  #### toMap
120
144
 
121
- Convert to Map (error on duplicate key).
145
+ Convert to Map (throws on duplicate key).
122
146
 
123
147
  ```typescript
124
- users.toMap((u) => u.id); // Map<number, User>
148
+ users.toMap((u) => u.id); // Map<number, User>
149
+ users.toMap((u) => u.id, (u) => u.name); // Map<number, string>
125
150
  ```
126
151
 
127
152
  #### toMapAsync
128
153
 
129
- Async Map conversion.
154
+ Async Map conversion (sequential execution).
130
155
 
131
156
  ```typescript
132
157
  import "@simplysm/core-common";
158
+
159
+ await items.toMapAsync(async (item) => await resolveKey(item));
133
160
  ```
134
161
 
135
162
  #### toArrayMap
@@ -146,30 +173,47 @@ Convert to `Map<K, Set<V>>`.
146
173
 
147
174
  ```typescript
148
175
  import "@simplysm/core-common";
176
+
177
+ users.toSetMap((u) => u.dept); // Map<string, Set<User>>
149
178
  ```
150
179
 
151
180
  #### toMapValues
152
181
 
153
- Aggregate Map by group.
182
+ Group by key and aggregate each group with a value selector.
154
183
 
155
184
  ```typescript
156
185
  import "@simplysm/core-common";
186
+
187
+ // Sum scores per department
188
+ users.toMapValues(
189
+ (u) => u.dept,
190
+ (group) => group.sum((u) => u.score),
191
+ ); // Map<string, number>
157
192
  ```
158
193
 
159
194
  #### toObject
160
195
 
161
- Convert to `Record<string, V>`.
196
+ Convert to `Record<string, V>` (throws on duplicate key).
162
197
 
163
198
  ```typescript
164
199
  import "@simplysm/core-common";
200
+
201
+ users.toObject((u) => u.name); // Record<string, User>
202
+ users.toObject((u) => u.name, (u) => u.id); // Record<string, number>
165
203
  ```
166
204
 
167
205
  #### toTree
168
206
 
169
- Convert to tree structure.
207
+ Convert flat array to tree structure.
170
208
 
171
209
  ```typescript
172
210
  import "@simplysm/core-common";
211
+
212
+ interface Category { id: number; parentId?: number; name: string }
213
+
214
+ const tree = categories.toTree("id", "parentId");
215
+ // Nodes with null/undefined parentId become roots
216
+ // Each node gains a `children: TreeArray<Category>[]` property
173
217
  ```
174
218
 
175
219
  ---
@@ -181,15 +225,32 @@ import "@simplysm/core-common";
181
225
  Remove duplicates (return new array).
182
226
 
183
227
  ```typescript
228
+ // Primitive values
184
229
  [1, 2, 2, 3, 3].distinct(); // [1, 2, 3]
230
+
231
+ // Object deep equality (O(n²))
232
+ objects.distinct();
233
+
234
+ // Reference equality (O(n)) — fastest
235
+ objects.distinct(true);
236
+ objects.distinct({ matchAddress: true });
237
+
238
+ // Custom key function (O(n)) — recommended for large arrays of objects
239
+ objects.distinct({ keyFn: (item) => item.id });
185
240
  ```
186
241
 
187
242
  #### distinctThis
188
243
 
189
- Remove duplicates (modify original).
244
+ Remove duplicates (modify original array).
190
245
 
191
246
  ```typescript
192
247
  import "@simplysm/core-common";
248
+
249
+ const arr = [1, 2, 2, 3];
250
+ arr.distinctThis(); // arr is now [1, 2, 3]
251
+
252
+ // Same options as distinct
253
+ arr.distinctThis({ keyFn: (item) => item.id });
193
254
  ```
194
255
 
195
256
  ---
@@ -201,7 +262,8 @@ import "@simplysm/core-common";
201
262
  Ascending sort (return new array).
202
263
 
203
264
  ```typescript
204
- users.orderBy((u) => u.name);
265
+ users.orderBy((u) => u.name); // Sort by name ascending
266
+ numbers.orderBy(); // Sort numbers ascending
205
267
  ```
206
268
 
207
269
  #### orderByDesc
@@ -214,18 +276,22 @@ users.orderByDesc((u) => u.id);
214
276
 
215
277
  #### orderByThis
216
278
 
217
- Ascending sort (modify original).
279
+ Ascending sort (modify original array).
218
280
 
219
281
  ```typescript
220
282
  import "@simplysm/core-common";
283
+
284
+ arr.orderByThis((item) => item.name);
221
285
  ```
222
286
 
223
287
  #### orderByDescThis
224
288
 
225
- Descending sort (modify original).
289
+ Descending sort (modify original array).
226
290
 
227
291
  ```typescript
228
292
  import "@simplysm/core-common";
293
+
294
+ arr.orderByDescThis((item) => item.score);
229
295
  ```
230
296
 
231
297
  ---
@@ -234,26 +300,57 @@ import "@simplysm/core-common";
234
300
 
235
301
  #### diffs
236
302
 
237
- Compare differences between two arrays.
303
+ Compare differences between two arrays. Returns INSERT/DELETE/UPDATE entries.
238
304
 
239
305
  ```typescript
240
306
  import "@simplysm/core-common";
307
+
308
+ const result = oldList.diffs(newList, { keys: ["id"] });
309
+ for (const diff of result) {
310
+ if (diff.source === undefined) { /* target-only: INSERT */ }
311
+ else if (diff.target === undefined) { /* source-only: DELETE */ }
312
+ else { /* both exist: UPDATE */ }
313
+ }
314
+
315
+ // Exclude certain fields from equality check
316
+ oldList.diffs(newList, { keys: ["id"], excludes: ["updatedAt"] });
241
317
  ```
242
318
 
243
319
  #### oneWayDiffs
244
320
 
245
- One-way diff comparison (create/update/same).
321
+ One-way diff comparison (create/update/same). Compares current items against original items.
246
322
 
247
323
  ```typescript
248
324
  import "@simplysm/core-common";
325
+
326
+ const diffs = currentItems.oneWayDiffs(originalItems, "id");
327
+ // { type: "create", item, orgItem: undefined }
328
+ // { type: "update", item, orgItem }
329
+ // { type: "same", item, orgItem } (only when includeSame: true)
330
+
331
+ // With options
332
+ currentItems.oneWayDiffs(originalItems, "id", {
333
+ includeSame: true, // Include unchanged items in result
334
+ excludes: ["updatedAt"], // Ignore these fields when checking for changes
335
+ includes: ["name"], // Only compare these fields
336
+ });
337
+
338
+ // Pass a pre-built Map for O(1) lookup
339
+ const orgMap = originalItems.toMap((item) => item.id);
340
+ currentItems.oneWayDiffs(orgMap, "id");
249
341
  ```
250
342
 
251
343
  #### merge
252
344
 
253
- Merge arrays.
345
+ Merge arrays: apply target changes onto source.
254
346
 
255
347
  ```typescript
256
348
  import "@simplysm/core-common";
349
+
350
+ const merged = source.merge(target, { keys: ["id"] });
351
+ // Items present in both: merged with objMerge
352
+ // Items only in target: appended
353
+ // Items only in source: kept as-is
257
354
  ```
258
355
 
259
356
  ---
@@ -262,26 +359,35 @@ import "@simplysm/core-common";
262
359
 
263
360
  #### sum
264
361
 
265
- Sum.
362
+ Sum values.
266
363
 
267
364
  ```typescript
268
365
  import "@simplysm/core-common";
366
+
367
+ [1, 2, 3].sum(); // 6
368
+ users.sum((u) => u.score); // total score
269
369
  ```
270
370
 
271
371
  #### min
272
372
 
273
- Minimum.
373
+ Minimum value.
274
374
 
275
375
  ```typescript
276
376
  import "@simplysm/core-common";
377
+
378
+ [3, 1, 2].min(); // 1
379
+ users.min((u) => u.age); // youngest age
277
380
  ```
278
381
 
279
382
  #### max
280
383
 
281
- Maximum.
384
+ Maximum value.
282
385
 
283
386
  ```typescript
284
387
  import "@simplysm/core-common";
388
+
389
+ [3, 1, 2].max(); // 3
390
+ users.max((u) => u.score); // highest score
285
391
  ```
286
392
 
287
393
  ---
@@ -290,34 +396,44 @@ import "@simplysm/core-common";
290
396
 
291
397
  #### insert
292
398
 
293
- Insert at specific position.
399
+ Insert at specific position (modify original).
294
400
 
295
401
  ```typescript
296
402
  import "@simplysm/core-common";
403
+
404
+ const arr = [1, 2, 3];
405
+ arr.insert(1, 10, 11); // [1, 10, 11, 2, 3]
297
406
  ```
298
407
 
299
408
  #### remove
300
409
 
301
- Remove item.
410
+ Remove item or items matching predicate (modify original).
302
411
 
303
412
  ```typescript
304
413
  import "@simplysm/core-common";
414
+
415
+ arr.remove(2); // Remove by value
416
+ arr.remove((item) => item > 2); // Remove by predicate
305
417
  ```
306
418
 
307
419
  #### toggle
308
420
 
309
- Remove if exists, add if not.
421
+ Toggle item — remove if exists, add if not (modify original).
310
422
 
311
423
  ```typescript
312
424
  import "@simplysm/core-common";
425
+
426
+ arr.toggle(3); // removes 3 if present, adds if not
313
427
  ```
314
428
 
315
429
  #### clear
316
430
 
317
- Remove all items.
431
+ Remove all items (modify original).
318
432
 
319
433
  ```typescript
320
434
  import "@simplysm/core-common";
435
+
436
+ arr.clear(); // arr is now []
321
437
  ```
322
438
 
323
439
  #### shuffle
@@ -326,6 +442,8 @@ Shuffle (return new array).
326
442
 
327
443
  ```typescript
328
444
  import "@simplysm/core-common";
445
+
446
+ [1, 2, 3, 4, 5].shuffle(); // random order
329
447
  ```
330
448
 
331
449
  ---
@@ -339,21 +457,25 @@ If key doesn't exist, set new value and return.
339
457
  ```typescript
340
458
  const map = new Map<string, number[]>();
341
459
 
342
- // Create and return if value doesn't exist
343
- const arr = map.getOrCreate("key", []);
344
- arr.push(1);
460
+ // Direct value
461
+ map.getOrCreate("key", []);
345
462
 
346
- // Create with factory function (when computation is expensive)
347
- map.getOrCreate("key2", () => expensiveComputation());
463
+ // Factory function (called only when key is missing)
464
+ map.getOrCreate("key", () => expensiveComputation());
348
465
  ```
349
466
 
467
+ > **Note:** If the Map value type is a function (e.g., `Map<string, () => void>`), wrap the function in a factory to store it as a value: `map.getOrCreate("key", () => myFn)`.
468
+
350
469
  ### update
351
470
 
352
- Update value for key using function.
471
+ Update value for key using function. The update function receives `undefined` if the key doesn't exist.
353
472
 
354
473
  ```typescript
355
474
  const countMap = new Map<string, number>();
356
475
  countMap.update("key", (v) => (v ?? 0) + 1); // Increment counter
476
+
477
+ const arrayMap = new Map<string, string[]>();
478
+ arrayMap.update("key", (v) => [...(v ?? []), "item"]); // Append to array
357
479
  ```
358
480
 
359
481
  ---
@@ -371,11 +493,11 @@ set.adds(4, 5, 6); // {1, 2, 3, 4, 5, 6}
371
493
 
372
494
  ### toggle
373
495
 
374
- Toggle value (remove if exists, add if not).
496
+ Toggle value (remove if exists, add if not). Optionally force add or delete.
375
497
 
376
498
  ```typescript
377
- set.toggle(2); // 2 exists so remove -> {1, 3, 4, 5, 6}
378
- set.toggle(7); // 7 doesn't exist so add -> {1, 3, 4, 5, 6, 7}
379
- set.toggle(8, "add"); // Force add
380
- set.toggle(1, "del"); // Force delete
499
+ set.toggle(2); // 2 exists remove
500
+ set.toggle(7); // 7 not exists add
501
+ set.toggle(8, "add"); // Force add regardless of current state
502
+ set.toggle(1, "del"); // Force delete regardless of current state
381
503
  ```
package/docs/features.md CHANGED
@@ -9,27 +9,32 @@ Async debounce queue (executes only last request).
9
9
  ```typescript
10
10
  import { DebounceQueue } from "@simplysm/core-common";
11
11
 
12
- using queue = new DebounceQueue(300); // 300ms debounce
12
+ using queue = new DebounceQueue(300); // 300ms debounce delay
13
13
 
14
14
  // Error handling
15
15
  queue.on("error", (err) => console.error(err));
16
16
 
17
- // Only last call is executed
17
+ // Only last call is executed after the delay
18
18
  queue.run(() => console.log("1")); // Ignored
19
19
  queue.run(() => console.log("2")); // Ignored
20
20
  queue.run(() => console.log("3")); // Executed after 300ms
21
+
22
+ // Omit delay for immediate execution (next event loop tick)
23
+ const immediate = new DebounceQueue();
21
24
  ```
22
25
 
26
+ > **Note:** A request added while the queue is running is executed immediately after the current run completes (no additional delay). This prevents requests from being lost.
27
+
23
28
  ---
24
29
 
25
30
  ## SerialQueue
26
31
 
27
- Async serial queue (sequential execution).
32
+ Async serial queue (sequential execution). Each task starts only after the previous one completes.
28
33
 
29
34
  ```typescript
30
35
  import { SerialQueue } from "@simplysm/core-common";
31
36
 
32
- using queue = new SerialQueue(100); // 100ms interval between tasks
37
+ using queue = new SerialQueue(100); // 100ms gap between tasks (optional, default 0)
33
38
 
34
39
  queue.on("error", (err) => console.error(err));
35
40
 
@@ -38,6 +43,8 @@ queue.run(async () => { await fetch("/api/2"); }); // Runs after #1 completes
38
43
  queue.run(async () => { await fetch("/api/3"); }); // Runs after #2 completes
39
44
  ```
40
45
 
46
+ > **Note:** Errors in a task are caught and emitted (or logged). Subsequent tasks continue regardless.
47
+
41
48
  ---
42
49
 
43
50
  ## EventEmitter
@@ -61,10 +68,11 @@ class MyService extends EventEmitter<MyEvents> {
61
68
  }
62
69
 
63
70
  const service = new MyService();
64
- service.on("data", (data) => console.log(data)); // data: string (type inferred)
65
- service.off("data", listener); // Remove listener
66
- service.listenerCount("data"); // Number of registered listeners
67
- service.dispose(); // Remove all listeners
71
+ const listener = (data: string) => console.log(data);
72
+ service.on("data", listener); // Register listener (type: string inferred)
73
+ service.off("data", listener); // Remove specific listener
74
+ service.listenerCount("data"); // Number of registered listeners
75
+ service.dispose(); // Remove all listeners
68
76
  ```
69
77
 
70
78
  ---
@@ -78,13 +86,18 @@ import { ZipArchive } from "@simplysm/core-common";
78
86
 
79
87
  // Read ZIP file
80
88
  await using archive = new ZipArchive(zipBytes);
81
- const content = await archive.get("file.txt");
82
- const exists = await archive.exists("data.json");
83
89
 
84
- // Extract all (with progress)
90
+ // Get single file
91
+ const content = await archive.get("file.txt"); // Uint8Array | undefined
92
+ const exists = await archive.exists("data.json"); // boolean
93
+
94
+ // Extract all files (with optional progress callback)
85
95
  const files = await archive.extractAll((progress) => {
86
- console.log(`${progress.fileName}: ${progress.extractedSize}/${progress.totalSize}`);
96
+ // progress: { fileName: string; totalSize: number; extractedSize: number }
97
+ const pct = Math.round((progress.extractedSize / progress.totalSize) * 100);
98
+ console.log(`${progress.fileName}: ${pct}%`);
87
99
  });
100
+ // files: Map<string, Uint8Array | undefined>
88
101
 
89
102
  // Create ZIP file
90
103
  await using newArchive = new ZipArchive();
@@ -92,3 +105,5 @@ newArchive.write("file.txt", textBytes);
92
105
  newArchive.write("data.json", jsonBytes);
93
106
  const zipBytes = await newArchive.compress();
94
107
  ```
108
+
109
+ > **Note:** `compress()` loads all files into memory first. Be mindful of memory usage for large archives.
package/docs/types.md CHANGED
@@ -46,7 +46,7 @@ switch (type) {
46
46
 
47
47
  ### TimeoutError
48
48
 
49
- Timeout error.
49
+ Timeout error. Thrown automatically by `waitUntil` when max attempts are exceeded.
50
50
 
51
51
  ```typescript
52
52
  import { TimeoutError } from "@simplysm/core-common";
@@ -82,23 +82,33 @@ DateTime.parse("2025-01-15 오전 10:30:00"); // Korean AM/PM
82
82
  DateTime.parse("2025-01-15T10:30:00Z"); // ISO 8601
83
83
 
84
84
  // Properties (read-only)
85
- dt.year; // 2025
86
- dt.month; // 1 (1-12)
87
- dt.day; // 15
88
- dt.hour; // 10
89
- dt.minute; // 30
90
- dt.second; // 0
91
- dt.millisecond; // 0
92
- dt.tick; // Millisecond timestamp
93
- dt.dayOfWeek; // Day of week (Sun~Sat: 0~6)
94
- dt.isValid; // Validity check
85
+ dt.year; // 2025
86
+ dt.month; // 1 (1-12)
87
+ dt.day; // 15
88
+ dt.hour; // 10
89
+ dt.minute; // 30
90
+ dt.second; // 0
91
+ dt.millisecond; // 0
92
+ dt.tick; // Millisecond timestamp
93
+ dt.dayOfWeek; // Day of week (Sun~Sat: 0~6)
94
+ dt.timezoneOffsetMinutes; // Timezone offset in minutes (e.g. 540 for UTC+9)
95
+ dt.isValid; // Validity check
95
96
 
96
97
  // Immutable transformations (return new instances)
97
98
  dt.setYear(2026); // Change year
98
99
  dt.setMonth(3); // Change month (day auto-adjusted)
100
+ dt.setDay(1); // Change day
101
+ dt.setHour(9); // Change hour
102
+ dt.setMinute(0); // Change minute
103
+ dt.setSecond(0); // Change second
104
+ dt.setMillisecond(0); // Change millisecond
105
+ dt.addYears(1); // 1 year later
106
+ dt.addMonths(1); // 1 month later
99
107
  dt.addDays(7); // 7 days later
100
108
  dt.addHours(-2); // 2 hours ago
101
- dt.addMonths(1); // 1 month later
109
+ dt.addMinutes(30); // 30 minutes later
110
+ dt.addSeconds(10); // 10 seconds later
111
+ dt.addMilliseconds(500); // 500ms later
102
112
 
103
113
  // Formatting
104
114
  dt.toFormatString("yyyy-MM-dd"); // "2025-01-15"
@@ -120,20 +130,40 @@ const d = new DateOnly(2025, 1, 15);
120
130
  DateOnly.parse("2025-01-15"); // No timezone influence
121
131
  DateOnly.parse("20250115"); // No timezone influence
122
132
 
123
- // Immutable transformations
124
- d.addDays(30);
125
- d.addMonths(-1);
133
+ // Properties (read-only)
134
+ d.year; // 2025
135
+ d.month; // 1
136
+ d.day; // 15
137
+ d.tick; // Millisecond timestamp
138
+ d.dayOfWeek; // Day of week (Sun~Sat: 0~6)
139
+ d.isValid; // Validity check
140
+
141
+ // Immutable transformations (return new instances)
142
+ d.setYear(2026);
126
143
  d.setMonth(2); // Jan 31 -> Feb 28 (auto-adjusted)
144
+ d.setDay(1);
145
+ d.addYears(1);
146
+ d.addMonths(-1);
147
+ d.addDays(30);
127
148
 
128
- // Week calculation (ISO 8601 standard)
149
+ // Week calculation (ISO 8601 standard: Monday start, min 4 days in first week)
129
150
  d.getWeekSeqOfYear(); // { year: 2025, weekSeq: 3 }
130
151
  d.getWeekSeqOfMonth(); // { year: 2025, monthSeq: 1, weekSeq: 3 }
131
152
 
132
153
  // US-style week (Sunday start, first week with 1+ days)
133
154
  d.getWeekSeqOfYear(0, 1);
155
+ d.getWeekSeqOfMonth(0, 1);
156
+
157
+ // Start date of the week containing this date
158
+ d.getWeekSeqStartDate(); // ISO 8601 (Monday start)
159
+ d.getWeekSeqStartDate(0, 1); // US-style (Sunday start)
134
160
 
135
- // Reverse calculate date from week
136
- DateOnly.getDateByYearWeekSeq({ year: 2025, weekSeq: 2 }); // 2025-01-06 (Monday)
161
+ // Base year/month for week sequence calculations
162
+ d.getBaseYearMonthSeqForWeekSeq(); // { year: 2025, monthSeq: 1 }
163
+
164
+ // Reverse calculate date from week number
165
+ DateOnly.getDateByYearWeekSeq({ year: 2025, weekSeq: 2 }); // 2025-01-06 (Monday)
166
+ DateOnly.getDateByYearWeekSeq({ year: 2025, month: 1, weekSeq: 3 }); // 2025-01-13 (Monday)
137
167
 
138
168
  // Formatting
139
169
  d.toFormatString("yyyy년 MM월 dd일"); // "2025년 01월 15일"
@@ -153,10 +183,25 @@ const t = new Time(14, 30, 0);
153
183
  Time.parse("14:30:00"); // HH:mm:ss
154
184
  Time.parse("14:30:00.123"); // HH:mm:ss.fff
155
185
  Time.parse("오후 2:30:00"); // Korean AM/PM
186
+ Time.parse("2025-01-15T14:30:00"); // ISO 8601 (time part only)
156
187
 
157
- // 24-hour cycle
158
- t.addHours(12); // 14:30 + 12 hours = 02:30 (cycles, not next day)
188
+ // Properties (read-only)
189
+ t.hour; // 14
190
+ t.minute; // 30
191
+ t.second; // 0
192
+ t.millisecond; // 0
193
+ t.tick; // Milliseconds since midnight
194
+ t.isValid; // Validity check
195
+
196
+ // Immutable transformations (return new instances, 24-hour cycle)
197
+ t.setHour(9);
198
+ t.setMinute(0);
199
+ t.setSecond(0);
200
+ t.setMillisecond(0);
201
+ t.addHours(12); // 14:30 + 12 hours = 02:30 (wraps around midnight)
159
202
  t.addMinutes(-60); // 14:30 - 60 minutes = 13:30
203
+ t.addSeconds(30);
204
+ t.addMilliseconds(500);
160
205
 
161
206
  // Formatting
162
207
  t.toFormatString("tt h:mm"); // "오후 2:30"
@@ -192,7 +237,7 @@ import { LazyGcMap } from "@simplysm/core-common";
192
237
 
193
238
  // using statement (recommended)
194
239
  using map = new LazyGcMap<string, object>({
195
- gcInterval: 10000, // GC execution interval: 10 seconds
240
+ gcInterval: 10000, // GC execution interval: 10 seconds (optional, defaults to expireTime/10)
196
241
  expireTime: 60000, // Item expiration time: 60 seconds
197
242
  onExpire: (key, value) => {
198
243
  console.log(`Expired: ${key}`);
@@ -203,7 +248,14 @@ map.set("key1", { data: "hello" });
203
248
  map.get("key1"); // Refreshes access time (LRU)
204
249
  map.getOrCreate("key2", () => ({})); // Create and return if not exists
205
250
  map.has("key1"); // Does not refresh access time
251
+ map.size; // Number of stored entries
206
252
  map.delete("key1");
253
+ map.clear(); // Remove all items (instance remains usable)
254
+
255
+ // Iteration
256
+ for (const [key, value] of map.entries()) { /* ... */ }
257
+ for (const key of map.keys()) { /* ... */ }
258
+ for (const value of map.values()) { /* ... */ }
207
259
  ```
208
260
 
209
261
  ---
@@ -302,6 +354,78 @@ type Input = { a: string; b?: number };
302
354
  type Output = ObjOptionalToUndef<Input>; // { a: string; b: number | undefined }
303
355
  ```
304
356
 
357
+ ### EqualOptions
358
+
359
+ Options for `objEqual`.
360
+
361
+ ```typescript
362
+ import type { EqualOptions } from "@simplysm/core-common";
363
+
364
+ // topLevelIncludes: only compare these keys (top level only)
365
+ // topLevelExcludes: skip these keys (top level only)
366
+ // ignoreArrayIndex: treat arrays as sets (O(n²))
367
+ // onlyOneDepth: shallow comparison (reference equality for nested values)
368
+ const options: EqualOptions = {
369
+ topLevelExcludes: ["updatedAt"],
370
+ ignoreArrayIndex: true,
371
+ };
372
+ ```
373
+
374
+ ### ObjMergeOptions
375
+
376
+ Options for `objMerge`.
377
+
378
+ ```typescript
379
+ import type { ObjMergeOptions } from "@simplysm/core-common";
380
+
381
+ // arrayProcess: "replace" (default) replaces arrays, "concat" merges and deduplicates
382
+ // useDelTargetNull: when true, a null target value deletes the key from the result
383
+ const options: ObjMergeOptions = {
384
+ arrayProcess: "concat",
385
+ useDelTargetNull: true,
386
+ };
387
+ ```
388
+
389
+ ### ObjMerge3KeyOptions
390
+
391
+ Per-key comparison options for `objMerge3`.
392
+
393
+ ```typescript
394
+ import type { ObjMerge3KeyOptions } from "@simplysm/core-common";
395
+
396
+ // keys: sub-keys to compare (equivalent to topLevelIncludes in objEqual)
397
+ // excludes: sub-keys to exclude from comparison
398
+ // ignoreArrayIndex: ignore array order when comparing
399
+ const options: ObjMerge3KeyOptions = {
400
+ keys: ["id", "name"],
401
+ ignoreArrayIndex: false,
402
+ };
403
+ ```
404
+
405
+ ### DtNormalizedMonth
406
+
407
+ Return type of `normalizeMonth`. Contains year/month/day after overflow normalization.
408
+
409
+ ```typescript
410
+ import type { DtNormalizedMonth } from "@simplysm/core-common";
411
+
412
+ // { year: number; month: number; day: number }
413
+ ```
414
+
415
+ ### ZipArchiveProgress
416
+
417
+ Progress information passed to the callback of `ZipArchive.extractAll`.
418
+
419
+ ```typescript
420
+ import type { ZipArchiveProgress } from "@simplysm/core-common";
421
+
422
+ // { fileName: string; totalSize: number; extractedSize: number }
423
+ await archive.extractAll((progress: ZipArchiveProgress) => {
424
+ const pct = Math.round((progress.extractedSize / progress.totalSize) * 100);
425
+ console.log(`${progress.fileName}: ${pct}%`);
426
+ });
427
+ ```
428
+
305
429
  ### ArrayDiffsResult
306
430
 
307
431
  Result of `Array.diffs()` — insert / delete / update entries.
@@ -323,6 +447,10 @@ Result of `Array.oneWayDiffs()` — create / update / same entries.
323
447
 
324
448
  ```typescript
325
449
  import type { ArrayDiffs2Result } from "@simplysm/core-common";
450
+
451
+ // { type: "create"; item: T; orgItem: undefined }
452
+ // { type: "update"; item: T; orgItem: T }
453
+ // { type: "same"; item: T; orgItem: T }
326
454
  ```
327
455
 
328
456
  ### TreeArray
@@ -336,3 +464,13 @@ interface Category { id: number; parentId: number | undefined; name: string }
336
464
  const tree: TreeArray<Category>[] = categories.toTree("id", "parentId");
337
465
  // Each node has a `children` array of the same type
338
466
  ```
467
+
468
+ ### ComparableType
469
+
470
+ Union of types that can be used as sort keys in `orderBy`, `orderByDesc`, etc.
471
+
472
+ ```typescript
473
+ import type { ComparableType } from "@simplysm/core-common";
474
+
475
+ // string | number | boolean | DateTime | DateOnly | Time | undefined
476
+ ```
package/docs/utils.md CHANGED
@@ -22,6 +22,8 @@ import { objEqual } from "@simplysm/core-common";
22
22
  objEqual({ a: 1, b: [2] }, { a: 1, b: [2] }); // true
23
23
  objEqual(arr1, arr2, { ignoreArrayIndex: true }); // Ignore array order
24
24
  objEqual(obj1, obj2, { topLevelExcludes: ["updatedAt"] }); // Exclude specific keys
25
+ objEqual(obj1, obj2, { topLevelIncludes: ["id", "name"] }); // Only compare these keys
26
+ objEqual(obj1, obj2, { onlyOneDepth: true }); // Shallow (reference) comparison
25
27
  ```
26
28
 
27
29
  ### objMerge
@@ -33,6 +35,14 @@ import { objMerge } from "@simplysm/core-common";
33
35
 
34
36
  objMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 } });
35
37
  // { a: 1, b: { c: 2, d: 3 } }
38
+
39
+ // Concat arrays instead of replacing
40
+ objMerge({ tags: ["a"] }, { tags: ["b"] }, { arrayProcess: "concat" });
41
+ // { tags: ["a", "b"] }
42
+
43
+ // Delete key when target value is null
44
+ objMerge({ a: 1, b: 2 }, { b: null }, { useDelTargetNull: true });
45
+ // { a: 1 }
36
46
  ```
37
47
 
38
48
  ### objMerge3
@@ -60,6 +70,18 @@ import { objOmit } from "@simplysm/core-common";
60
70
  objOmit(user, ["password", "email"]);
61
71
  ```
62
72
 
73
+ ### objOmitByFilter
74
+
75
+ Exclude keys matching a predicate function.
76
+
77
+ ```typescript
78
+ import { objOmitByFilter } from "@simplysm/core-common";
79
+
80
+ // Remove all keys starting with "_"
81
+ objOmitByFilter(data, (key) => String(key).startsWith("_"));
82
+ // { name: "Alice", age: 30 } (private _internal key removed)
83
+ ```
84
+
63
85
  ### objPick
64
86
 
65
87
  Select specific keys.
@@ -78,6 +100,23 @@ Query value by chain path (`"a.b[0].c"`).
78
100
  import { objGetChainValue } from "@simplysm/core-common";
79
101
 
80
102
  objGetChainValue(obj, "a.b[0].c");
103
+
104
+ // Optional: returns undefined instead of throwing when intermediate path is missing
105
+ objGetChainValue(obj, "a.b[0].c", true);
106
+ ```
107
+
108
+ ### objGetChainValueByDepth
109
+
110
+ Descend the same key repeatedly to a given depth and return the value.
111
+
112
+ ```typescript
113
+ import { objGetChainValueByDepth } from "@simplysm/core-common";
114
+
115
+ const nested = { parent: { parent: { name: "root" } } };
116
+ objGetChainValueByDepth(nested, "parent", 2); // { name: "root" }
117
+
118
+ // Optional: returns undefined instead of throwing when path is missing
119
+ objGetChainValueByDepth(nested, "parent", 5, true); // undefined
81
120
  ```
82
121
 
83
122
  ### objSetChainValue
@@ -126,6 +165,9 @@ Type-safe `Object.fromEntries`.
126
165
 
127
166
  ```typescript
128
167
  import { objFromEntries } from "@simplysm/core-common";
168
+
169
+ const entries: ["a" | "b", number][] = [["a", 1], ["b", 2]];
170
+ objFromEntries(entries); // { a: number; b: number }
129
171
  ```
130
172
 
131
173
  ### objMap
@@ -135,7 +177,11 @@ Transform each entry of object and return new object.
135
177
  ```typescript
136
178
  import { objMap } from "@simplysm/core-common";
137
179
 
138
- objMap(colors, (key, rgb) => [null, `rgb(${rgb})`]); // Transform values only (keep keys)
180
+ // Transform values only (pass null as new key to keep original key)
181
+ objMap(colors, (key, rgb) => [null, `rgb(${rgb})`]);
182
+
183
+ // Transform both keys and values
184
+ objMap(colors, (key, rgb) => [`${key}Light`, `rgb(${rgb})`]);
139
185
  ```
140
186
 
141
187
  ---
@@ -165,6 +211,11 @@ const json = jsonStringify(data, { space: 2 });
165
211
  // For logging: hide binary data
166
212
  jsonStringify(data, { redactBytes: true });
167
213
  // Uint8Array content replaced with "__hidden__"
214
+
215
+ // Custom replacer (called before built-in type conversion)
216
+ jsonStringify(data, {
217
+ replacer: (key, value) => (key === "secret" ? undefined : value),
218
+ });
168
219
  ```
169
220
 
170
221
  ### jsonParse
@@ -243,6 +294,9 @@ PascalCase conversion.
243
294
 
244
295
  ```typescript
245
296
  import { strToPascalCase } from "@simplysm/core-common";
297
+
298
+ strToPascalCase("hello-world"); // "HelloWorld"
299
+ strToPascalCase("hello_world"); // "HelloWorld"
246
300
  ```
247
301
 
248
302
  ### strToCamelCase
@@ -253,6 +307,7 @@ camelCase conversion.
253
307
  import { strToCamelCase } from "@simplysm/core-common";
254
308
 
255
309
  strToCamelCase("hello-world"); // "helloWorld"
310
+ strToCamelCase("HelloWorld"); // "helloWorld"
256
311
  ```
257
312
 
258
313
  ### strToKebabCase
@@ -271,6 +326,8 @@ snake_case conversion.
271
326
 
272
327
  ```typescript
273
328
  import { strToSnakeCase } from "@simplysm/core-common";
329
+
330
+ strToSnakeCase("HelloWorld"); // "hello_world"
274
331
  ```
275
332
 
276
333
  ### strIsNullOrEmpty
@@ -293,6 +350,10 @@ Insert at specific position in string.
293
350
 
294
351
  ```typescript
295
352
  import { strInsert } from "@simplysm/core-common";
353
+
354
+ strInsert("Hello World", 5, ","); // "Hello, World"
355
+ strInsert("abc", 0, "X"); // "Xabc"
356
+ strInsert("abc", 3, "X"); // "abcX"
296
357
  ```
297
358
 
298
359
  ---
@@ -307,6 +368,7 @@ Parse string to integer (remove non-digit characters).
307
368
  import { numParseInt } from "@simplysm/core-common";
308
369
 
309
370
  numParseInt("12,345원"); // 12345
371
+ numParseInt(3.7); // 3 (truncated, not rounded)
310
372
  ```
311
373
 
312
374
  ### numParseFloat
@@ -325,6 +387,9 @@ Round float and return integer.
325
387
 
326
388
  ```typescript
327
389
  import { numParseRoundedInt } from "@simplysm/core-common";
390
+
391
+ numParseRoundedInt("3.7"); // 4
392
+ numParseRoundedInt("3.2"); // 3
328
393
  ```
329
394
 
330
395
  ### numFormat
@@ -336,6 +401,7 @@ import { numFormat } from "@simplysm/core-common";
336
401
 
337
402
  numFormat(1234567, { max: 2 }); // "1,234,567"
338
403
  numFormat(1234, { min: 2, max: 2 }); // "1,234.00"
404
+ numFormat(undefined); // undefined
339
405
  ```
340
406
 
341
407
  ### numIsNullOrEmpty
@@ -368,12 +434,20 @@ Convert date/time to string according to format string. Supports the same format
368
434
  | `dd` | 0-padded day | 01~31 |
369
435
  | `d` | Day | 1~31 |
370
436
  | `tt` | AM/PM | 오전, 오후 |
371
- | `HH` | 0-padded 24-hour | 00~23 |
372
437
  | `hh` | 0-padded 12-hour | 01~12 |
438
+ | `h` | 12-hour | 1~12 |
439
+ | `HH` | 0-padded 24-hour | 00~23 |
440
+ | `H` | 24-hour | 0~23 |
373
441
  | `mm` | 0-padded minute | 00~59 |
442
+ | `m` | Minute | 0~59 |
374
443
  | `ss` | 0-padded second | 00~59 |
444
+ | `s` | Second | 0~59 |
375
445
  | `fff` | Millisecond (3 digits) | 000~999 |
376
- | `zzz` | Timezone offset | +09:00 |
446
+ | `ff` | Millisecond (2 digits) | 00~99 |
447
+ | `f` | Millisecond (1 digit) | 0~9 |
448
+ | `zzz` | Timezone offset (±HH:mm) | +09:00 |
449
+ | `zz` | Timezone offset (±HH) | +09 |
450
+ | `z` | Timezone offset (±H) | +9 |
377
451
 
378
452
  ```typescript
379
453
  import { formatDate } from "@simplysm/core-common";
@@ -383,17 +457,34 @@ formatDate("yyyy-MM-dd", { year: 2024, month: 3, day: 15 });
383
457
 
384
458
  formatDate("yyyy년 M월 d일 (ddd)", { year: 2024, month: 3, day: 15 });
385
459
  // "2024년 3월 15일 (금)"
460
+
461
+ formatDate("tt h:mm:ss", { hour: 14, minute: 30, second: 45 });
462
+ // "오후 2:30:45"
386
463
  ```
387
464
 
388
465
  ### normalizeMonth
389
466
 
390
- Normalize year/month/day when setting month.
467
+ Normalize year/month/day when setting month. Handles month overflow and day clamping.
391
468
 
392
469
  ```typescript
393
470
  import { normalizeMonth } from "@simplysm/core-common";
394
471
 
395
472
  normalizeMonth(2025, 13, 15); // { year: 2026, month: 1, day: 15 }
396
473
  normalizeMonth(2025, 2, 31); // { year: 2025, month: 2, day: 28 }
474
+ normalizeMonth(2025, 0, 1); // { year: 2024, month: 12, day: 1 }
475
+ ```
476
+
477
+ ### convert12To24
478
+
479
+ Convert 12-hour (AM/PM) to 24-hour format.
480
+
481
+ ```typescript
482
+ import { convert12To24 } from "@simplysm/core-common";
483
+
484
+ convert12To24(12, false); // 0 (12 AM = midnight)
485
+ convert12To24(12, true); // 12 (12 PM = noon)
486
+ convert12To24(1, false); // 1 (1 AM)
487
+ convert12To24(1, true); // 13 (1 PM)
397
488
  ```
398
489
 
399
490
  ---
@@ -475,6 +566,9 @@ import { waitUntil } from "@simplysm/core-common";
475
566
  // Wait for condition (100ms interval, max 50 attempts = 5 seconds)
476
567
  await waitUntil(() => isReady, 100, 50);
477
568
  // Throws TimeoutError after 50 attempts
569
+
570
+ // Unlimited wait (omit maxCount)
571
+ await waitUntil(() => isReady, 200);
478
572
  ```
479
573
 
480
574
  ---
@@ -483,7 +577,7 @@ await waitUntil(() => isReady, 100, 50);
483
577
 
484
578
  ### transferableEncode
485
579
 
486
- Serialize custom types into Worker-transferable form.
580
+ Serialize custom types into Worker-transferable form. Returns `{ result, transferList }` where `transferList` contains `ArrayBuffer` instances for zero-copy transfer.
487
581
 
488
582
  ```typescript
489
583
  import { transferableEncode } from "@simplysm/core-common";
@@ -518,6 +612,7 @@ Combine paths (`path.join` replacement).
518
612
  import { pathJoin } from "@simplysm/core-common";
519
613
 
520
614
  pathJoin("/home", "user", "file.txt"); // "/home/user/file.txt"
615
+ pathJoin("a/", "/b/", "/c"); // "a/b/c"
521
616
  ```
522
617
 
523
618
  ### pathBasename
@@ -533,19 +628,21 @@ pathBasename("file.txt", ".txt"); // "file"
533
628
 
534
629
  ### pathExtname
535
630
 
536
- Extract extension (`path.extname` replacement).
631
+ Extract extension (`path.extname` replacement). Returns empty string for hidden files (e.g., `.gitignore`).
537
632
 
538
633
  ```typescript
539
634
  import { pathExtname } from "@simplysm/core-common";
540
635
 
541
- pathExtname("file.txt"); // ".txt"
636
+ pathExtname("file.txt"); // ".txt"
637
+ pathExtname(".gitignore"); // ""
638
+ pathExtname("archive.tar.gz"); // ".gz"
542
639
  ```
543
640
 
544
641
  ---
545
642
 
546
643
  ## Template literal tags (template-strings)
547
644
 
548
- Tag functions for IDE code highlighting. Actual behavior is string combination + indentation cleanup.
645
+ Tag functions for IDE code highlighting. Actual behavior is string combination + leading/trailing blank line removal + common indentation removal.
549
646
 
550
647
  ### js
551
648
 
@@ -553,6 +650,12 @@ JavaScript code highlighting.
553
650
 
554
651
  ```typescript
555
652
  import { js } from "@simplysm/core-common";
653
+
654
+ const code = js`
655
+ function hello() {
656
+ return "world";
657
+ }
658
+ `;
556
659
  ```
557
660
 
558
661
  ### ts
@@ -615,6 +718,7 @@ import { getPrimitiveTypeStr } from "@simplysm/core-common";
615
718
  getPrimitiveTypeStr("hello"); // "string"
616
719
  getPrimitiveTypeStr(123); // "number"
617
720
  getPrimitiveTypeStr(new DateTime()); // "DateTime"
721
+ getPrimitiveTypeStr(new Uint8Array()); // "Bytes"
618
722
  ```
619
723
 
620
724
  ### env
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/core-common",
3
- "version": "13.0.29",
3
+ "version": "13.0.30",
4
4
  "description": "심플리즘 패키지 - 코어 모듈 (common)",
5
5
  "author": "김석래",
6
6
  "license": "Apache-2.0",