@rimbu/graph
Version:
Immutable Graph data structures for TypeScript
329 lines (274 loc) • 8.62 kB
text/typescript
import { TraverseState, type RelatedTo, type ToJSON } from '@rimbu/common';
import { Stream, type StreamSource } from '@rimbu/stream';
import { NonEmptyBase } from '@rimbu/collection-types/map-custom';
import type { GraphTypesContextImpl } from '@rimbu/graph/custom';
import type {
GraphBase,
GraphElement,
Link,
WithGraphValues,
} from '../../common/index.mjs';
import type { RSet } from '@rimbu/collection-types';
export class GraphNonEmpty<
N,
Tp extends GraphTypesContextImpl,
TpG extends WithGraphValues<Tp, N, any> = WithGraphValues<Tp, N, any>,
>
extends NonEmptyBase<GraphElement<N>>
implements GraphBase.NonEmpty<N, 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>(linkMap as any, connectionSize);
}
copyE(linkMap: TpG['linkMap'], connectionSize: number): TpG['normal'] {
if (linkMap.nonEmpty()) return this.copy(linkMap, connectionSize) as any;
return this.context.empty();
}
assumeNonEmpty(): any {
return this;
}
asNormal(): any {
return this;
}
forEach(
f: (node: GraphElement<N>, 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, RSet<N>] | 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: N | typeof done;
while (
!state.halted &&
done !== (target = targetsIter.fastNext(done))
) {
f([node, target], state.nextIndex(), state.halt);
}
}
}
}
stream(): Stream.NonEmpty<GraphElement<N>> {
return this.linkMap.stream().flatMap(([node, targets]) => {
if (!targets.nonEmpty()) return [[node]];
return targets
.stream()
.map((target) => [node, target] as GraphElement<N>);
});
}
get nodeSize(): number {
return this.linkMap.size;
}
streamNodes(): Stream.NonEmpty<N> {
return this.linkMap.streamKeys();
}
streamConnections(): Stream<WithGraphValues<Tp, N, any>['link']> {
return this.linkMap
.stream()
.flatMap(([node1, targets]) =>
targets.stream().map((node2) => [node1, node2] as [N, N])
);
}
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?.has(node2) ?? false;
}
getConnectionStreamFrom<UN = N>(node1: RelatedTo<N, UN>): Stream<Link<N>> {
const targets = this.linkMap.get(node1);
if (undefined === targets) return Stream.empty();
return targets.stream().map((node2) => [node1, node2] as [N, N]);
}
getConnectionStreamTo<UN = N>(node: RelatedTo<N, UN>): any {
if (this.isDirected) {
return this.linkMap.stream().collect(([source, targets], _, skip) => {
if (!targets?.has(node)) return skip;
return [source, node];
});
}
const targets = this.linkMap.get(node);
if (undefined === targets) return Stream.empty();
return targets.stream().map((node1) => [node1, node]);
}
getConnectionsFrom<UN = N>(node1: RelatedTo<N, UN>): TpG['linkConnections'] {
return this.linkMap.get(
node1,
this.context.linkConnectionsContext.empty<N>()
);
}
isSink<UN = N>(node: RelatedTo<N, UN>): boolean {
const targets = this.linkMap.get(node);
return targets?.isEmpty ?? false;
}
isSource<UN>(node: RelatedTo<N, UN>): boolean {
return (
this.linkMap.hasKey(node) &&
this.linkMap.streamValues().every((targets) => !targets.has(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): TpG['nonEmpty'] {
const newLinkMap = this.linkMap.modifyAt(node1, {
ifNew: this.context.linkConnectionsContext.of(node2),
ifExists: (targets) => targets.add(node2),
});
if (newLinkMap === this.linkMap) return this as any;
const newConnectionSize = this.connectionSize + 1;
if (node1 === node2) {
return this.context.createNonEmpty(newLinkMap as any, newConnectionSize);
}
if (this.isDirected) {
return this.copy(
newLinkMap
.modifyAt(node2, {
ifNew: () => this.context.linkConnectionsContext.empty(),
})
.assumeNonEmpty(),
newConnectionSize
);
}
return this.copy(
newLinkMap
.modifyAt(node2, {
ifNew: () => this.context.linkConnectionsContext.of(node1),
ifExists: (targets) => targets.add(node1),
})
.assumeNonEmpty(),
newConnectionSize
);
}
connectAll(
links: StreamSource<WithGraphValues<Tp, N, any>['link']>
): TpG['nonEmpty'] {
const builder = this.toBuilder();
builder.connectAll(links as any);
return builder.build().assumeNonEmpty();
}
disconnect<UN>(
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.remove(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.remove(node1)),
newConnectionSize
);
}
disconnectAll<UN>(
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.has(source))
) {
return source;
}
return skip;
});
return this.removeNodes(unconnectedNodes);
}
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: ']' })}`,
});
}
toJSON(): ToJSON<[N, [N][]][]> {
return {
dataType: this.context.typeTag,
value: this.linkMap
.stream()
.map(
(entry) =>
[
entry[0],
entry[1]
.stream()
.map((v) => [v] as [N])
.toArray(),
] as [N, [N][]]
)
.toArray(),
};
}
toBuilder(): TpG['builder'] {
return this.context.createBuilder(this as any);
}
}