@rimbu/graph
Version:
Immutable Graph data structures for TypeScript
451 lines (355 loc) • 11.2 kB
text/typescript
import { RimbuError, type Token } from '@rimbu/base';
import { OptLazy, OptLazyOr, type RelatedTo } from '@rimbu/common';
import { Stream, type StreamSource } from '@rimbu/stream';
import type { ValuedGraphTypesContextImpl } from '@rimbu/graph/custom';
import type { Link, WithGraphValues } from '../../common/index.mjs';
import { ValuedGraphElement } from '../../common/index.mjs';
export class ValuedGraphBuilder<
N,
V,
Tp extends ValuedGraphTypesContextImpl,
TpG extends WithGraphValues<Tp, N, V> = WithGraphValues<Tp, N, V>,
> {
connectionSize = 0;
constructor(
readonly isDirected: boolean,
readonly context: TpG['context'],
public source?: TpG['nonEmpty']
) {
if (undefined !== source) this.connectionSize = source.connectionSize;
}
_linkMap?: TpG['linkMapBuilder'];
_lock = 0;
checkLock(): void {
if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError();
}
get linkMap(): TpG['linkMapBuilder'] {
if (undefined === this._linkMap) {
if (undefined === this.source) {
this._linkMap = this.context.linkMapContext.builder();
} else {
this._linkMap = this.source.linkMap
.mapValues((targets) => targets.toBuilder())
.toBuilder();
}
}
return this._linkMap!;
}
get isEmpty(): boolean {
if (this.source) return this.source.isEmpty;
return this.linkMap.isEmpty;
}
get nodeSize(): number {
if (this.source) return this.source.nodeSize;
return this.linkMap.size;
}
// prettier-ignore
hasNode = <UN,>(node: RelatedTo<N, UN>): boolean => {
if (this.source) return this.source.hasNode(node);
return this.linkMap.hasKey(node);
};
// prettier-ignore
hasConnection = <UN,>(
node1: RelatedTo<N, UN>,
node2: RelatedTo<N, UN>
): boolean => {
if (this.source) return this.source.hasConnection(node1, node2);
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 => {
if (undefined !== this.source) {
return this.source.getValue(node1, node2, otherwise!);
}
const targets = this.linkMap.get(node1);
if (undefined === targets) return OptLazy(otherwise!);
return targets.get(node2, otherwise!);
};
addNodeInternal = (node: N): boolean => {
const changed = this.linkMap.modifyAt(node, {
ifNew: this.context.linkConnectionsContext.builder,
});
if (changed) this.source = undefined;
return changed;
};
addNode = (node: N): boolean => {
this.checkLock();
return this.addNodeInternal(node);
};
addNodes = (nodes: StreamSource<N>): boolean => {
this.checkLock();
return (
Stream.from(nodes).filterPure({ pred: this.addNodeInternal }).count() > 0
);
};
// prettier-ignore
removeNodeInternal = <UN,>(node: RelatedTo<N, UN>): boolean => {
const targets = this.linkMap.removeKey(node);
if (!targets) return false;
this.source = undefined;
if (this.isDirected) {
this.linkMap.forEach(([sourceNode, targets]) => {
if (targets.removeKey(node)) {
if (sourceNode !== node) this.connectionSize--;
}
});
} else {
this.connectionSize -= targets.size;
targets.forEach(([target]) =>
this.linkMap.updateAt(target, (values) => {
values.removeKey(node);
return values;
})
);
}
return true;
};
// prettier-ignore
removeNode = <UN,>(node: RelatedTo<N, UN>): boolean => {
this.checkLock();
return this.removeNodeInternal(node);
};
// prettier-ignore
removeNodes = <UN,>(nodes: StreamSource<RelatedTo<N, UN>>): boolean => {
this.checkLock();
return Stream.from(nodes).filterPure({ pred: this.removeNodeInternal }).count() > 0;
};
connectInternal = (node1: N, node2: N, value: V): boolean => {
let changed = false;
this.linkMap.modifyAt(node1, {
ifNew: () => {
const targetBuilder = this.context.linkConnectionsContext.builder<
N,
V
>();
targetBuilder.set(node2, value);
this.connectionSize++;
changed = true;
return targetBuilder;
},
ifExists: (targets) => {
const oldSize = targets.size;
if (targets.set(node2, value)) {
if (targets.size !== oldSize) this.connectionSize++;
changed = true;
}
return targets;
},
});
if (changed) this.source = undefined;
if (changed && node1 !== node2) {
this.linkMap.modifyAt(node2, {
ifNew: () => {
const targetBuilder = this.context.linkConnectionsContext.builder<
N,
V
>();
if (!this.isDirected) targetBuilder.set(node1, value);
return targetBuilder;
},
ifExists: (targets) => {
if (!this.isDirected) targets.set(node1, value);
return targets;
},
});
}
return changed;
};
connect = (node1: N, node2: N, value: V): boolean => {
this.checkLock();
return this.connectInternal(node1, node2, value);
};
connectAll = (connections: StreamSource<TpG['link']>): boolean => {
this.checkLock();
return (
Stream.applyFilter(connections as StreamSource<[N, N, V]>, {
pred: this.connectInternal,
}).count() > 0
);
};
addGraphElement = (element: ValuedGraphElement<N, V>): boolean => {
if (ValuedGraphElement.isLink(element)) {
return this.connectInternal(element[0], element[1], element[2]);
}
return this.addNodeInternal(element[0]);
};
addGraphElements = (
elements: StreamSource<ValuedGraphElement<N, V>>
): boolean => {
return (
Stream.from(elements).filterPure({ pred: this.addGraphElement }).count() >
0
);
};
modifyAt = (
node1: N,
node2: N,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((value: V, remove: Token) => V | Token) | V;
}
): boolean => {
this.checkLock();
const preConnectionSize = this.connectionSize;
let changed = false;
let addedOrUpdatedValue: V;
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;
changed = true;
addedOrUpdatedValue = newValue;
this.connectionSize++;
const builder = this.context.linkMapContext.builder<N, V>();
builder.set(node2, newValue);
return builder;
},
ifExists: (valueMap) => {
const { ifExists } = options;
if (undefined === ifExists) return valueMap;
valueMap.modifyAt(node2, {
ifNew: (none) => {
if (undefined === options.ifNew) return none;
const newValue = OptLazyOr<V, Token>(options.ifNew, none);
if (none === newValue) return none;
changed = true;
addedOrUpdatedValue = newValue;
this.connectionSize++;
return newValue;
},
ifExists: (currentValue, remove) => {
const newValue =
ifExists instanceof Function
? ifExists(currentValue, remove)
: ifExists;
if (Object.is(newValue, currentValue)) return currentValue;
changed = true;
if (remove === newValue) {
this.connectionSize--;
} else {
addedOrUpdatedValue = newValue;
}
return newValue;
},
});
return valueMap;
},
});
if (!changed) return false;
if (this.isDirected) return true;
// edge graph, need to update counterpart
if (this.connectionSize === preConnectionSize) {
// value was updated
this.linkMap.modifyAt(node2, {
ifNew: () => {
const builder = this.context.linkMapContext.builder<N, V>();
builder.set(node1, addedOrUpdatedValue);
return builder;
},
ifExists: (valueMap) => {
valueMap.set(node1, addedOrUpdatedValue);
return valueMap;
},
});
return true;
}
if (this.connectionSize < preConnectionSize) {
// value was removed
this.linkMap.modifyAt(node2, {
ifExists: (valueMap) => {
valueMap.removeKey(node1);
return valueMap;
},
});
return true;
}
// value was added
this.linkMap.modifyAt(node2, {
ifNew: () => {
const builder = this.context.linkMapContext.builder<N, V>();
builder.set(node1, addedOrUpdatedValue);
return builder;
},
ifExists: (valueMap) => {
valueMap.set(node1, addedOrUpdatedValue);
return valueMap;
},
});
return true;
};
// prettier-ignore
disconnectInternal = <UN,>(
node1: RelatedTo<N, UN>,
node2: RelatedTo<N, UN>
): boolean => {
if (
!this.linkMap.context.isValidKey(node1) ||
!this.linkMap.context.isValidKey(node2)
) {
return false;
}
let changed = false;
const token = Symbol();
this.linkMap.updateAt(node1, (targets) => {
if (token !== targets.removeKey(node2, token)) {
this.connectionSize--;
changed = true;
}
return targets;
});
if (changed) this.source = undefined;
if (changed && node1 !== node2 && !this.isDirected) {
this.linkMap.updateAt(node2, (targets) => {
targets.removeKey(node1);
return targets;
});
}
return changed;
};
// prettier-ignore
disconnect = <UN,>(
node1: RelatedTo<N, UN>,
node2: RelatedTo<N, UN>
): boolean => {
this.checkLock();
return this.disconnectInternal(node1, node2);
};
// prettier-ignore
disconnectAll = <UN,>(
connections: StreamSource<Link<RelatedTo<N, UN>>>
): boolean => {
this.checkLock();
return (
Stream.applyFilter(
connections as StreamSource<[N, N]>,
{ pred: this.disconnectInternal }
).count() > 0
);
};
build = (): TpG['normal'] => {
if (undefined !== this.source) return this.source as any;
if (this.isEmpty) return this.context.empty<N, V>();
const linkMap = this.linkMap
.buildMapValues((targets) => targets.build())
.assumeNonEmpty();
return this.context.createNonEmpty(linkMap, this.connectionSize);
};
// prettier-ignore
buildMapValues = <V2,>(
mapFun: (value: V, node1: N, node2: N) => V2
): WithGraphValues<Tp, N, V2>['normal'] => {
if (undefined !== this.source) return this.source.mapValues(mapFun) as any;
if (this.isEmpty) return this.context.empty<N, V2>();
const linkMap = this.linkMap
.buildMapValues((targets, source) =>
targets.buildMapValues((value, target) => mapFun(value, source, target))
)
.assumeNonEmpty();
return this.context.createNonEmpty(linkMap, this.connectionSize) as any;
};
}