UNPKG

@rimbu/multiset

Version:

An immutable Set where each element can occur multiple times

760 lines (748 loc) 26.1 kB
import type { RMap, VariantMap } from '@rimbu/collection-types/map'; import type { Elem, WithElem } from '@rimbu/collection-types/map-custom'; import type { ArrayNonEmpty, RelatedTo, ToJSON, TraverseState, } from '@rimbu/common'; import type { FastIterable, Reducer, Stream, StreamSource, Streamable, } from '@rimbu/stream'; export interface VariantMultiSetBase< T, Tp extends VariantMultiSetBase.Types = VariantMultiSetBase.Types, > extends FastIterable<T> { /** * Returns true if the collection is empty. * @example * ```ts * HashMultiSet.empty<number>().isEmpty // => true * HashMultiSet.of(1, 2, 2).isEmpty // => false * ``` */ readonly isEmpty: boolean; /** * Returns the number of values in the collection. * @example * ```ts * HashMultiSet.of(1, 2).size // => 2 * HashMultiSet.of(1, 2, 2).size // => 3 * ``` */ readonly size: number; /** * Returns the number of distinct values in the collection. * @example * ```ts * HashMultiSet.of(1, 2).sizeDistinct // => 2 * HashMultiSet.of(1, 2, 2).sizeDistinct // => 2 * ``` */ readonly sizeDistinct: number; /** * Returns the Map representation of this collection. * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * const map: HashMap.NonEmpty<number, number> = m.countMap * ``` */ readonly countMap: WithElem<Tp, T>['countMap']; /** * Returns true if there is at least one entry in the collection, and instructs the compiler to treat the collection * as a .NonEmpty type. * @example * ```ts * const m: HashMultiSet<number> = HashMultiSet.of(1, 2, 2) * m.stream().first(0) // compiler allows fallback value since the Stream may be empty * if (m.nonEmpty()) { * m.stream().first(0) // compiler error: fallback value not allowed since Stream is not empty * } * ``` */ nonEmpty(): this is WithElem<Tp, T>['nonEmpty']; /** * Returns the collection as a .NonEmpty type * @throws RimbuError.EmptyCollectionAssumedNonEmptyError if the collection is empty * @example * ```ts * HashMultiSet.empty<number>().assumeNonEmpty() // => throws * const m: HashMultiSet<number> = HashMultiSet.of(1, 2) * const m2: HashMultiSet.NonEmpty<number> = m // => compiler error * const m3: HashMultiSet.NonEmpty<number> = m.assumeNonEmpty() * ``` * @note returns reference to this collection */ assumeNonEmpty(): WithElem<Tp, T>['nonEmpty']; /** * Returns a Stream containing all values of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).stream().toArray() // => [1, 2, 2] * ``` */ stream(): Stream<T>; /** * Returns a Stream containing all distinct values of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).stream().toArray() // => [1, 2] * ``` */ streamDistinct(): Stream<T>; /** * Returns the collection where the given `amount` (default: 'ALL') of the given `value` * are removed. * @param value - the value to remove * @param options - (optional) an object containing the following properties:<br/> * - amount: (default: 'ALL') the amount of values to remove, or 'ALL' to remove all values. * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.remove(5).toArray() // => [1, 2, 2] * m.remove(2).toArray() // => [1] * m.remove(2, 1).toArray() // => [1, 2] * ``` */ remove<U = T>( value: RelatedTo<T, U>, options?: { amount?: number | 'ALL' } ): WithElem<Tp, T>['normal']; /** * Returns the collection where every single value from given `values` `StreamSource` is * removed. * @param values - a `StreamSource` containing values to remove. * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.removeAllSingle([5, 6]).toArray() // => [1, 2, 2] * m.removeAllSingle([2, 3]).toArray() // => [1, 2] * m.removeAllSingle([2, 3, 2]).toArray() // => [1] * ``` */ removeAllSingle<U = T>( values: StreamSource<RelatedTo<T, U>> ): WithElem<Tp, T>['normal']; /** * Returns the collection where for every value from given `values` `StreamSource`, * all values in the collection are removed. * @param values - a `StreamSource` containing values to remove. * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.removeAllEvery([5, 6]).toArray() // => [1, 2, 2] * m.removeAllEvery([2, 3]).toArray() // => [1] * ``` */ removeAllEvery<U = T>( values: StreamSource<RelatedTo<T, U>> ): WithElem<Tp, T>['normal']; /** * Returns true if the given `value` exists in the collection. * @param value - the value to look for * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.has(5) // => false * m.has(2) // => true * ``` */ has<U = T>(value: RelatedTo<T, U>): boolean; /** * Returns the amount of occurrances of the given `value` in the collection. * @param value - the value to look for * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.count(5) // => 0 * m.count(2) // => 2 * ``` */ count<U = T>(value: RelatedTo<T, U>): number; /** * Performs given function `f` for each value of the collection, using given `state` as initial traversal state. * @param f - the function to perform for each value, receiving:<br/> * - `value`: the next value<br/> * - `index`: the index of the value<br/> * - `halt`: a function that, if called, ensures that no new values are passed * @param options - (optional) an object containing the following properties:<br/> * - state: (optional) the traversal state * @example * ```ts * HashMultiSet.of(1, 2, 2, 3).forEach((entry, i, halt) => { * console.log(entry) * if (i >= 1) halt() * }) * // => logs [1, 1] [2, 2] * ``` */ forEach( f: (value: T, index: number, halt: () => void) => void, options?: { state?: TraverseState } ): void; /** * Returns the collection containing only those values for which the given `pred` function returns true. * @param pred - a predicate function receiving:<br/> * - `entry`: the next entry consisting of the value and its count<br/> * - `index`: the entry index<br/> * - `halt`: a function that, when called, ensures no next entries are passed * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will negate the predicate * @note if the predicate is a type guard, the return type is automatically inferred * @example * ```ts * HashMultiSet.of(1, 2, 2, 3) * .filterEntries(entry => entry[1] > 1) * .toArray() * // => [[2, 2]] * ``` */ filterEntries<TF extends T>( pred: (entry: readonly [T, number], index: number) => entry is [TF, number], options?: { negate?: false | undefined } ): WithElem<Tp, TF>['normal']; filterEntries<TF extends T>( pred: (entry: readonly [T, number], index: number) => entry is [TF, number], options: { negate: true } ): WithElem<Tp, Exclude<T, TF>>['normal']; filterEntries( pred: (entry: readonly [T, number], index: number) => boolean, options?: { negate?: boolean } ): WithElem<Tp, T>['normal']; /** * Returns an array containing all values in this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).toArray() // => [1, 2, 2] * ``` * @note O(log(N)) * @note it is safe to mutate the returned array, however, the array elements are not copied, thus should be treated as read-only */ toArray(): T[]; /** * Returns a string representation of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).toString() // => HashMultiSet(1, 2, 2) * ``` */ toString(): string; /** * Returns a JSON representation of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).toJSON() // => { dataType: 'HashMultiSet', value: [[1, 1], [2, 2]] } * ``` */ toJSON(): ToJSON<(readonly [T, number])[]>; } export namespace VariantMultiSetBase { export interface NonEmpty< T, Tp extends VariantMultiSetBase.Types = VariantMultiSetBase.Types, > extends VariantMultiSetBase<T, Tp>, Streamable.NonEmpty<T> { /** * Returns false since this collection is known to be non-empty * @example * ```ts * HashMultiSet.of(1, 2, 2).isEmpty // => false * ``` */ readonly isEmpty: false; /** * Returns the Map representation of this collection. * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * const map: HashMap.NonEmpty<number, number> = m.countMap * ``` */ readonly countMap: WithElem<Tp, T>['countMapNonEmpty']; /** * Returns true since this collection is known to be non-empty * @example * ```ts * HashMultiSet.of(1, 2, 2).nonEmpty() // => true * ``` */ nonEmpty(): this is WithElem<Tp, T>['nonEmpty']; /** * Returns this collection typed as a 'possibly empty' collection. * @example * ```ts * HashMultiSet.of(1, 2).asNormal(); // type: HashMultiSet<number> * ``` */ asNormal(): WithElem<Tp, T>['normal']; /** * Returns a non-empty Stream containing all values of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).stream().toArray() // => [1, 2, 2] * ``` */ stream(): Stream.NonEmpty<T>; /** * Returns a non-empty Stream containing all distinct values of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).stream().toArray() // => [1, 2] * ``` */ streamDistinct(): Stream.NonEmpty<T>; /** * Returns a non-empty array containing all values in this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).toArray() // => [1, 2, 2] * ``` * @note O(log(N)) * @note it is safe to mutate the returned array, however, the array elements are not copied, thus should be treated as read-only */ toArray(): ArrayNonEmpty<T>; } /** * Utility interface that provides higher-kinded types for this collection. */ export interface Types extends Elem { readonly normal: VariantMultiSetBase<this['_T']>; readonly nonEmpty: VariantMultiSetBase.NonEmpty<this['_T']>; readonly countMap: VariantMap<this['_T'], number>; readonly countMapNonEmpty: VariantMap.NonEmpty<this['_T'], number>; } } export interface MultiSetBase< T, Tp extends MultiSetBase.Types = MultiSetBase.Types, > extends VariantMultiSetBase<T, Tp> { /** * Returns the `context` associated to this collection instance. */ readonly context: WithElem<Tp, T>['context']; /** * Returns the collection with the given `value` added `amount` times. * @param value - the value to add * @param amount - (default: 1) the amount of values to add * @example * ```ts * HashMultiSet.of(1, 2).add(2).toArray() // => [1, 2, 2] * HashMultiSet.of(1, 2).add(3, 2).toArray() // => [1, 2, 3, 3] * ``` * @note amount < 0 will be normalized to 0 */ add(value: T): WithElem<Tp, T>['nonEmpty']; add(value: T, amount: number): WithElem<Tp, T>['normal']; /** * Returns the collection with the values in `values` added. * @param values - a `StreamSource` containing values to add * @example * ```ts * HashMultiSet.of(1, 2).addAll([2, 3]).toArray() // => [1, 2, 2, 3] * ``` */ addAll(values: StreamSource.NonEmpty<T>): WithElem<Tp, T>['nonEmpty']; addAll(values: StreamSource<T>): WithElem<Tp, T>['normal']; /** * Returns the collection where for every entry in `entries` consisting of a tuple * of a value and an amount, that value is added `amount` times. * @param entries - a `StreamSource` containing tuples that contain a value and an amount * @example * ```ts * HashMultiSet.of(1, 2).addEntries([[2, 2], [3, 2]]).toArray() * // => [1, 2, 2, 2, 3, 3] * ``` */ addEntries( entries: StreamSource<readonly [T, number]> ): WithElem<Tp, T>['normal']; /** * Returns the collection where the amount of values of `value` if set to `amount`. * @param value - the value of which to set the amount * @param amount - the new amount of values * * @note if amount <= 0, the value will be removed * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.setCount(1, 2).toArray() // => [1, 1, 2, 2] * m.setCount(2, 0).toArray() // => [1] * ``` */ setCount(value: T, amount: number): WithElem<Tp, T>['normal']; /** * Returns the collection where the count of the given `value` is modified according to * the given `update` function. * @param value - the value of which to modify the count * @param update - a function taking the current count and returning a new count. * * @note if the given `value` does not exists, the `update` function is called with 0. * @note if the result of `update` is <= 0, the value will be removed (or not added) * @example * ```ts * const m = HashMultiSet.of(1, 2, 2) * m.modifyCount(1, v => v + 1).toArray() // => [1, 1, 2, 2] * m.modifyCount(3, v => v + 1).toArray() // => [1, 2, 2, 3] * ``` */ modifyCount( value: T, update: (currentCount: number) => number ): WithElem<Tp, T>['normal']; /** * Returns a builder object containing the entries of this collection. * @example * ```ts * const builder: HashMultiSet.Builder<number> * = HashMultiSet.of(1, 2, 2).toBuilder() * ``` */ toBuilder(): WithElem<Tp, T>['builder']; } export namespace MultiSetBase { export interface NonEmpty< T, Tp extends MultiSetBase.Types = MultiSetBase.Types, > extends VariantMultiSetBase.NonEmpty<T, Tp>, Omit<MultiSetBase<T, Tp>, keyof VariantMultiSetBase.NonEmpty<any, any>>, Streamable.NonEmpty<T> { /** * Returns a non-empty Stream containing all values of this collection. * @example * ```ts * HashMultiSet.of(1, 2, 2).stream().toArray() // => [1, 2, 2] * ``` */ stream(): Stream.NonEmpty<T>; /** * Returns the collection with the given `value` added `amount` times. * @param value - the value to add * @param amount - (default: 1) the amount of values to add * @example * ```ts * HashMultiSet.of(1, 2).add(2).toArray() // => [1, 2, 2] * HashMultiSet.of(1, 2).add(3, 2).toArray() // => [1, 2, 3, 3] * ``` * @note amount < 0 will be normalized to 0 */ add(value: T, amount?: number): WithElem<Tp, T>['nonEmpty']; /** * Returns the collection with the values in `values` added. * @param values - a `StreamSource` containing values to add * @example * ```ts * HashMultiSet.of(1, 2).addAll([2, 3]).toArray() // => [1, 2, 2, 3] * ``` */ addAll(values: StreamSource<T>): WithElem<Tp, T>['nonEmpty']; /** * Returns the collection where for every entry in `entries` consisting of a tuple * of a value and an amount, that value is added `amount` times. * @param entries - a `StreamSource` containing tuples that contain a value and an amount * @example * ```ts * HashMultiSet.of(1, 2).addEntries([[2, 2], [3, 2]]).toArray() * // => [1, 2, 2, 2, 3, 3] * ``` */ addEntries( entries: StreamSource<readonly [T, number]> ): WithElem<Tp, T>['nonEmpty']; } export interface Factory<Tp extends MultiSetBase.Types, UT = unknown> { /** * Returns the (singleton) empty instance of this type and context with given key and value types. * @example * ```ts * HashMultiSet.empty<number>() // => HashMultiSet<number> * HashMultiSet.empty<string>() // => HashMultiSet<string> * ``` */ empty<T extends UT>(): WithElem<Tp, T>['normal']; /** * Returns an immutable multiset of this collection type and context, containing the given `values`. * @param values - a non-empty array of values * @example * ```ts * HashMultiSet.of(1, 2, 2) // => HashMultiSet.NonEmpty<number> * ``` */ of<T extends UT>(...values: ArrayNonEmpty<T>): WithElem<Tp, T>['nonEmpty']; /** * Returns an immutable multiset of this type and context, containing the values in the given `sources` * `StreamSource`. * @param sources - a non-empty array of `StreamSource` instances containing values to add * @example * ```ts * HashMultiSet.from([1, 2], [2, 3, 4]).toArray() // => [1, 2, 2, 3, 4] * ``` */ from<T extends UT>( ...sources: ArrayNonEmpty<StreamSource.NonEmpty<T>> ): WithElem<Tp, T>['nonEmpty']; from<T extends UT>( ...sources: ArrayNonEmpty<StreamSource<T>> ): WithElem<Tp, T>['normal']; /** * Returns an empty builder instance for this type of collection and context. * @example * ```ts * HashMultiSet.builder<number>() // => HashMultiSet.Builder<number> * ``` */ builder<T extends UT>(): WithElem<Tp, T>['builder']; /** * Returns a `Reducer` that appends received items to a MultiSet and returns the MultiSet as a result. When a `source` is given, * the reducer will first create a MultiSet from the source, and then add elements to it. * @param source - (optional) an initial source of elements to add to * @example * ```ts * const someList = [1, 2, 3]; * const result = Stream.range({ start: 20, amount: 5 }).reduce(SortedMultiSet.reducer(someList)) * result.toArray() // => [1, 2, 3, 20, 21, 22, 23, 24] * ``` * @note uses a MultiSet builder under the hood. If the given `source` is a MultiSet in the same context, it will directly call `.toBuilder()`. */ reducer<T extends UT>( source?: StreamSource<T> ): Reducer<T, WithElem<Tp, T>['normal']>; } export interface Context< UT, Tp extends MultiSetBase.Types = MultiSetBase.Types, > extends MultiSetBase.Factory<Tp, UT> { /** * A string tag defining the specific collection type * @example * ```ts * HashMultiSet.defaultContext().typeTag // => 'HashMultiSet' * ``` */ readonly typeTag: string; readonly _types: Tp; /** * The context used for the internal countMap instances. */ readonly countMapContext: WithElem<Tp, UT>['countMapContext']; /** * Returns true if given `obj` could be a valid key in this Context. * @param obj - the object to check * @example * ```ts * HashMultiSet.defaultContext().isValidElem(1) // => true * ``` */ isValidElem(key: any): key is UT; } export interface Builder< T, Tp extends MultiSetBase.Types = MultiSetBase.Types, > { /** * Returns the amount of values in the builder. * @example * ```ts * HashMultiSet.of(1, 2, 2).toBuilder().size * // => 3 * ``` */ readonly size: number; /** * Returns the amount of distinct values in the builder. * @example * ```ts * HashMultiSet.of(1, 2, 2).toBuilder().sizeDistinct * // => 2 * ``` */ readonly sizeDistinct: number; /** * Returns true if there are no values in the builder. * @example * ```ts * HashMultiSet.of(1, 2, 2).toBuilder().isEmpty * // => false * ``` */ readonly isEmpty: boolean; /** * Returns true if the given `value` is present in the builder. * @param value - the value to look for * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.has(2) // => true * s.has(10) // => false * ``` */ has<U = T>(value: RelatedTo<T, U>): boolean; /** * Adds given `value` to the builder. * @param value - the value to add * @returns true if the data in the builder has changed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.add(2) // => true * s.add(3, 5) // => true * s.add(3, 0) // => false * ``` */ add(value: T, amount?: number): boolean; /** * Adds the values in given `values` `StreamSource` to the builder. * @param values - the values to add * @returns true if the data in the builder has changed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.addAll(1, 3) // => false * s.addAll(2, 10) // => true * ``` */ addAll(values: StreamSource<T>): boolean; /** * Adds for each tuple of a value and amount in the given `entries`, the amount of values * to the builder. * @param entries - a `StreamSource` containing tuples of a value and amount * @returns true if the data in the builder has changed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.addEntries([[1, 2], [2, 3]]) // => true * s.addEntries([[1, 0], [3, 0]]) // => false * ``` */ addEntries(entries: StreamSource<readonly [T, number]>): boolean; /** * Removes given `amount` or all of given `value` from the builder. * @param value - the value to remove * @param amount - (default: 'ALL') the amount of values to remove * @returns the amount of elements that are removed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.remove(10) // => 0 * s.remove(1, 2) // => 1 * s.remove(2, 2) // => 2 * ``` */ remove<U = T>(value: RelatedTo<T, U>, amount?: number | 'ALL'): number; /** * Removes every single value in given `values` from the builder. * @param values - a `StreamSource` of values to remove. * @returns true if the data in the builder has changed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.removeAllSingle([10, 11]) // => false * s.removeAllSingle([1, 11]) // => true * ``` */ removeAllSingle<U = T>(values: StreamSource<RelatedTo<T, U>>): boolean; /** * Removes every instance of the given `values` from the builder. * @param values - a `StreamSource` of values to remove. * @returns true if the data in the builder has changed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.removeAllEvery([10, 11]) // => false * s.removeAllEvery([1, 11]) // => true * ``` */ removeAllEvery<U = T>(values: StreamSource<RelatedTo<T, U>>): boolean; /** * Sets the amount of given `value` in the collection to `amount`. * @param value - the value for which to set the amount * @param amount - the amount of values * @returns true if the data in the builder has changed * @note if amount <= 0, the value will be removed * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.setCount(1, 1) // => false * s.setCount(1, 3) // => true * ``` */ setCount(value: T, amount: number): boolean; /** * Changes the amount of given `value` in the builder according to the result of given `update` * function. * @param value - the value of which to update the amount * @param update - a function taking the current count and returning a new count. * @returns true if the data in the builder has changed * * @note if the given `value` does not exists, the `update` function is called with 0. * @note if the result of `update` is <= 0, the value will be removed (or not added) * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.modifyCount(3, v => v) // => false * s.modifyCount(3, v => v + 1) // => true * s.modifyCount(2, v => v + 1) // => true * ``` */ modifyCount(value: T, update: (currentCount: number) => number): boolean; /** * Returns the amount of given `value` in the builder. * @param value - the value to look for * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * s.count(10) // => 0 * s.count(2) // => 2 * ``` */ count<U = T>(value: RelatedTo<T, U>): number; /** * Performs given function `f` for each value of the collection, using given `state` as initial traversal state. * @param f - the function to perform for each value, receiving:<br/> * - `value`: the next value<br/> * - `index`: the index of the value<br/> * - `halt`: a function that, if called, ensures that no new values are passed * @param options - (optional) an object containing the following properties:<br/> * - state: (optional) the traversal state * @throws RibuError.ModifiedBuilderWhileLoopingOverItError if the builder is modified while * looping over it * @example * ```ts * HashMultiSet.of(1, 2, 2, 3).toBuilder().forEach((entry, i, halt) => { * console.log(entry) * if (i >= 1) halt() * }) * // => logs [1, 1] [2, 2] * ``` */ forEach( f: (value: T, index: number, halt: () => void) => void, options?: { state?: TraverseState } ): void; /** * Returns an immutable instance containing the values in this builder. * @example * ```ts * const s = HashMultiSet.of(1, 2, 2).toBuilder() * const s2: HashMultiSet<number> = m.build() * ``` */ build(): WithElem<Tp, T>['normal']; } /** * Utility interface that provides higher-kinded types for this collection. */ export interface Types extends VariantMultiSetBase.Types { readonly normal: MultiSetBase<this['_T']>; readonly nonEmpty: MultiSetBase.NonEmpty<this['_T']>; readonly context: MultiSetBase.Context<this['_T']>; readonly builder: MultiSetBase.Builder<this['_T']>; readonly countMap: RMap<this['_T'], number>; readonly countMapNonEmpty: RMap.NonEmpty<this['_T'], number>; readonly countMapContext: RMap.Context<this['_T']>; } }