mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-28 19:31:03 -05:00
Made ArraySet
A set implementation which can use custom equality comparers
This commit is contained in:
parent
97a17208b2
commit
bddf78c669
2 changed files with 482 additions and 0 deletions
230
src/utils/collections/set.ts
Normal file
230
src/utils/collections/set.ts
Normal file
|
@ -0,0 +1,230 @@
|
|||
import { EqualityComparer, createDefaultEqualityComparer } from "@/utils/comparison";
|
||||
import { $i } from "./iterable";
|
||||
|
||||
/**
|
||||
* Checks if a given value is an instance of a {@link Set}-like object.
|
||||
*
|
||||
* @template T - The element type of the `Set`-like object.
|
||||
*
|
||||
* @param value - The value to be checked.
|
||||
*
|
||||
* @returns A boolean indicating whether the value is a `Set`-like object or not.
|
||||
*/
|
||||
export function isSet<T>(value: unknown): value is Set<T> {
|
||||
if (value instanceof Set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const set = value as Set<T>;
|
||||
return (
|
||||
!!set &&
|
||||
typeof set.values === "function" &&
|
||||
typeof set.add === "function" &&
|
||||
typeof set.delete === "function" &&
|
||||
typeof set.has === "function" &&
|
||||
typeof set[Symbol.iterator] === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given value is an instance of a {@link ReadOnlySet}-like object.
|
||||
*
|
||||
* @template T - The element type of the `ReadOnlySet`-like object.
|
||||
*
|
||||
* @param value - The value to be checked.
|
||||
*
|
||||
* @returns A boolean indicating whether the value is a `ReadOnlySet`-like object or not.
|
||||
*/
|
||||
export function isReadOnlySet<T>(value: unknown): value is ReadonlySet<T> {
|
||||
if (value instanceof Set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const set = value as ReadonlySet<T>;
|
||||
return (
|
||||
!!set &&
|
||||
typeof set.values === "function" &&
|
||||
typeof set.has === "function" &&
|
||||
typeof set[Symbol.iterator] === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link Set} using an array under the hood.
|
||||
*
|
||||
* @template T - The type of values in the Map.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Recommended for small collections and/or for occasions when you need to provide a custom equality comparer.
|
||||
*/
|
||||
export class ArraySet<T> implements Set<T> {
|
||||
/**
|
||||
* The array of values.
|
||||
*/
|
||||
private readonly _values: T[];
|
||||
|
||||
/**
|
||||
* The equality comparer used to compare values.
|
||||
*/
|
||||
private readonly _comparer: EqualityComparer<T>;
|
||||
|
||||
/**
|
||||
* Constructs an empty {@link ArraySet}.
|
||||
*
|
||||
* @param comparer - The equality comparer to use for comparing values.
|
||||
*/
|
||||
constructor(comparer?: EqualityComparer<T>);
|
||||
|
||||
/**
|
||||
* Constructs an {@link ArraySet} from an iterable of values.
|
||||
*
|
||||
* @param values - The iterable of values.
|
||||
* @param comparer - The equality comparer to use for comparing values.
|
||||
*/
|
||||
constructor(values: Iterable<T>, comparer?: EqualityComparer<T>);
|
||||
|
||||
/**
|
||||
* Constructs an {@link ArraySet} from either an iterable of values or an equality comparer.
|
||||
*
|
||||
* @param valuesOrComparer - The iterable of values or the equality comparer to use for comparing values.
|
||||
* @param comparer - The equality comparer to use for comparing values (if `valuesOrComparer` is an iterable).
|
||||
*/
|
||||
constructor(valuesOrComparer?: Iterable<T> | EqualityComparer<T>, comparer?: EqualityComparer<T>) {
|
||||
// If valuesOrComparer is a function, it must be the comparer, so use it.
|
||||
// Otherwise, use the default comparer.
|
||||
comparer ??= typeof valuesOrComparer === "function" ? valuesOrComparer : createDefaultEqualityComparer<T>();
|
||||
|
||||
// If valuesOrComparer is undefined or is in fact a comparer, create an empty array of values.
|
||||
const values = valuesOrComparer && valuesOrComparer !== comparer ? valuesOrComparer as Iterable<T> : [];
|
||||
|
||||
this._values = [];
|
||||
this._comparer = comparer;
|
||||
for (const value of values) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in the set.
|
||||
*/
|
||||
get size(): number {
|
||||
return this._values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value to the set.
|
||||
*
|
||||
* @param value - The value to add to the set.
|
||||
*
|
||||
* @returns The set object, for chaining purposes.
|
||||
*/
|
||||
add(value: T): this {
|
||||
const i = $i(this._values).indexOf(value, this._comparer);
|
||||
if (i === -1) {
|
||||
this._values.push(value);
|
||||
} else {
|
||||
this._values[i] = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether a value exists in the set or not.
|
||||
*
|
||||
* @param value - The value to search for in the set.
|
||||
*
|
||||
* @returns `true` if the given value exists in the set; otherwise, `false`.
|
||||
*/
|
||||
has(value: T): boolean {
|
||||
return $i(this._values).includes(value, this._comparer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the value from the set.
|
||||
*
|
||||
* @param value - The value to remove from the set.
|
||||
*
|
||||
* @returns `true` if the value was found and removed; otherwise, `false`.
|
||||
*/
|
||||
delete(value: T): boolean {
|
||||
const i = $i(this._values).indexOf(value, this._comparer);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._values.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all values from the set.
|
||||
*/
|
||||
clear(): void {
|
||||
this._values.splice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Funnily enough, returns an iterator over the values in the set.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This method exists because somebody thought that we need to keep
|
||||
* `Set`'s and `Map`'s APIs similar for some reason.
|
||||
*/
|
||||
keys(): IterableIterator<T> {
|
||||
return this._values[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over the values in the set.
|
||||
*/
|
||||
values(): IterableIterator<T> {
|
||||
return this._values[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Iterator} object that contains an array of `[value, value]`
|
||||
* for each element in the {@link ArraySet} object, in insertion order.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This method exists because somebody thought that we need to keep
|
||||
* `Set`'s and `Map`'s APIs similar for some reason.
|
||||
*/
|
||||
*entries(): IterableIterator<[T, T]> {
|
||||
const values = this._values;
|
||||
for (let i = 0; i < values.length; ++i) {
|
||||
yield [values[i], values[i]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function once per each value in the set.
|
||||
*
|
||||
* @param callbackFn - Function to execute for each value in the set.
|
||||
* @param thisArg - Object to use as `this` when executing `callbackFn`.
|
||||
*/
|
||||
forEach(callbackFn: (value: T, theSameValueAgain: T, set: ArraySet<T>) => void, thisArg?: any): void {
|
||||
callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg);
|
||||
const values = this._values;
|
||||
|
||||
for (let i = 0; i < values.length; ++i) {
|
||||
callbackFn(values[i], values[i], this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over the values in the set.
|
||||
*/
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this._values[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this object.
|
||||
*/
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Set";
|
||||
}
|
||||
}
|
252
tests/unit/utils/collections/set.spec.ts
Normal file
252
tests/unit/utils/collections/set.spec.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
import { IGNORE_CASE_EQUALITY_COMPARER } from "@/utils/comparison/string-equality-comparer";
|
||||
import { isSet, isReadOnlySet, ArraySet } from "@/utils/collections/set";
|
||||
|
||||
const readOnlySetLike = {
|
||||
keys: () => {},
|
||||
values: () => {},
|
||||
entries: () => {},
|
||||
has: () => {},
|
||||
[Symbol.iterator]: () => {}
|
||||
};
|
||||
|
||||
const setLike = {
|
||||
...readOnlySetLike,
|
||||
add: () => {},
|
||||
delete: () => {},
|
||||
};
|
||||
|
||||
describe("isSet", () => {
|
||||
test("returns true for Set instances", () => {
|
||||
expect(isSet(new Set())).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for Set-like objects", () => {
|
||||
expect(isSet(setLike)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for non-Set-like objects", () => {
|
||||
expect(isSet({})).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for null and undefined", () => {
|
||||
expect(isSet(null)).toBe(false);
|
||||
expect(isSet(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isReadOnlySet", () => {
|
||||
test("returns true for Set instances", () => {
|
||||
expect(isReadOnlySet(new Set())).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for ReadOnlySet-like objects", () => {
|
||||
expect(isReadOnlySet(readOnlySetLike)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for non-ReadOnlySet-like objects", () => {
|
||||
expect(isReadOnlySet({})).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for null and undefined", () => {
|
||||
expect(isReadOnlySet(null)).toBe(false);
|
||||
expect(isReadOnlySet(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArraySet", () => {
|
||||
describe("constructor", () => {
|
||||
test("creates an empty set when no parameters are provided", () => {
|
||||
const set = new ArraySet();
|
||||
|
||||
expect(set.size).toBe(0);
|
||||
});
|
||||
|
||||
test("creates a set from an iterable of values", () => {
|
||||
const set = new ArraySet(["one", "two"]);
|
||||
|
||||
expect(set.size).toBe(2);
|
||||
expect(Array.from(set)).toEqual(["one", "two"])
|
||||
});
|
||||
|
||||
test("creates a set from an iterable of entries and a custom comparer", () => {
|
||||
const set = new ArraySet(["one", "two"], IGNORE_CASE_EQUALITY_COMPARER);
|
||||
|
||||
expect(set.size).toBe(2);
|
||||
expect(set.has("ONE")).toBe(true);
|
||||
expect(set.has("TWO")).toBe(true);
|
||||
});
|
||||
|
||||
test("creates a set from an iterable of entries, and eliminates duplicates", () => {
|
||||
const set = new ArraySet([1, 2, 1]);
|
||||
|
||||
expect(set.size).toBe(2);
|
||||
expect(set.has(1)).toBe(true);
|
||||
expect(set.has(2)).toBe(true);
|
||||
});
|
||||
|
||||
test("creates a set from an iterable of entries and a custom comparer, and eliminates duplicates", () => {
|
||||
const set = new ArraySet(["one", "two", "ONE"], IGNORE_CASE_EQUALITY_COMPARER);
|
||||
|
||||
expect(set.size).toBe(2);
|
||||
expect(set.has("ONE")).toBe(true);
|
||||
expect(set.has("TWO")).toBe(true);
|
||||
expect(Array.from(set)).toEqual(["ONE", "two"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
test("adds a new key-value pair if the key is not present", () => {
|
||||
const set = new ArraySet();
|
||||
|
||||
set.add(1);
|
||||
expect(set.has(1)).toBe(true);
|
||||
expect(set.size).toBe(1);
|
||||
});
|
||||
|
||||
test("respects custom comparer when setting value by key", () => {
|
||||
const set = new ArraySet(["one"], IGNORE_CASE_EQUALITY_COMPARER);
|
||||
|
||||
set.add("Two");
|
||||
expect(set.has("two")).toBe(true);
|
||||
|
||||
set.add("TWO");
|
||||
expect(set.has("two")).toBe(true);
|
||||
|
||||
expect(set.size).toBe(2);
|
||||
expect(Array.from(set)).toEqual(["one", "TWO"]);
|
||||
});
|
||||
|
||||
test("updates the value if the key is already present", () => {
|
||||
const set = new ArraySet(["one"]);
|
||||
|
||||
set.add("one");
|
||||
expect(set.has("one")).toBe(true);
|
||||
expect(set.size).toBe(1);
|
||||
expect(Array.from(set)).toEqual(["one"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("has", () => {
|
||||
test("returns true if the key is present", () => {
|
||||
const set = new ArraySet([1]);
|
||||
|
||||
expect(set.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("respects custom comparer when checking for key presence", () => {
|
||||
const set = new ArraySet(["one"], IGNORE_CASE_EQUALITY_COMPARER);
|
||||
|
||||
expect(set.has("one")).toBe(true);
|
||||
expect(set.has("One")).toBe(true);
|
||||
expect(set.has("ONE")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false if the key is not present", () => {
|
||||
const set = new ArraySet();
|
||||
|
||||
expect(set.has(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
test("removes the entry with the specified key", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
|
||||
expect(set.delete(1)).toBe(true);
|
||||
expect(set.has(1)).toBe(false);
|
||||
expect(set.size).toBe(1);
|
||||
});
|
||||
|
||||
test("respects custom comparer when deleting by key", () => {
|
||||
const set = new ArraySet(["one"], IGNORE_CASE_EQUALITY_COMPARER);
|
||||
|
||||
expect(set.delete("One")).toBe(true);
|
||||
expect(set.has("one")).toBe(false);
|
||||
expect(set.delete("ONE")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false if the key is not present", () => {
|
||||
const set = new ArraySet();
|
||||
|
||||
expect(set.delete(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
test("removes all entries", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
set.clear();
|
||||
|
||||
expect(set.size).toBe(0);
|
||||
expect(set.has(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keys", () => {
|
||||
test("returns an iterator over the values (ironically)", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
const keys = Array.from(set.keys());
|
||||
|
||||
expect(keys).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("values", () => {
|
||||
test("returns an iterator over the values", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
|
||||
const values = Array.from(set.values());
|
||||
|
||||
expect(values).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("entries", () => {
|
||||
test("returns an iterator over the value-value pairs", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
const entries = Array.from(set.entries());
|
||||
|
||||
expect(entries).toEqual([[1, 1], [2, 2]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forEach", () => {
|
||||
test("calls the specified callback function for each value", () => {
|
||||
const set = new ArraySet(["one", "two"]);
|
||||
const callback = jest.fn();
|
||||
|
||||
set.forEach(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenNthCalledWith(1, "one", "one", set);
|
||||
expect(callback).toHaveBeenNthCalledWith(2, "two", "two", set);
|
||||
});
|
||||
|
||||
test("binds the callback function to the provided thisArg", () => {
|
||||
const set = new ArraySet([1, 2]);
|
||||
const thisArg = {};
|
||||
|
||||
set.forEach(function (this: typeof thisArg) {
|
||||
expect(this).toBe(thisArg);
|
||||
}, thisArg);
|
||||
});
|
||||
});
|
||||
|
||||
describe("[Symbol.iterator]", () => {
|
||||
test("returns an iterator over the values", () => {
|
||||
const set = new ArraySet(["one", "two"]);
|
||||
|
||||
const values = Array.from(set[Symbol.iterator]());
|
||||
|
||||
expect(values).toEqual(["one", "two"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("[Symbol.toStringTag]", () => {
|
||||
test("returns 'Set'", () => {
|
||||
const set = new ArraySet();
|
||||
|
||||
expect(set[Symbol.toStringTag]).toBe("Set");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue