You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
2.8 KiB
128 lines
2.8 KiB
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
* @oncall react_native
|
|
*/
|
|
|
|
export interface ReadOnlyCountingSet<T> extends Iterable<T> {
|
|
has(item: T): boolean;
|
|
@@iterator(): Iterator<T>;
|
|
+size: number;
|
|
count(item: T): number;
|
|
forEach<ThisT>(
|
|
callbackFn: (
|
|
this: ThisT,
|
|
value: T,
|
|
key: T,
|
|
set: ReadOnlyCountingSet<T>,
|
|
) => mixed,
|
|
|
|
// NOTE: Should be optional, but Flow seems happy to infer undefined here
|
|
// which is what we want.
|
|
thisArg: ThisT,
|
|
): void;
|
|
}
|
|
|
|
/**
|
|
* A Set that only deletes a given item when the number of delete(item) calls
|
|
* matches the number of add(item) calls. Iteration and `size` are in terms of
|
|
* *unique* items.
|
|
*/
|
|
export default class CountingSet<T> implements ReadOnlyCountingSet<T> {
|
|
#map: Map<T, number> = new Map();
|
|
|
|
constructor(items?: Iterable<T>) {
|
|
if (items) {
|
|
if (items instanceof CountingSet) {
|
|
this.#map = new Map(items.#map);
|
|
} else {
|
|
for (const item of items) {
|
|
this.add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
has(item: T): boolean {
|
|
return this.#map.has(item);
|
|
}
|
|
|
|
add(item: T): void {
|
|
const newCount = this.count(item) + 1;
|
|
this.#map.set(item, newCount);
|
|
}
|
|
|
|
delete(item: T): void {
|
|
const newCount = this.count(item) - 1;
|
|
if (newCount <= 0) {
|
|
this.#map.delete(item);
|
|
} else {
|
|
this.#map.set(item, newCount);
|
|
}
|
|
}
|
|
|
|
keys(): Iterator<T> {
|
|
return this.#map.keys();
|
|
}
|
|
|
|
values(): Iterator<T> {
|
|
return this.#map.keys();
|
|
}
|
|
|
|
*entries(): Iterator<[T, T]> {
|
|
for (const item of this) {
|
|
yield [item, item];
|
|
}
|
|
}
|
|
|
|
// Iterate over unique entries
|
|
// $FlowIssue[unsupported-syntax]
|
|
[Symbol.iterator](): Iterator<T> {
|
|
return this.values();
|
|
}
|
|
|
|
/*::
|
|
// For Flow's benefit
|
|
@@iterator(): Iterator<T> {
|
|
return this.values();
|
|
}
|
|
*/
|
|
|
|
// Number of unique entries
|
|
// $FlowIssue[unsafe-getters-setters]
|
|
get size(): number {
|
|
return this.#map.size;
|
|
}
|
|
|
|
count(item: T): number {
|
|
return this.#map.get(item) ?? 0;
|
|
}
|
|
|
|
clear(): void {
|
|
this.#map.clear();
|
|
}
|
|
|
|
forEach<ThisT>(
|
|
callbackFn: (this: ThisT, value: T, key: T, set: CountingSet<T>) => mixed,
|
|
thisArg: ThisT,
|
|
): void {
|
|
for (const item of this) {
|
|
callbackFn.call(thisArg, item, item, this);
|
|
}
|
|
}
|
|
|
|
// For Jest purposes. Ideally a custom serializer would be enough, but in
|
|
// practice there is hardcoded magic for Set in toEqual (etc) that we cannot
|
|
// extend to custom collection classes. Instead let's assume values are
|
|
// sortable ( = strings) and make this look like an array with some stable
|
|
// order.
|
|
toJSON(): mixed {
|
|
return [...this].sort();
|
|
}
|
|
}
|