UNPKG

@rimbu/graph

Version:

Immutable Graph data structures for TypeScript

476 lines (390 loc) 12.5 kB
import type { Token } from '@rimbu/base'; import { NonEmptyBase } from '@rimbu/collection-types/map-custom'; import { OptLazy, OptLazyOr, TraverseState, type RelatedTo, type ToJSON, } from '@rimbu/common'; import { Stream, type StreamSource } from '@rimbu/stream'; import type { ValuedGraphBase, ValuedGraphTypesContextImpl, } from '@rimbu/graph/custom'; import type { Link, ValuedGraphElement, ValuedLink, WithGraphValues, } from '../../common/index.mjs'; import type { RMap } from '@rimbu/collection-types'; export class ValuedGraphNonEmpty< N, V, Tp extends ValuedGraphTypesContextImpl, TpG extends WithGraphValues<Tp, N, V> = WithGraphValues<Tp, N, V>, > extends NonEmptyBase<ValuedGraphElement<N, V>> implements ValuedGraphBase.NonEmpty<N, V, Tp> { _NonEmptyType!: TpG['nonEmpty']; constructor( readonly isDirected: boolean, readonly context: TpG['context'], readonly linkMap: TpG['linkMapNonEmpty'], readonly connectionSize: number ) { super(); } copy( linkMap: TpG['linkMapNonEmpty'], connectionSize: number ): TpG['nonEmpty'] { if (linkMap === this.linkMap && connectionSize === this.connectionSize) { return this as any; } return this.context.createNonEmpty<N, V>(linkMap as any, connectionSize); } copyE(linkMap: TpG['linkMap'], connectionSize: number): TpG['normal'] { if (linkMap.nonEmpty()) { return this.copy(linkMap, connectionSize) as TpG['normal']; } return this.context.empty(); } assumeNonEmpty(): any { return this; } asNormal(): any { return this; } forEach( f: ( entry: ValuedGraphElement<N, V>, index: number, halt: () => void ) => void, options: { state?: TraverseState } = {} ): void { const { state = TraverseState() } = options; const mapIter = this.linkMap[Symbol.iterator](); const done = Symbol(); let targetsEntry: readonly [N, RMap<N, V>] | typeof done; while (!state.halted && done !== (targetsEntry = mapIter.fastNext(done))) { const [node, targets] = targetsEntry; if (targets.isEmpty) { f([node], state.nextIndex(), state.halt); } else { const targetsIter = targets[Symbol.iterator](); let target: readonly [N, V] | typeof done; while ( !state.halted && done !== (target = targetsIter.fastNext(done)) ) { const [targetNode, value] = target; f([node, targetNode, value], state.nextIndex(), state.halt); } } } } stream(): Stream.NonEmpty<ValuedGraphElement<N, V>> { return this.linkMap.stream().flatMap(([node, targets]) => { if (!targets.nonEmpty()) return [[node]]; return targets .stream() .map( ([target, value]) => [node, target, value] as ValuedGraphElement<N, V> ); }); } get nodeSize(): number { return this.linkMap.size; } streamNodes(): Stream.NonEmpty<N> { return this.linkMap.streamKeys(); } streamConnections(): Stream<ValuedLink<N, V>> { return this.linkMap .stream() .flatMap(([node1, targets]) => targets .stream() .map(([node2, value]) => [node1, node2, value] as [N, N, V]) ); } hasNode<UN = N>(node: RelatedTo<N, UN>): boolean { return this.linkMap.hasKey(node); } hasConnection<UN = N>( node1: RelatedTo<N, UN>, node2: RelatedTo<N, UN> ): boolean { const targets = this.linkMap.get(node1); return targets?.hasKey(node2) ?? false; } getValue<UN, O>( node1: RelatedTo<N, UN>, node2: RelatedTo<N, UN>, otherwise?: OptLazy<O> ): V | O { const targets = this.linkMap.get(node1); if (undefined === targets) return OptLazy(otherwise!); return targets.get(node2, otherwise!); } getConnectionStreamFrom<UN = N>( node1: RelatedTo<N, UN> ): Stream<ValuedLink<N, V>> { const targets = this.linkMap.get(node1); if (undefined === targets) return Stream.empty(); return targets .stream() .map(([node2, value]) => [node1, node2, value] as [N, N, V]); } getConnectionStreamTo<UN = N>( node: RelatedTo<N, UN> ): Stream<ValuedLink<N, V>> { if (this.isDirected) { return this.streamConnections().filter(([_, node2]) => node2 === node); } const targets = this.linkMap.get(node); if (undefined === targets) return Stream.empty(); return targets .stream() .map(([node1, value]) => [node1, node, value] as [N, N, V]); } getConnectionsFrom<UN = N>(node1: RelatedTo<N, UN>): TpG['linkConnections'] { return this.linkMap.get( node1, this.context.linkConnectionsContext.empty<N, V>() ); } isSink<UN = N>(node: RelatedTo<N, UN>): boolean { const targets = this.linkMap.get(node); return targets?.isEmpty ?? false; } isSource<UN = N>(node: RelatedTo<N, UN>): boolean { return ( this.linkMap.hasKey(node) && this.linkMap.streamValues().every((targets) => !targets.hasKey(node)) ); } addNode(node: N): TpG['nonEmpty'] { return this.copy( this.linkMap .modifyAt(node, { ifNew: this.context.linkConnectionsContext.empty }) .assumeNonEmpty(), this.connectionSize ); } addNodes(nodes: StreamSource<N>): TpG['nonEmpty'] { const builder = this.toBuilder(); builder.addNodes(nodes); return builder.build().assumeNonEmpty(); } removeNode<UN = N>(node: RelatedTo<N, UN>): TpG['normal'] { const builder = this.toBuilder(); builder.removeNode(node); return builder.build(); } removeNodes<UN>(nodes: StreamSource<RelatedTo<N, UN>>): TpG['normal'] { const builder = this.toBuilder(); builder.removeNodes(nodes); return builder.build(); } connect(node1: N, node2: N, value: V): TpG['nonEmpty'] { const newLinkMap = this.linkMap.modifyAt(node1, { ifNew: this.context.linkConnectionsContext.of([node2, value]), ifExists: (targets) => targets.set(node2, value), }); if (newLinkMap === this.linkMap) return this as any; const newConnectionSize = this.connectionSize + 1; if (Object.is(node1, node2) || this.isDirected) { return this.context.createNonEmpty( newLinkMap.assumeNonEmpty(), newConnectionSize ); } return this.copy( newLinkMap .modifyAt(node2, { ifNew: () => { if (this.isDirected) { return this.context.linkConnectionsContext.empty(); } return this.context.linkConnectionsContext.of([node1, value]); }, }) .assumeNonEmpty(), newConnectionSize ); } connectAll( links: StreamSource<WithGraphValues<Tp, N, V>['link']> ): TpG['nonEmpty'] { const builder = this.toBuilder(); builder.connectAll(links as any); return builder.build().assumeNonEmpty(); } modifyAt( node1: N, node2: N, options: { ifNew?: OptLazyOr<V, Token>; ifExists?: ((value: V, remove: Token) => V | Token) | V; } ): TpG['nonEmpty'] { let newConnectionSize = this.connectionSize; let addedOrUpdatedValue: V; const newLinkMap = this.linkMap.modifyAt(node1, { ifNew: (none) => { if (undefined === options.ifNew) return none; const newValue = OptLazyOr<V, Token>(options.ifNew, none); if (none === newValue) return none; addedOrUpdatedValue = newValue; newConnectionSize++; return this.context.linkMapContext.of([node2, newValue]); }, ifExists: (valueMap) => { const { ifExists } = options; if (undefined === ifExists) return valueMap; return valueMap.modifyAt(node2, { ifNew: (none) => { if (undefined === options.ifNew) return none; const newValue = OptLazyOr<V, Token>(options.ifNew, none); if (none === newValue) return none; addedOrUpdatedValue = newValue; newConnectionSize++; return newValue; }, ifExists: (currentValue, remove) => { const newValue = ifExists instanceof Function ? ifExists(currentValue, remove) : ifExists; if (Object.is(newValue, currentValue)) return currentValue; if (remove === newValue) { newConnectionSize--; } else { addedOrUpdatedValue = newValue; } return newValue; }, }); }, }); if (newLinkMap === this.linkMap) return this as any; if (this.isDirected) { return this.copy(newLinkMap.assumeNonEmpty(), newConnectionSize); } // edge graph, need to update counterpart if (newConnectionSize === this.connectionSize) { // value was updated const newLinkMap2 = newLinkMap.modifyAt(node2, { ifNew: () => this.context.linkMapContext.of([node1, addedOrUpdatedValue]), ifExists: (valueMap) => valueMap.set(node1, addedOrUpdatedValue), }); return this.copy(newLinkMap2.assumeNonEmpty(), newConnectionSize); } if (newConnectionSize < this.connectionSize) { // value was removed const newLinkMap2 = newLinkMap.modifyAt(node2, { ifExists: (valueMap) => valueMap.removeKey(node1), }); return this.copy(newLinkMap2.assumeNonEmpty(), newConnectionSize); } // value was added const newLinkMap2 = newLinkMap.modifyAt(node2, { ifNew: () => this.context.linkMapContext.of([node1, addedOrUpdatedValue]), ifExists: (valueMap) => valueMap.set(node1, addedOrUpdatedValue), }); return this.copy(newLinkMap2.assumeNonEmpty(), newConnectionSize); } disconnect<UN = N>( node1: RelatedTo<N, UN>, node2: RelatedTo<N, UN> ): TpG['nonEmpty'] { if ( !this.linkMap.context.isValidKey(node1) || !this.linkMap.context.isValidKey(node2) ) return this as any; const newLinkMap = this.linkMap.updateAt(node1, (targets) => targets.removeKey(node2) ); if (newLinkMap === this.linkMap) return this as any; const newConnectionSize = this.connectionSize - 1; if (this.isDirected) return this.copy(newLinkMap, newConnectionSize); return this.copy( newLinkMap.updateAt(node2, (targets) => targets.removeKey(node1)), newConnectionSize ); } disconnectAll<UN = N>( links: StreamSource<Link<RelatedTo<N, UN>>> ): TpG['nonEmpty'] { const builder = this.toBuilder(); builder.disconnectAll(links); return builder.build().assumeNonEmpty(); } removeUnconnectedNodes(): TpG['normal'] { if (!this.isDirected) { const newLinkMap = this.linkMap.filter(([_, targets]) => targets.nonEmpty() ); return this.copyE(newLinkMap, this.connectionSize); } const unconnectedNodes = this.linkMap .stream() .collect(([source, targets], _, skip) => { if ( targets.isEmpty && !this.linkMap.streamValues().some((t) => t.hasKey(source)) ) { return source; } return skip; }); return this.removeNodes(unconnectedNodes); } mapValues<V2>( mapFun: (value: V, node1: N, node2: N) => V2 ): WithGraphValues<Tp, N, V2>['nonEmpty'] { const newLinkMap = this.linkMap.mapValues((targets, node1) => targets.mapValues((value, node2) => mapFun(value, node1, node2)) ); return this.context.createNonEmpty<N, V2>( newLinkMap, this.connectionSize ) as any; } toString(): string { const connector = this.isDirected ? '->' : '<->'; return this.linkMap.stream().join({ start: `${this.context.typeTag}(\n `, sep: ',\n ', end: '\n)', valueToString: ([node, targets]) => `${node} ${connector} ${targets.stream().join({ start: '[', sep: ', ', end: ']', valueToString: ([node2, value]) => `{${node2}: ${value}}`, })}`, }); } toJSON(): ToJSON<[N, (readonly [N, V])[]][]> { return { dataType: this.context.typeTag, value: this.linkMap .stream() .map( (entry) => [entry[0], entry[1].toArray()] as [N, (readonly [N, V])[]] ) .toArray(), }; } toBuilder(): TpG['builder'] { return this.context.createBuilder<N, V>(this as any); } }