import "./Map.ext"; import { Type } from "../types/type/Type"; import { WrappedType } from "../types/wrap/WrappedType"; import { NeverEntryError } from "../errors/NeverEntryError"; import { ObjectUtils } from "../utils/ObjectUtils"; import { DateOnly } from "../types/date-time/DateOnly"; import { DateTime } from "../types/date-time/DateTime"; import { Time } from "../types/date-time/Time"; // ----------------------------------------- // 인터페이스 // ----------------------------------------- interface IReadonlyArrayExt { single(predicate?: (item: T, index: number) => boolean): T | undefined; first(predicate?: (item: T, index: number) => boolean): T | undefined; filterAsync(predicate: (item: T, index: number) => Promise): Promise; last(predicate?: (item: T, index: number) => boolean): T | undefined; filterExists(): NonNullable[]; ofType(type: Type>): N[]; mapAsync(selector: (item: T, index: number) => Promise): Promise; mapMany(): T; mapMany(selector: (item: T, index: number) => R[]): R[]; mapManyAsync(selector: (item: T, index: number) => Promise): Promise; parallelAsync(fn: (item: T, index: number) => Promise): Promise; groupBy(keySelector: (item: T, index: number) => K): { key: K; values: T[] }[]; groupBy( keySelector: (item: T, index: number) => K, valueSelector: (item: T, index: number) => V, ): { key: K; values: V[]; }[]; toMap(keySelector: (item: T, index: number) => K): Map; toMap( keySelector: (item: T, index: number) => K, valueSelector: (item: T, index: number) => V, ): Map; toMapAsync(keySelector: (item: T, index: number) => Promise): Promise>; toMapAsync( keySelector: (item: T, index: number) => Promise | K, valueSelector: (item: T, index: number) => Promise | V, ): Promise>; toArrayMap(keySelector: (item: T, index: number) => K): Map; toArrayMap( keySelector: (item: T, index: number) => K, valueSelector: (item: T, index: number) => V, ): Map; toSetMap(keySelector: (item: T, index: number) => K): Map>; toSetMap( keySelector: (item: T, index: number) => K, valueSelector: (item: T, index: number) => V, ): Map>; toMapValues( keySelector: (item: T, index: number) => K, valueSelector: (items: T[]) => V, ): Map; toObject(keySelector: (item: T, index: number) => string): Record; toObject( keySelector: (item: T, index: number) => string, valueSelector: (item: T, index: number) => V, ): Record; toTree(keyProp: K, parentKey: P): ITreeArray[]; distinct(matchAddress?: boolean): T[]; orderBy(selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined): T[]; orderByDesc( selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined, ): T[]; diffs

( target: P[], options?: { keys?: string[]; excludes?: string[] }, ): TArrayDiffsResult[]; oneWayDiffs( orgItems: T[] | Map, keyPropNameOrFn: K | ((item: T) => K), options?: { includeSame?: boolean; excludes?: string[]; includes?: string[]; }, ): TArrayDiffs2Result[]; merge

(target: P[], options?: { keys?: string[]; excludes?: string[] }): (T | P | (T & P))[]; sum(selector?: (item: T, index: number) => number): number; min(): T extends number | string ? T | undefined : never; min

(selector?: (item: T, index: number) => P): P | undefined; max(): T extends number | string ? T | undefined : never; max

(selector?: (item: T, index: number) => P): P | undefined; shuffle(): T[]; } interface IMutableArrayExt { distinctThis(matchAddress?: boolean): T[]; orderByThis( selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined, ): T[]; orderByDescThis( selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined, ): T[]; insert(index: number, ...items: T[]): this; remove(item: T): this; remove(selector: (item: T, index: number) => boolean): this; toggle(item: T): this; clear(): this; } // ----------------------------------------- // 헬퍼함수 // ----------------------------------------- function toComparable(value: unknown): string | number | boolean | undefined { if (value instanceof DateOnly || value instanceof DateTime || value instanceof Time) { return value.tick; } return value as string | number | boolean | undefined; } function compareForOrder(pp: unknown, pn: unknown, desc: boolean): number { const cpp = toComparable(pp); const cpn = toComparable(pn); if (cpn === cpp) return 0; if (typeof cpp === "undefined") return desc ? 1 : -1; if (typeof cpn === "undefined") return desc ? -1 : 1; if (typeof cpn === "string" && typeof cpp === "string") { return desc ? cpn.localeCompare(cpp) : cpp.localeCompare(cpn); } if (typeof cpn === "number" && typeof cpp === "number") { return desc ? (cpn < cpp ? -1 : cpn > cpp ? 1 : 0) : cpn > cpp ? -1 : cpn < cpp ? 1 : 0; } if (typeof cpn === "boolean" && typeof cpp === "boolean") { return cpn === cpp ? 0 : cpn ? (desc ? 1 : -1) : desc ? -1 : 1; } throw new Error(`orderBy를 사용할 수 없는 타입입니다. (${typeof cpp}, ${typeof cpn})`); } // ----------------------------------------- // 구현 // ----------------------------------------- const arrayReadonlyExtensions: IReadonlyArrayExt & ThisType = { single(predicate?: (item: T, index: number) => boolean): T | undefined { const arr = predicate !== undefined ? this.filter(predicate) : this; if (arr.length > 1) { throw new Error(`복수의 결과물이 있습니다. (${arr.length}개)`); } return arr[0]; }, first(predicate?: (item: T, index: number) => boolean): T | undefined { return predicate !== undefined ? this.find(predicate) : this[0]; }, async filterAsync(predicate: (item: T, index: number) => Promise): Promise { const arr: T[] = []; for (let i = 0; i < this.length; i++) { if (await predicate(this[i], i)) { arr.push(this[i]); } } return arr; }, last(predicate?: (item: T, index: number) => boolean): T | undefined { if (predicate !== undefined) { for (let i = this.length - 1; i >= 0; i--) { if (predicate(this[i], i)) { return this[i]; } } return undefined; } else { return this[this.length - 1]; } }, filterExists(): NonNullable[] { return this.filter((item) => item != null); }, ofType(type: Type>): N[] { return this.filter((item) => item instanceof type || item?.constructor === type) as N[]; }, async mapAsync(selector: (item: T, index: number) => Promise): Promise { const result: R[] = []; for (let i = 0; i < this.length; i++) { result.push(await selector(this[i], i)); } return result; }, mapMany(selector?: (item: T, index: number) => R[]): T | R[] { const arr = selector ? this.map(selector) : this; return arr.flat().filterExists(); }, async mapManyAsync(selector?: (item: T, index: number) => Promise): Promise { const arr = selector !== undefined ? await this.mapAsync(selector) : this; return arr.mapMany(); }, async parallelAsync(fn: (item: T, index: number) => Promise): Promise { return await Promise.all(this.map(async (item, index) => await fn(item, index))); }, groupBy( keySelector: (item: T, index: number) => K, valueSelector?: (item: T, index: number) => V, ): { key: K; values: (V | T)[]; }[] { const result: { key: K; values: (V | T)[] }[] = []; for (let i = 0; i < this.length; i++) { const keyObj = keySelector(this[i], i); const valueObj = valueSelector !== undefined ? valueSelector(this[i], i) : this[i]; const existsRecord = result.find((item) => ObjectUtils.equal(item.key, keyObj)); if (existsRecord !== undefined) { existsRecord.values.push(valueObj); } else { result.push({ key: keyObj, values: [valueObj] }); } } return result; }, toMap( keySelector: (item: T, index: number) => K, valueSelector?: (item: T, index: number) => V, ): Map { const result = new Map(); for (let i = 0; i < this.length; i++) { const item = this[i]; const keyObj = keySelector(item, i); const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item; if (result.has(keyObj)) { throw new Error(`키가 중복되었습니다. (중복된키: ${JSON.stringify(keyObj)})`); } result.set(keyObj, valueObj); } return result; }, async toMapAsync( keySelector: (item: T, index: number) => Promise | K, valueSelector?: (item: T, index: number) => Promise | V, ): Promise> { const result = new Map(); for (let i = 0; i < this.length; i++) { const item = this[i]; const keyObj = await keySelector(item, i); const valueObj = valueSelector !== undefined ? await valueSelector(item, i) : item; if (result.has(keyObj)) { throw new Error(`키가 중복되었습니다. (중복된키: ${JSON.stringify(keyObj)})`); } result.set(keyObj, valueObj); } return result; }, toArrayMap( keySelector: (item: T, index: number) => K, valueSelector?: (item: T, index: number) => V, ): Map { const result = new Map(); for (let i = 0; i < this.length; i++) { const item = this[i]; const keyObj = keySelector(item, i); const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item; const arr = result.getOrCreate(keyObj, []); arr.push(valueObj); } return result; }, toSetMap( keySelector: (item: T, index: number) => K, valueSelector?: (item: T, index: number) => V, ): Map> { const result = new Map>(); for (let i = 0; i < this.length; i++) { const item = this[i]; const keyObj = keySelector(item, i); const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item; const set = result.getOrCreate(keyObj, new Set()); set.add(valueObj); } return result; }, toMapValues( keySelector: (item: T, index: number) => K, valueSelector: (items: T[]) => V, ): Map { const itemsMap = new Map(); for (let i = 0; i < this.length; i++) { const item = this[i]; const keyObj = keySelector(item, i); const arr = itemsMap.getOrCreate(keyObj, []); arr.push(item); } const result = new Map(); for (const key of itemsMap.keys()) { result.set(key, valueSelector(itemsMap.get(key)!)); } return result; }, toObject( keySelector: (item: T, index: number) => string, valueSelector?: (item: T, index: number) => V, ): Record { const result: Record = {}; for (let i = 0; i < this.length; i++) { const item = this[i]; const key = keySelector(item, i); const valueObj = valueSelector !== undefined ? valueSelector(item, i) : item; if (result[key] !== undefined) { throw new Error(`키가 중복되었습니다. (중복된키: ${key})`); } result[key] = valueObj; } return result; }, toTree(key: K, parentKey: P): ITreeArray[] { const fn = (items: T[]): ITreeArray[] => { return items.map((item) => ({ ...ObjectUtils.clone(item), children: fn(this.filter((item1) => item1[parentKey] === item[key])), })); }; const rootItems = this.filter((item1) => item1[parentKey] == null); return fn(rootItems); }, distinct(matchAddress?: boolean): T[] { if (matchAddress === true) return [...new Set(this)]; const seen = new Map(); const result: T[] = []; for (const item of this) { // primitive 타입은 빠른 경로 if (item === null || typeof item !== "object") { const type = typeof item; // symbol, function은 Map key로 직접 사용 (identity 비교) if (type === "symbol" || type === "function") { if (!result.includes(item)) { result.push(item); } continue; } // 나머지 primitive는 타입 prefix + 특수 케이스 처리 let key = type + ":"; if (Object.is(item, -0)) { key += "-0"; } else { key += String(item); } if (!seen.has(key)) { seen.set(key, item); result.push(item); } continue; } if (!result.some((item1) => ObjectUtils.equal(item1, item))) { result.push(item); } } return result; }, orderBy( selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined, ): T[] { return this.concat().sort((p, n) => { const pp = selector ? selector(p) : p; const pn = selector ? selector(n) : n; return compareForOrder(pp, pn, false); }); }, orderByDesc( selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined, ): T[] { return this.concat().sort((p, n) => { const pp = selector ? selector(p) : p; const pn = selector ? selector(n) : n; return compareForOrder(pp, pn, true); }); }, diffs( target: P[], options?: { keys?: string[]; excludes?: string[]; }, ): TArrayDiffsResult[] { const result: TArrayDiffsResult[] = []; const uncheckedTarget = ([] as P[]).concat(target); // source 비교시, 수정으로 판단되거나 변경사항이 없는것으로 판단된 target 은 제외시킴 for (const sourceItem of this) { //target 에 동일한 항목이 없을 때 const sameTarget = uncheckedTarget.single((targetItem) => ObjectUtils.equal( targetItem, sourceItem, options?.excludes !== undefined ? { excludes: options.excludes } : undefined, ), ); if (sameTarget === undefined) { //키 설정시 if (options?.keys !== undefined) { //target 에 동일한 항목은 아니지만, key 가 같은게 있는 경우: source => target 수정된 항목 const sameKeyTargetItem = uncheckedTarget.single((targetItem) => ObjectUtils.equal(targetItem, sourceItem, { includes: options.keys }), ); if (sameKeyTargetItem !== undefined) { result.push({ source: sourceItem, target: sameKeyTargetItem }); uncheckedTarget.remove(sameKeyTargetItem); continue; } } //기타: source 에서 삭제된 항목 result.push({ source: sourceItem, target: undefined }); } else { uncheckedTarget.remove(sameTarget); } } for (const uncheckedTargetItem of uncheckedTarget) { //target 에 추가된 항목 result.push({ source: undefined, target: uncheckedTargetItem }); } return result; }, oneWayDiffs, K extends keyof T>( orgItems: T[] | Map, keyPropNameOrFn: K | ((item: T) => K), options?: { includeSame?: boolean; excludes?: string[]; includes?: string[]; }, ): TArrayDiffs2Result[] { const orgItemMap = orgItems instanceof Map ? orgItems : orgItems.toMap((orgItem) => typeof keyPropNameOrFn === "function" ? keyPropNameOrFn(orgItem) : orgItem[keyPropNameOrFn], ); const includeSame = options?.includeSame ?? false; const diffs: TArrayDiffs2Result[] = []; for (const item of this) { const keyValue = typeof keyPropNameOrFn === "function" ? keyPropNameOrFn(item) : item[keyPropNameOrFn]; if (keyValue == null) { diffs.push({ type: "create", item, orgItem: undefined }); continue; } const orgItem = orgItemMap.get(keyValue); if (!orgItem) { diffs.push({ type: "create", item, orgItem: undefined }); continue; } if ( ObjectUtils.equal(orgItem, item, { excludes: options?.excludes, includes: options?.includes, }) ) { if (includeSame) { diffs.push({ type: "same", item, orgItem }); } continue; } diffs.push({ type: "update", item, orgItem }); } return diffs; }, merge( target: P[], options?: { keys?: string[]; excludes?: string[]; }, ): (T | P | (T & P))[] { const diffs = this.diffs(target, options); const result: (T | P | (T & P))[] = ObjectUtils.clone(this); for (const diff of diffs) { // 변경시 if (diff.source !== undefined && diff.target !== undefined) { const resultSourceItem = result.single((item) => ObjectUtils.equal(item, diff.source)); if (resultSourceItem === undefined) { throw new NeverEntryError(); } result[result.indexOf(resultSourceItem)] = ObjectUtils.merge(diff.source, diff.target); } // 추가시 else if (diff.target !== undefined) { result.push(diff.target); } } return result; }, sum(selector?: (item: T, index: number) => number): number { let result = 0; for (let i = 0; i < this.length; i++) { const item = selector !== undefined ? selector(this[i], i) : this[i]; if (typeof item !== "number") { throw new Error("sum 은 number 에 대해서만 사용할 수 있습니다."); } result += item; } return result; }, min(selector?: (item: T, index: number) => string | number): string | number | undefined { let result: string | number | undefined; for (let i = 0; i < this.length; i++) { const item = selector !== undefined ? selector(this[i], i) : this[i]; if (typeof item !== "number" && typeof item !== "string") { throw new Error("min 은 number/string 에 대해서만 사용할 수 있습니다."); } if (result === undefined || result > item) { result = item; } } return result; }, max(selector?: (item: T, index: number) => string | number): string | number | undefined { let result: string | number | undefined; for (let i = 0; i < this.length; i++) { const item = selector !== undefined ? selector(this[i], i) : this[i]; if (typeof item !== "number" && typeof item !== "string") { throw new Error("max 은 number/string 에 대해서만 사용할 수 있습니다."); } if (result === undefined || result < item) { result = item; } } return result; }, shuffle(): T[] { if (this.length <= 1) { return [...this]; } const result = [...this]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; }, }; const arrayMutableExtensions: IMutableArrayExt & ThisType = { distinctThis(matchAddress?: boolean): T[] { // 뒤에서부터 순회, 앞에 같은 요소가 있으면 제거 for (let i = this.length - 1; i >= 0; i--) { const item = this[i]; let hasDuplicateBefore = false; for (let j = 0; j < i; j++) { if (matchAddress === true ? this[j] === item : ObjectUtils.equal(this[j], item)) { hasDuplicateBefore = true; break; } } if (hasDuplicateBefore) { this.splice(i, 1); } } return this; }, orderByThis( selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined, ): T[] { return this.sort((p, n) => { const pp = selector ? selector(p) : p; const pn = selector ? selector(n) : n; return compareForOrder(pp, pn, false); }); }, orderByDescThis( selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined, ): T[] { return this.sort((p, n) => { const pp = selector ? selector(p) : p; const pn = selector ? selector(n) : n; return compareForOrder(pp, pn, true); }); }, insert(index: number, ...items: T[]): T[] { this.splice(index, 0, ...items); return this; }, remove(itemOrSelector: T | ((item: T, index: number) => boolean)): T[] { const removeItems = typeof itemOrSelector === "function" ? this.filter((itemOrSelector as (item: T, index: number) => boolean).bind(this)) : [itemOrSelector]; for (const removeItem of removeItems) { while (this.includes(removeItem)) { this.splice(this.indexOf(removeItem), 1); } } return this; }, toggle(this: T[], item: T): T[] { if (this.includes(item)) { this.remove(item); } else { this.push(item); } return this; }, clear(this: T[]): T[] { return this.remove(() => true); }, }; for (const [name, fn] of Object.entries({ ...arrayReadonlyExtensions, ...arrayMutableExtensions, })) { Object.defineProperty(Array.prototype, name, { value: fn, enumerable: false, writable: true, configurable: true, }); } declare global { interface ReadonlyArray extends IReadonlyArrayExt {} interface Array extends IReadonlyArrayExt, IMutableArrayExt {} } export type TArrayDiffsResult = | { source: undefined; target: P } // INSERT | { source: T; target: undefined } // DELETE | { source: T; target: P }; // UPDATE export type TArrayDiffs2Result = | { type: "create"; item: T; orgItem: undefined } | { type: "update"; item: T; orgItem: T } | { type: "same"; item: T; orgItem: T }; export type ITreeArray = T & { children: ITreeArray[] };