@simplysm/core-common 13.0.0-beta.2 → 13.0.0-beta.21

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.
Files changed (84) hide show
  1. package/dist/common.types.js +4 -4
  2. package/dist/errors/argument-error.js +1 -1
  3. package/dist/errors/not-implemented-error.js +1 -1
  4. package/dist/errors/timeout-error.js +1 -1
  5. package/dist/extensions/arr-ext.helpers.js +4 -4
  6. package/dist/extensions/arr-ext.js +9 -9
  7. package/dist/features/debounce-queue.js +2 -2
  8. package/dist/features/serial-queue.js +3 -3
  9. package/dist/index.js +30 -30
  10. package/dist/types/date-only.js +2 -2
  11. package/dist/types/date-time.js +2 -2
  12. package/dist/types/time.js +2 -2
  13. package/dist/types/uuid.js +1 -1
  14. package/dist/utils/bytes.js +1 -1
  15. package/dist/utils/json.js +8 -8
  16. package/dist/utils/obj.js +5 -5
  17. package/dist/utils/primitive.js +5 -5
  18. package/dist/utils/transferable.js +4 -4
  19. package/dist/utils/wait.js +1 -1
  20. package/package.json +7 -4
  21. package/.cache/typecheck-browser.tsbuildinfo +0 -1
  22. package/.cache/typecheck-node.tsbuildinfo +0 -1
  23. package/.cache/typecheck-tests-browser.tsbuildinfo +0 -1
  24. package/.cache/typecheck-tests-node.tsbuildinfo +0 -1
  25. package/src/common.types.ts +0 -91
  26. package/src/env.ts +0 -11
  27. package/src/errors/argument-error.ts +0 -40
  28. package/src/errors/not-implemented-error.ts +0 -32
  29. package/src/errors/sd-error.ts +0 -53
  30. package/src/errors/timeout-error.ts +0 -36
  31. package/src/extensions/arr-ext.helpers.ts +0 -53
  32. package/src/extensions/arr-ext.ts +0 -777
  33. package/src/extensions/arr-ext.types.ts +0 -258
  34. package/src/extensions/map-ext.ts +0 -86
  35. package/src/extensions/set-ext.ts +0 -68
  36. package/src/features/debounce-queue.ts +0 -116
  37. package/src/features/event-emitter.ts +0 -112
  38. package/src/features/serial-queue.ts +0 -94
  39. package/src/globals.ts +0 -12
  40. package/src/index.ts +0 -55
  41. package/src/types/date-only.ts +0 -329
  42. package/src/types/date-time.ts +0 -294
  43. package/src/types/lazy-gc-map.ts +0 -244
  44. package/src/types/time.ts +0 -210
  45. package/src/types/uuid.ts +0 -113
  46. package/src/utils/bytes.ts +0 -160
  47. package/src/utils/date-format.ts +0 -239
  48. package/src/utils/json.ts +0 -230
  49. package/src/utils/num.ts +0 -97
  50. package/src/utils/obj.ts +0 -956
  51. package/src/utils/path.ts +0 -40
  52. package/src/utils/primitive.ts +0 -33
  53. package/src/utils/str.ts +0 -252
  54. package/src/utils/template-strings.ts +0 -132
  55. package/src/utils/transferable.ts +0 -269
  56. package/src/utils/wait.ts +0 -40
  57. package/src/utils/xml.ts +0 -105
  58. package/src/zip/sd-zip.ts +0 -218
  59. package/tests/errors/errors.spec.ts +0 -196
  60. package/tests/extensions/array-extension.spec.ts +0 -790
  61. package/tests/extensions/map-extension.spec.ts +0 -147
  62. package/tests/extensions/set-extension.spec.ts +0 -74
  63. package/tests/types/date-only.spec.ts +0 -636
  64. package/tests/types/date-time.spec.ts +0 -391
  65. package/tests/types/lazy-gc-map.spec.ts +0 -692
  66. package/tests/types/time.spec.ts +0 -559
  67. package/tests/types/types.spec.ts +0 -55
  68. package/tests/types/uuid.spec.ts +0 -91
  69. package/tests/utils/bytes-utils.spec.ts +0 -230
  70. package/tests/utils/date-format.spec.ts +0 -371
  71. package/tests/utils/debounce-queue.spec.ts +0 -272
  72. package/tests/utils/json.spec.ts +0 -475
  73. package/tests/utils/number.spec.ts +0 -184
  74. package/tests/utils/object.spec.ts +0 -827
  75. package/tests/utils/path.spec.ts +0 -78
  76. package/tests/utils/primitive.spec.ts +0 -55
  77. package/tests/utils/sd-event-emitter.spec.ts +0 -216
  78. package/tests/utils/serial-queue.spec.ts +0 -365
  79. package/tests/utils/string.spec.ts +0 -294
  80. package/tests/utils/template-strings.spec.ts +0 -96
  81. package/tests/utils/transferable.spec.ts +0 -698
  82. package/tests/utils/wait.spec.ts +0 -145
  83. package/tests/utils/xml.spec.ts +0 -146
  84. package/tests/zip/sd-zip.spec.ts +0 -234
@@ -1,777 +0,0 @@
1
- /**
2
- * Array 확장 메서드
3
- *
4
- * @remarks 각 메서드의 TSDoc은 타입 정의 파일(arr-ext.types.ts) 참조
5
- */
6
-
7
- import "./map-ext";
8
- import { objClone, objEqual, objMerge } from "../utils/obj";
9
- import type { PrimitiveTypeStr, Type } from "../common.types";
10
- import { DateTime } from "../types/date-time";
11
- import { DateOnly } from "../types/date-only";
12
- import { Time } from "../types/time";
13
- import { Uuid } from "../types/uuid";
14
- import { ArgumentError } from "../errors/argument-error";
15
- import { SdError } from "../errors/sd-error";
16
- import { compareForOrder } from "./arr-ext.helpers";
17
- import type {
18
- ReadonlyArrayExt,
19
- MutableArrayExt,
20
- ArrayDiffsResult,
21
- ArrayDiffs2Result,
22
- TreeArray,
23
- } from "./arr-ext.types";
24
-
25
- //#region 구현
26
-
27
- const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
28
- single<T>(predicate?: (item: T, index: number) => boolean): T | undefined {
29
- const arr = predicate !== undefined ? this.filter(predicate) : this;
30
- if (arr.length > 1) {
31
- throw new ArgumentError("복수의 결과물이 있습니다.", { count: arr.length });
32
- }
33
- return arr[0];
34
- },
35
-
36
- first<T>(predicate?: (item: T, index: number) => boolean): T | undefined {
37
- return predicate !== undefined ? this.find(predicate) : this[0];
38
- },
39
-
40
- async filterAsync<T>(predicate: (item: T, index: number) => Promise<boolean>): Promise<T[]> {
41
- const arr: T[] = [];
42
- for (let i = 0; i < this.length; i++) {
43
- if (await predicate(this[i], i)) {
44
- arr.push(this[i]);
45
- }
46
- }
47
- return arr;
48
- },
49
-
50
- last<T>(predicate?: (item: T, index: number) => boolean): T | undefined {
51
- if (predicate !== undefined) {
52
- for (let i = this.length - 1; i >= 0; i--) {
53
- if (predicate(this[i], i)) {
54
- return this[i];
55
- }
56
- }
57
-
58
- return undefined;
59
- } else {
60
- return this[this.length - 1];
61
- }
62
- },
63
-
64
- filterExists<T>(): NonNullable<T>[] {
65
- return this.filter((item) => item != null);
66
- },
67
-
68
- ofType<T, N extends T>(type: PrimitiveTypeStr | Type<N>): N[] {
69
- // PrimitiveTypeStr인 경우
70
- if (typeof type === "string") {
71
- return this.filter((item) => {
72
- switch (type) {
73
- case "string":
74
- return typeof item === "string";
75
- case "number":
76
- return typeof item === "number";
77
- case "boolean":
78
- return typeof item === "boolean";
79
- case "DateTime":
80
- return item instanceof DateTime;
81
- case "DateOnly":
82
- return item instanceof DateOnly;
83
- case "Time":
84
- return item instanceof Time;
85
- case "Uuid":
86
- return item instanceof Uuid;
87
- case "Bytes":
88
- return item instanceof Uint8Array;
89
- default: {
90
- // exhaustive check: PrimitiveTypeStr에 새 타입 추가 시 컴파일 에러 발생
91
- const _exhaustive: never = type;
92
- throw new ArgumentError(`지원하지 않는 타입: ${_exhaustive}`);
93
- }
94
- }
95
- }) as N[];
96
- }
97
-
98
- // Type<N> (생성자)인 경우
99
- return this.filter((item) => item instanceof type || item?.constructor === type) as N[];
100
- },
101
-
102
- async mapAsync<T, R>(selector: (item: T, index: number) => Promise<R>): Promise<R[]> {
103
- const result: R[] = [];
104
- for (let i = 0; i < this.length; i++) {
105
- result.push(await selector(this[i], i));
106
- }
107
- return result;
108
- },
109
-
110
- mapMany<T, R>(selector?: (item: T, index: number) => R[]): T | R[] {
111
- const arr = selector ? this.map(selector) : this;
112
- return arr.flat().filterExists();
113
- },
114
-
115
- async mapManyAsync<T, R>(selector?: (item: T, index: number) => Promise<R[]>): Promise<T | R[]> {
116
- const arr = selector !== undefined ? await this.mapAsync(selector) : this;
117
- return arr.mapMany();
118
- },
119
-
120
- parallelAsync<T, R>(fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
121
- return Promise.all(this.map(fn));
122
- },
123
-
124
- // 배열을 키별로 그룹화
125
- // 성능 고려사항:
126
- // - primitive 키 (string, number 등): O(n) - Map 기반
127
- // - 객체 키: O(n²) - objEqual 비교
128
- groupBy<T, K, V>(
129
- keySelector: (item: T, index: number) => K,
130
- valueSelector?: (item: T, index: number) => V,
131
- ): {
132
- key: K;
133
- values: (V | T)[];
134
- }[] {
135
- const result: { key: K; values: (V | T)[] }[] = [];
136
-
137
- // primitive 키 최적화를 위한 Map (키 문자열 -> result 인덱스)
138
- const primitiveKeyIndex = new Map<string, number>();
139
-
140
- for (let i = 0; i < this.length; i++) {
141
- const keyObj = keySelector(this[i], i);
142
- const valueObj = valueSelector !== undefined ? valueSelector(this[i], i) : this[i];
143
-
144
- // primitive 키는 Map으로 O(n) 처리
145
- if (keyObj == null || typeof keyObj !== "object") {
146
- const keyStr = typeof keyObj + ":" + String(keyObj);
147
- const existingIndex = primitiveKeyIndex.get(keyStr);
148
- if (existingIndex !== undefined) {
149
- result[existingIndex].values.push(valueObj);
150
- } else {
151
- primitiveKeyIndex.set(keyStr, result.length);
152
- result.push({ key: keyObj, values: [valueObj] });
153
- }
154
- continue;
155
- }
156
-
157
- // 객체 키는 기존 방식 O(n²)
158
- const existsRecord = result.find((item) => objEqual(item.key, keyObj));
159
- if (existsRecord !== undefined) {
160
- existsRecord.values.push(valueObj);
161
- } else {
162
- result.push({ key: keyObj, values: [valueObj] });
163
- }
164
- }
165
-
166
- return result;
167
- },
168
-
169
- toMap<T, K, V>(
170
- keySelector: (item: T, index: number) => K,
171
- valueSelector?: (item: T, index: number) => V,
172
- ): Map<K, V | T> {
173
- const result = new Map<K, V | T>();
174
-
175
- for (let i = 0; i < this.length; i++) {
176
- const item = this[i];
177
-
178
- const keyObj = keySelector(item, i);
179
- const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item;
180
-
181
- if (result.has(keyObj)) {
182
- throw new ArgumentError("키가 중복되었습니다.", { duplicatedKey: keyObj });
183
- }
184
- result.set(keyObj, valueObj);
185
- }
186
-
187
- return result;
188
- },
189
-
190
- async toMapAsync<T, K, V>(
191
- keySelector: (item: T, index: number) => Promise<K> | K,
192
- valueSelector?: (item: T, index: number) => Promise<V> | V,
193
- ): Promise<Map<K, V | T>> {
194
- const result = new Map<K, V | T>();
195
-
196
- for (let i = 0; i < this.length; i++) {
197
- const item = this[i];
198
-
199
- const keyObj = await keySelector(item, i);
200
- const valueObj = valueSelector !== undefined ? await valueSelector(item, i) : item;
201
-
202
- if (result.has(keyObj)) {
203
- throw new ArgumentError("키가 중복되었습니다.", { duplicatedKey: keyObj });
204
- }
205
- result.set(keyObj, valueObj);
206
- }
207
-
208
- return result;
209
- },
210
-
211
- toArrayMap<T, K, V>(
212
- keySelector: (item: T, index: number) => K,
213
- valueSelector?: (item: T, index: number) => V,
214
- ): Map<K, (V | T)[]> {
215
- const result = new Map<K, (V | T)[]>();
216
-
217
- for (let i = 0; i < this.length; i++) {
218
- const item = this[i];
219
-
220
- const keyObj = keySelector(item, i);
221
- const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item;
222
-
223
- const arr = result.getOrCreate(keyObj, []);
224
- arr.push(valueObj);
225
- }
226
-
227
- return result;
228
- },
229
-
230
- toSetMap<T, K, V>(
231
- keySelector: (item: T, index: number) => K,
232
- valueSelector?: (item: T, index: number) => V,
233
- ): Map<K, Set<V | T>> {
234
- const result = new Map<K, Set<V | T>>();
235
-
236
- for (let i = 0; i < this.length; i++) {
237
- const item = this[i];
238
-
239
- const keyObj = keySelector(item, i);
240
- const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item;
241
-
242
- const set = result.getOrCreate(keyObj, new Set<V | T>());
243
- set.add(valueObj);
244
- }
245
-
246
- return result;
247
- },
248
-
249
- toMapValues<T, K, V>(keySelector: (item: T, index: number) => K, valueSelector: (items: T[]) => V): Map<K, V | T> {
250
- const itemsMap = new Map<K, T[]>();
251
-
252
- for (let i = 0; i < this.length; i++) {
253
- const item = this[i];
254
-
255
- const keyObj = keySelector(item, i);
256
-
257
- const arr = itemsMap.getOrCreate(keyObj, []);
258
- arr.push(item);
259
- }
260
-
261
- const result = new Map<K, V | T>();
262
-
263
- for (const key of itemsMap.keys()) {
264
- result.set(key, valueSelector(itemsMap.get(key)!));
265
- }
266
-
267
- return result;
268
- },
269
-
270
- toObject<T, V>(
271
- keySelector: (item: T, index: number) => string,
272
- valueSelector?: (item: T, index: number) => V,
273
- ): Record<string, V | T> {
274
- const result: Record<string, V | T> = {};
275
-
276
- for (let i = 0; i < this.length; i++) {
277
- const item = this[i];
278
-
279
- const key = keySelector(item, i);
280
- const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item;
281
-
282
- // undefined 값은 "없음"으로 취급하여 덮어쓰기 허용
283
- if (result[key] !== undefined) {
284
- throw new ArgumentError("키가 중복되었습니다.", { duplicatedKey: key });
285
- }
286
- result[key] = valueObj;
287
- }
288
-
289
- return result;
290
- },
291
-
292
- toTree<T, K extends keyof T, P extends keyof T>(key: K, parentKey: P): TreeArray<T>[] {
293
- // O(n) 최적화: 맵 기반 인덱싱
294
- const childrenMap = this.toArrayMap((item) => item[parentKey]);
295
-
296
- const fn = (items: T[]): TreeArray<T>[] => {
297
- return items.map((item) => ({
298
- ...objClone(item),
299
- children: fn(childrenMap.get(item[key]) ?? []),
300
- }));
301
- };
302
-
303
- const rootItems = this.filter((item1) => item1[parentKey] == null);
304
- return fn(rootItems);
305
- },
306
-
307
- distinct<T>(options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number }): T[] {
308
- // 옵션 정규화
309
- const opts = typeof options === "boolean" ? { matchAddress: options } : (options ?? {});
310
-
311
- // matchAddress: Set 기반 O(n)
312
- if (opts.matchAddress === true) return [...new Set(this)];
313
-
314
- // keyFn 제공 시: 커스텀 키 기반 O(n)
315
- if (opts.keyFn) {
316
- const seen = new Set<string | number>();
317
- const result: T[] = [];
318
- for (const item of this) {
319
- const key = opts.keyFn(item);
320
- if (!seen.has(key)) {
321
- seen.add(key);
322
- result.push(item);
323
- }
324
- }
325
- return result;
326
- }
327
-
328
- // 기본: 타입별 처리
329
- const seen = new Map<string, T>();
330
- const seenRefs = new Set<symbol | ((...args: unknown[]) => unknown)>(); // symbol/function용 O(n) 처리
331
- const result: T[] = [];
332
-
333
- for (const item of this) {
334
- // primitive 타입은 빠른 경로
335
- if (item === null || typeof item !== "object") {
336
- const type = typeof item;
337
-
338
- // symbol, function은 Set으로 identity 비교 (O(n))
339
- if (type === "symbol" || type === "function") {
340
- if (!seenRefs.has(item)) {
341
- seenRefs.add(item);
342
- result.push(item);
343
- }
344
- continue;
345
- }
346
-
347
- // 나머지 primitive는 타입 prefix + 특수 케이스 처리
348
- let key = type + ":";
349
- if (Object.is(item, -0)) {
350
- key += "-0";
351
- } else {
352
- key += String(item);
353
- }
354
-
355
- if (!seen.has(key)) {
356
- seen.set(key, item);
357
- result.push(item);
358
- }
359
- continue;
360
- }
361
-
362
- if (!result.some((item1) => objEqual(item1, item))) {
363
- result.push(item);
364
- }
365
- }
366
-
367
- return result;
368
- },
369
-
370
- orderBy<T>(selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined): T[] {
371
- return [...this].sort((p, n) => {
372
- const pp = selector == null ? p : selector(p);
373
- const pn = selector == null ? n : selector(n);
374
- return compareForOrder(pp, pn, false);
375
- });
376
- },
377
-
378
- orderByDesc<T>(selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined): T[] {
379
- return [...this].sort((p, n) => {
380
- const pp = selector == null ? p : selector(p);
381
- const pn = selector == null ? n : selector(n);
382
- return compareForOrder(pp, pn, true);
383
- });
384
- },
385
-
386
- diffs<T, P>(
387
- target: P[],
388
- options?: {
389
- keys?: string[];
390
- excludes?: string[];
391
- },
392
- ): ArrayDiffsResult<T, P>[] {
393
- const result: ArrayDiffsResult<T, P>[] = [];
394
-
395
- const uncheckedTarget = [...target];
396
- const uncheckedTargetSet = new Set(uncheckedTarget);
397
- const hasKeys = options?.keys !== undefined && options.keys.length > 0;
398
- const excludeOpts = { topLevelExcludes: options?.excludes };
399
-
400
- // keys 옵션이 있는 경우 target을 keys 기준으로 Map에 미리 인덱싱하여 O(n×m) → O(n+m) 개선
401
- // 키 값이 같은 target이 여러 개 있을 수 있으므로 배열로 저장
402
- const keyIndexedTarget = hasKeys ? new Map<string, P[]>() : undefined;
403
-
404
- if (keyIndexedTarget) {
405
- for (const targetItem of uncheckedTarget) {
406
- const keyStr = JSON.stringify(options!.keys!.map((k) => (targetItem as Record<string, unknown>)[k]));
407
- const arr = keyIndexedTarget.get(keyStr);
408
- if (arr) {
409
- arr.push(targetItem);
410
- } else {
411
- keyIndexedTarget.set(keyStr, [targetItem]);
412
- }
413
- }
414
- }
415
-
416
- for (const sourceItem of this) {
417
- // 전체 일치(sameTarget) 우선, 없으면 키 일치(sameKeyTarget) 검색
418
- let sameTarget: P | undefined;
419
- let sameKeyTarget: P | undefined;
420
-
421
- // Set 기반 건너뛰기로 이미 매칭된 항목 스킵 (splice O(n) 제거)
422
- for (const targetItem of uncheckedTarget) {
423
- if (!uncheckedTargetSet.has(targetItem)) continue;
424
- if (objEqual(targetItem, sourceItem, excludeOpts)) {
425
- sameTarget = targetItem;
426
- break;
427
- }
428
- }
429
-
430
- // 전체 일치가 없고 keys 옵션이 있으면 Map에서 O(1) 조회
431
- if (sameTarget === undefined && keyIndexedTarget) {
432
- const sourceKeyStr = JSON.stringify(options!.keys!.map((k) => (sourceItem as Record<string, unknown>)[k]));
433
- const candidates = keyIndexedTarget.get(sourceKeyStr);
434
- if (candidates && candidates.length > 0) {
435
- // uncheckedTargetSet에서 O(1) 조회로 아직 남아있는 첫 번째 항목 선택
436
- sameKeyTarget = candidates.find((c) => uncheckedTargetSet.has(c));
437
- }
438
- }
439
-
440
- if (sameTarget !== undefined) {
441
- uncheckedTargetSet.delete(sameTarget);
442
- } else if (sameKeyTarget !== undefined) {
443
- result.push({ source: sourceItem, target: sameKeyTarget });
444
- uncheckedTargetSet.delete(sameKeyTarget);
445
- } else {
446
- result.push({ source: sourceItem, target: undefined });
447
- }
448
- }
449
-
450
- for (const uncheckedTargetItem of uncheckedTargetSet) {
451
- result.push({ source: undefined, target: uncheckedTargetItem });
452
- }
453
-
454
- return result;
455
- },
456
-
457
- oneWayDiffs<T extends Record<string, unknown>, K extends keyof T>(
458
- orgItems: T[] | Map<T[K], T>,
459
- keyPropNameOrFn: K | ((item: T) => K),
460
- options?: {
461
- includeSame?: boolean;
462
- excludes?: string[];
463
- includes?: string[];
464
- },
465
- ): ArrayDiffs2Result<T>[] {
466
- const orgItemMap =
467
- orgItems instanceof Map
468
- ? orgItems
469
- : orgItems.toMap((orgItem) =>
470
- typeof keyPropNameOrFn === "function" ? keyPropNameOrFn(orgItem) : orgItem[keyPropNameOrFn],
471
- );
472
- const includeSame = options?.includeSame ?? false;
473
-
474
- const diffs: ArrayDiffs2Result<T>[] = [];
475
- for (const item of this) {
476
- const keyValue = typeof keyPropNameOrFn === "function" ? keyPropNameOrFn(item) : item[keyPropNameOrFn];
477
- if (keyValue == null) {
478
- diffs.push({ type: "create", item, orgItem: undefined });
479
- continue;
480
- }
481
-
482
- const orgItem = orgItemMap.get(keyValue);
483
- if (!orgItem) {
484
- diffs.push({ type: "create", item, orgItem: undefined });
485
- continue;
486
- }
487
-
488
- if (
489
- objEqual(orgItem, item, {
490
- topLevelExcludes: options?.excludes,
491
- topLevelIncludes: options?.includes,
492
- })
493
- ) {
494
- if (includeSame) {
495
- diffs.push({ type: "same", item, orgItem });
496
- }
497
- continue;
498
- }
499
-
500
- diffs.push({ type: "update", item, orgItem });
501
- }
502
- return diffs;
503
- },
504
-
505
- merge<T, P>(
506
- target: P[],
507
- options?: {
508
- keys?: string[];
509
- excludes?: string[];
510
- },
511
- ): (T | P | (T & P))[] {
512
- const diffs = this.diffs(target, options);
513
-
514
- const result: (T | P | (T & P))[] = objClone(this);
515
-
516
- // source 항목의 원본 인덱스를 미리 계산하여 O(n) 검색을 O(1)로 개선
517
- const sourceIndexMap = new Map<T, number>();
518
- for (let i = 0; i < this.length; i++) {
519
- sourceIndexMap.set(this[i], i);
520
- }
521
-
522
- for (const diff of diffs) {
523
- // 변경시
524
- if (diff.source !== undefined && diff.target !== undefined) {
525
- const sourceIndex = sourceIndexMap.get(diff.source);
526
- if (sourceIndex === undefined) {
527
- throw new SdError("예상치 못한 오류: merge에서 source 항목을 찾을 수 없습니다.");
528
- }
529
- result[sourceIndex] = objMerge(diff.source, diff.target);
530
- }
531
- // 추가시
532
- else if (diff.target !== undefined) {
533
- result.push(diff.target);
534
- }
535
- }
536
-
537
- return result;
538
- },
539
-
540
- sum<T>(selector?: (item: T, index: number) => number): number {
541
- let result = 0;
542
- for (let i = 0; i < this.length; i++) {
543
- const item = selector !== undefined ? selector(this[i], i) : this[i];
544
- if (typeof item !== "number") {
545
- throw new ArgumentError("sum 은 number 에 대해서만 사용할 수 있습니다.", { type: typeof item });
546
- }
547
- result += item;
548
- }
549
-
550
- return result;
551
- },
552
-
553
- min<T>(selector?: (item: T, index: number) => string | number): string | number | undefined {
554
- let result: string | number | undefined;
555
- for (let i = 0; i < this.length; i++) {
556
- const item = selector !== undefined ? selector(this[i], i) : this[i];
557
- if (typeof item !== "number" && typeof item !== "string") {
558
- throw new ArgumentError("min 은 number/string 에 대해서만 사용할 수 있습니다.", { type: typeof item });
559
- }
560
- if (result === undefined || result > item) {
561
- result = item;
562
- }
563
- }
564
-
565
- return result;
566
- },
567
-
568
- max<T>(selector?: (item: T, index: number) => string | number): string | number | undefined {
569
- let result: string | number | undefined;
570
- for (let i = 0; i < this.length; i++) {
571
- const item = selector !== undefined ? selector(this[i], i) : this[i];
572
- if (typeof item !== "number" && typeof item !== "string") {
573
- throw new ArgumentError("max 은 number/string 에 대해서만 사용할 수 있습니다.", { type: typeof item });
574
- }
575
- if (result === undefined || result < item) {
576
- result = item;
577
- }
578
- }
579
-
580
- return result;
581
- },
582
-
583
- shuffle<T>(): T[] {
584
- if (this.length <= 1) {
585
- return [...this];
586
- }
587
-
588
- const result = [...this];
589
- for (let i = result.length - 1; i > 0; i--) {
590
- const j = Math.floor(Math.random() * (i + 1));
591
- [result[i], result[j]] = [result[j], result[i]];
592
- }
593
- return result;
594
- },
595
- };
596
-
597
- const arrayMutableExtensions: MutableArrayExt<any> & ThisType<any[]> = {
598
- distinctThis<T>(options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number }): T[] {
599
- // 옵션 정규화
600
- const opts = typeof options === "boolean" ? { matchAddress: options } : (options ?? {});
601
-
602
- // matchAddress: Set 기반 O(n)
603
- // 첫 번째 등장한 요소를 유지하기 위해 정방향 순회 후 제거할 인덱스 수집
604
- if (opts.matchAddress === true) {
605
- const seen = new Set<T>();
606
- const toRemove: number[] = [];
607
- for (let i = 0; i < this.length; i++) {
608
- if (seen.has(this[i])) {
609
- toRemove.push(i);
610
- } else {
611
- seen.add(this[i]);
612
- }
613
- }
614
- // 역순으로 제거 (인덱스 변화 방지)
615
- for (let i = toRemove.length - 1; i >= 0; i--) {
616
- this.splice(toRemove[i], 1);
617
- }
618
- return this;
619
- }
620
-
621
- // keyFn 제공 시: 커스텀 키 기반 O(n)
622
- // 첫 번째 등장한 요소를 유지하기 위해 정방향 순회 후 제거할 인덱스 수집
623
- if (opts.keyFn) {
624
- const seen = new Set<string | number>();
625
- const toRemove: number[] = [];
626
- for (let i = 0; i < this.length; i++) {
627
- const key = opts.keyFn(this[i]);
628
- if (seen.has(key)) {
629
- toRemove.push(i);
630
- } else {
631
- seen.add(key);
632
- }
633
- }
634
- // 역순으로 제거 (인덱스 변화 방지)
635
- for (let i = toRemove.length - 1; i >= 0; i--) {
636
- this.splice(toRemove[i], 1);
637
- }
638
- return this;
639
- }
640
-
641
- // 기본: 타입별 처리 (primitive 최적화)
642
- const seen = new Map<string, T>();
643
- const seenRefs = new Set<symbol | ((...args: unknown[]) => unknown)>();
644
- const toRemoveSet = new Set<number>();
645
-
646
- for (let i = 0; i < this.length; i++) {
647
- const item = this[i];
648
-
649
- // primitive 타입은 빠른 경로 O(n)
650
- if (item === null || typeof item !== "object") {
651
- const type = typeof item;
652
-
653
- // symbol, function은 Set으로 identity 비교
654
- if (type === "symbol" || type === "function") {
655
- if (seenRefs.has(item)) {
656
- toRemoveSet.add(i);
657
- } else {
658
- seenRefs.add(item);
659
- }
660
- continue;
661
- }
662
-
663
- // 나머지 primitive는 타입 prefix + 특수 케이스 처리
664
- let key = type + ":";
665
- if (Object.is(item, -0)) {
666
- key += "-0";
667
- } else {
668
- key += String(item);
669
- }
670
-
671
- if (seen.has(key)) {
672
- toRemoveSet.add(i);
673
- } else {
674
- seen.set(key, item);
675
- }
676
- continue;
677
- }
678
-
679
- // 객체는 깊은 비교 O(n²) - 제거되지 않은 이전 항목들과 비교
680
- let hasDuplicateBefore = false;
681
- for (let j = 0; j < i; j++) {
682
- // toRemoveSet에 있는 인덱스는 건너뜀 (O(1) 조회)
683
- if (toRemoveSet.has(j)) continue;
684
- if (objEqual(this[j], item)) {
685
- hasDuplicateBefore = true;
686
- break;
687
- }
688
- }
689
- if (hasDuplicateBefore) {
690
- toRemoveSet.add(i);
691
- }
692
- }
693
-
694
- // 역순으로 제거 (인덱스 변화 방지)
695
- const toRemoveArr = Array.from(toRemoveSet).sort((a, b) => b - a);
696
- for (const idx of toRemoveArr) {
697
- this.splice(idx, 1);
698
- }
699
-
700
- return this;
701
- },
702
-
703
- orderByThis<T>(selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined): T[] {
704
- return this.sort((p, n) => {
705
- const pp = selector?.(p) ?? p;
706
- const pn = selector?.(n) ?? n;
707
- return compareForOrder(pp, pn, false);
708
- });
709
- },
710
-
711
- orderByDescThis<T>(selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined): T[] {
712
- return this.sort((p, n) => {
713
- const pp = selector?.(p) ?? p;
714
- const pn = selector?.(n) ?? n;
715
- return compareForOrder(pp, pn, true);
716
- });
717
- },
718
-
719
- insert<T>(index: number, ...items: T[]): T[] {
720
- this.splice(index, 0, ...items);
721
- return this;
722
- },
723
-
724
- remove<T>(itemOrSelector: T | ((item: T, index: number) => boolean)): T[] {
725
- const shouldRemove =
726
- typeof itemOrSelector === "function"
727
- ? (itemOrSelector as (item: T, index: number) => boolean)
728
- : (item: T) => item === itemOrSelector;
729
-
730
- // 역방향 순회로 인덱스 변경 문제 방지 (O(n) 성능)
731
- for (let i = this.length - 1; i >= 0; i--) {
732
- if (shouldRemove(this[i], i)) {
733
- this.splice(i, 1);
734
- }
735
- }
736
-
737
- return this;
738
- },
739
-
740
- toggle<T>(this: T[], item: T): T[] {
741
- if (this.includes(item)) {
742
- this.remove(item);
743
- } else {
744
- this.push(item);
745
- }
746
- return this;
747
- },
748
-
749
- clear<T>(this: T[]): T[] {
750
- return this.remove(() => true);
751
- },
752
- };
753
-
754
- for (const [name, fn] of Object.entries({
755
- ...arrayReadonlyExtensions,
756
- ...arrayMutableExtensions,
757
- })) {
758
- Object.defineProperty(Array.prototype, name, {
759
- value: fn,
760
- enumerable: false,
761
- writable: true,
762
- configurable: true,
763
- });
764
- }
765
-
766
- //#endregion
767
-
768
- //#region 타입 선언
769
-
770
- declare global {
771
- interface ReadonlyArray<T> extends ReadonlyArrayExt<T> {}
772
- interface Array<T> extends ReadonlyArrayExt<T>, MutableArrayExt<T> {}
773
- }
774
-
775
- //#endregion
776
-
777
- export type { ArrayDiffsResult, ArrayDiffs2Result, TreeArray, ComparableType } from "./arr-ext.types";