@msom/common
Version:
@msom/common
321 lines (291 loc) • 9.42 kB
text/typescript
import { Nullable, createFunction } from "..";
import { assert } from "../assert";
/** 集合事件类型定义 */
export type CollectionEvent = Record<never, never>;
/** 集合键类型,可以是字符串或数字 */
export type CollectionKey = string | number | symbol;
/**
* 获取集合元素键值的函数类型
* @template T 元素类型
*/
export type CollectionGetKey<T = unknown> = (data: T) => CollectionKey;
/**
* 数组转换接口定义
* @template T 元素类型
*/
interface ToArray<T = unknown> {
/**
* 将集合转换为数组(不带映射器)
* @param filter 可选的过滤函数
* @param mapper 可选的映射函数(此重载中不使用)
*/
toArray(
filter?: Nullable | createFunction<[T, CollectionKey, this, boolean]>,
mapper?: Nullable
): T[];
/**
* 将集合转换为数组(带映射器)
* @template RT 映射后的类型
* @param filter 可选的过滤函数
* @param mapper 可选的映射函数
*/
toArray<RT>(
filter?: Nullable | createFunction<[T, CollectionKey, this, boolean]>,
mapper?: createFunction<[T, CollectionKey, this, RT]>
): RT[];
}
/**
* 集合类,提供基于键值的元素存储和管理
* @template T 元素类型
*/
export class Collection<T = unknown> implements Iterable<T>, ToArray<T> {
/** 获取元素键值的函数 */
private declare getKey: CollectionGetKey<T>;
/** 存储所有元素的数组 */
private declare elements: Array<T>;
/** 键值到元素的映射 */
private declare elMap: Map<CollectionKey, T>;
/** 键值到数组索引的映射 */
private declare indexMap: Map<CollectionKey, number>;
/**
* 创建集合实例
* @param getKey 获取元素键值的函数
*/
constructor(getKey?: CollectionGetKey<T>) {
// 确保提供了获取键值的函数
assert(getKey, "miss get unique key");
this.getKey = getKey;
// 初始化存储结构
this.elements = new Array<T>();
this.elMap = new Map<CollectionKey, T>();
this.indexMap = new Map<CollectionKey, number>();
}
/**
* 获得集合的实际元素数量
*/
size(): number {
return Reflect.ownKeys(this.elements).length;
}
/**
* 根据键值获取元素
* @param key 元素的键值
* @returns 对应的元素或undefined
*/
get(key: CollectionKey) {
return this.elMap.get(key);
}
/**
* 检查是否存在指定键值的元素
* @param key 要检查的键值
* @returns 是否存在
*/
hasKey(key: CollectionKey) {
return this.elMap.has(key);
}
/**
* 检查元素是否在集合中
* @param element 要检查的元素
* @returns 是否存在
*/
hasElement(element: T) {
return this.hasKey(this.getKey(element));
}
/**
* 添加元素到集合
* @param element 要添加的元素
* @param force 当元素已存在时是否强制替换,默认为false
*/
add(element: T, force?: boolean) {
const key = this.getKey(element);
const has = this.elMap.has(key);
if (!has) {
// 元素不存在时,添加到数组末尾
const index = this.elements.push(element) - 1;
this.indexMap.set(key, index);
this.elMap.set(key, element);
} else if (force) {
// 元素存在且force为true时,替换原有元素
const index = this.indexMap.get(key);
assert(index);
this.elMap.set(key, element);
this.elements.splice(index, 1, element);
}
}
/**
* 使用指定的键值添加元素
* @param key 指定的键值
* @param element 要添加的元素
* @param force 当键值已存在时是否强制替换,默认为false
*/
addKey(key: CollectionKey, element: T, force?: boolean) {
const has = this.elMap.has(key);
if (!has) {
// 键值不存在时,添加到数组末尾
const index = this.elements.push(element) - 1;
this.indexMap.set(key, index);
this.elMap.set(key, element);
} else if (force) {
// 键值存在且force为true时,替换原有元素
const index = this.indexMap.get(key);
assert(index != undefined);
this.elMap.set(key, element);
this.elements.splice(index, 1, element);
}
}
/**
* 批量添加元素
* @param iterator 可迭代的元素集合
* @param force 当元素已存在时是否强制替换,默认为false
*/
addAll(iterator: Iterable<T>, force?: boolean) {
const { next }: Iterator<T, T> = iterator[Symbol.iterator]();
let result = next();
while (!result.done) {
this.add(result.value, force);
result = next();
}
}
/**
* 在指定位置插入元素
* @param element 待插入的元素
* @param index 插入位置,范围[0, length]。如果超出范围会被自动调整到有效范围内
* @param exist 当元素已存在时的处理选项
* @param exist.index 是否保持原有元素的位置。true: 保持原位置,false: 使用新位置
* @param exist.element 是否使用新元素替换原有元素。true: 使用新元素,false: 保持原有元素
*/
insert(
element: T,
index: number,
exist?: { index?: boolean; element?: boolean }
) {
const key = this.getKey(element);
const has = this.elMap.has(key);
const { index: cIndex, element: cElement } = exist || {};
if (!has) {
// 元素不存在时,直接插入到指定位置
this.elMap.set(key, element);
index = Math.min(this.elements.length, Math.max(0, index));
this.elements.splice(index, 0, element);
} else {
// 元素已存在时的处理
const oIndex = this.indexMap.get(key);
assert(oIndex);
const oElement = this.elements[oIndex];
const placeholder = Symbol("placegholder");
// 使用占位符标记原位置
this.elements[oIndex] = placeholder as unknown as T;
if (!cIndex) {
// 如果不采用新位置,则使用原位置
index = oIndex;
}
if (!cElement) {
// 如果不使用新元素,则使用原元素
element = oElement;
}
// 在目标位置插入元素
this.elements.splice(index, 0, element);
// 移除占位符
this.elements = this.elements.filter((v) => v !== placeholder);
// 更新索引映射
this.updateIndexMap();
}
}
/**
* 移除指定元素
* @param element 要移除的元素
* @returns 是否成功移除
*/
removeElement(element: T): boolean {
const key = this.getKey(element);
return !!this.remove(key);
}
/**
* 根据键值移除元素
* @param key 要移除的元素的键值
* @returns 被移除的元素,如果元素不存在则返回undefined
*/
remove(key: CollectionKey): T | undefined {
const has = this.elMap.has(key);
if (has) {
// 获取元素在数组中的索引
const index = this.indexMap.get(key);
assert(index);
// 从数组中移除元素
this.elements.splice(index, 1);
// 更新索引映射
this.updateIndexMap();
// 从映射中移除元素
this.elMap.delete(key);
}
return undefined;
}
/**
* 清空集合中的所有元素
*/
clear() {
this.elMap.clear();
this.elements.length = 0;
this.indexMap.clear();
}
/**
* 更新索引映射
* 当元素数组发生变化时,需要重新计算每个元素的索引
* @private
*/
private updateIndexMap() {
const { elements, indexMap } = this;
const { length } = elements;
indexMap.clear();
for (let i = 0; i < length; i++) {
const element = elements[i];
const key = this.getKey(element);
indexMap.set(key, i);
}
}
/**
* 实现Iterable接口,使集合可以被迭代
* 使用生成器函数遍历集合中的所有元素
* @yields 集合中的每个元素
*/
[Symbol.iterator](): Iterator<T, T, undefined> {
let i = 0;
return {
next: () => {
const j = i;
i++;
return {
value: this.elements[j],
done: j >= this.elements.length,
};
},
};
}
/**
* 遍历集合中的所有元素
* @param handler 处理每个元素的回调函数
*/
each(handler: createFunction<[T, CollectionKey, this, void]>) {
this.elMap.forEach((el, k) => handler(el, k, this));
}
toArray<RT = T>(
filter?: Nullable | createFunction<[T, CollectionKey, this, boolean]>,
mapper?: Nullable | createFunction<[T, CollectionKey, this, RT]>
): RT[] {
const result: RT[] = [];
const elements = this.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const key = this.getKey(element);
// 如果有过滤函数且返回false,则跳过当前元素
if (filter && !filter(element, key, this)) {
continue;
}
// 如果有映射函数,使用映射后的值;否则直接使用原始值
const value = mapper
? mapper(element, key, this)
: (element as unknown as RT);
result.push(value);
}
return result;
}
}