@electric-sql/d2ts
Version:
D2TS is a TypeScript implementation of Differential Dataflow.
249 lines • 8.73 kB
JavaScript
import { DefaultMap, chunkedArrayPush, hash } from './utils.js';
/**
* A multiset of data.
*/
export class MultiSet {
#inner;
constructor(data = []) {
this.#inner = data;
}
toString(indent = false) {
return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`;
}
toJSON() {
return JSON.stringify(Array.from(this.getInner()));
}
static fromJSON(json) {
return new MultiSet(JSON.parse(json));
}
/**
* Apply a function to all records in the collection.
*/
map(f) {
return new MultiSet(this.#inner.map(([data, multiplicity]) => [f(data), multiplicity]));
}
/**
* Filter out records for which a function f(record) evaluates to False.
*/
filter(f) {
return new MultiSet(this.#inner.filter(([data, _]) => f(data)));
}
/**
* Negate all multiplicities in the collection.
*/
negate() {
return new MultiSet(this.#inner.map(([data, multiplicity]) => [data, -multiplicity]));
}
/**
* Concatenate two collections together.
*/
concat(other) {
const out = [];
chunkedArrayPush(out, this.#inner);
chunkedArrayPush(out, other.getInner());
return new MultiSet(out);
}
/**
* Produce as output a collection that is logically equivalent to the input
* but which combines identical instances of the same record into one
* (record, multiplicity) pair.
*/
consolidate() {
const consolidated = new DefaultMap(() => 0);
const values = new Map();
let hasString = false;
let hasNumber = false;
let hasOther = false;
for (const [data, _] of this.#inner) {
if (typeof data === 'string') {
hasString = true;
}
else if (typeof data === 'number') {
hasNumber = true;
}
else {
hasOther = true;
break;
}
}
const requireJson = hasOther || (hasString && hasNumber);
for (const [data, multiplicity] of this.#inner) {
const key = requireJson ? hash(data) : data;
if (requireJson && !values.has(key)) {
values.set(key, data);
}
consolidated.update(key, (count) => count + multiplicity);
}
const result = [];
for (const [key, multiplicity] of consolidated.entries()) {
if (multiplicity !== 0) {
const parsedKey = requireJson ? values.get(key) : key;
result.push([parsedKey, multiplicity]);
}
}
return new MultiSet(result);
}
/**
* Match pairs (k, v1) and (k, v2) from the two input collections and produce (k, (v1, v2)).
*/
join(other) {
const out = [];
for (const [[k1, v1], d1] of this.#inner) {
for (const [[k2, v2], d2] of other.getInner()) {
if (k1 === k2) {
out.push([[k1, [v1, v2]], d1 * d2]);
}
}
}
return new MultiSet(out);
}
/**
* Apply a reduction function to all record values, grouped by key.
* This method only works on KeyedData types and returns KeyedData results.
*/
reduce(f) {
const keys = new DefaultMap(() => []);
const out = [];
for (const [[key, val], multiplicity] of this.#inner) {
keys.update(key, (existing) => {
existing.push([val, multiplicity]);
return existing;
});
}
for (const [key, vals] of keys.entries()) {
const results = f(vals);
for (const [val, multiplicity] of results) {
out.push([[key, val], multiplicity]);
}
}
return new MultiSet(out);
}
/**
* Count the number of times each key occurs in the collection.
*/
count() {
return this.reduce((vals) => {
let out = 0;
for (const [_, multiplicity] of vals) {
out += multiplicity;
}
return [[out, 1]];
});
}
/**
* Produce the sum of all the values paired with a key, for all keys in the collection.
*/
sum() {
return this.reduce((vals) => {
let out = 0;
for (const [val, multiplicity] of vals) {
out += val * multiplicity;
}
return [[out, 1]];
});
}
/**
* Produce the minimum value associated with each key in the collection.
*
* Note that no record may have negative multiplicity when computing the min,
* as it is unclear what exactly the minimum record is in that case.
*/
min() {
return this.reduce((vals) => {
const consolidated = new Map();
for (const [val, multiplicity] of vals) {
const current = consolidated.get(val)?.[1] || 0;
consolidated.set(val, [val, current + multiplicity]);
}
const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0);
if (validVals.length === 0)
return [];
let minEntry = validVals[0];
for (const entry of validVals) {
if (entry[1] <= 0) {
throw new Error('Negative multiplicities not allowed in min operation');
}
if (entry[0] < minEntry[0]) {
minEntry = entry;
}
}
return [[minEntry[0], 1]];
});
}
/**
* Produce the maximum value associated with each key in the collection.
*
* Note that no record may have negative multiplicity when computing the max,
* as it is unclear what exactly the maximum record is in that case.
*/
max() {
return this.reduce((vals) => {
const consolidated = new Map();
for (const [val, multiplicity] of vals) {
const current = consolidated.get(val)?.[1] || 0;
consolidated.set(val, [val, current + multiplicity]);
}
const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0);
if (validVals.length === 0)
return [];
let maxEntry = validVals[0];
for (const entry of validVals) {
if (entry[1] <= 0) {
throw new Error('Negative multiplicities not allowed in max operation');
}
if (entry[0] > maxEntry[0]) {
maxEntry = entry;
}
}
return [[maxEntry[0], 1]];
});
}
/**
* Reduce the collection to a set of elements (from a multiset).
*
* Note that no record may have negative multiplicity when producing this set,
* as elements of sets may only have multiplicity one, and it is unclear that is
* an appropriate output for elements with negative multiplicity.
*/
distinct() {
return this.reduce((vals) => {
const consolidated = new Map();
for (const [val, multiplicity] of vals) {
const key = hash(val);
const current = consolidated.get(key)?.[1] || 0;
consolidated.set(key, [val, current + multiplicity]);
}
const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0);
for (const [_, multiplicity] of validVals) {
if (multiplicity <= 0) {
throw new Error('Negative multiplicities not allowed in distinct operation');
}
}
return validVals.map(([val, _]) => [val, 1]);
});
}
/**
* Repeatedly invoke a function f on a collection, and return the result
* of applying the function an infinite number of times (fixedpoint).
*
* Note that if the function does not converge to a fixedpoint this implementation
* will run forever.
*/
iterate(f) {
let curr = new MultiSet(this.#inner);
let result = f(curr);
while (JSON.stringify(result.#inner) !== JSON.stringify(curr.#inner)) {
curr = result;
result = f(curr);
}
return curr;
}
extend(other) {
const otherArray = other instanceof MultiSet ? other.getInner() : other;
chunkedArrayPush(this.#inner, otherArray);
}
getInner() {
return this.#inner;
}
}
//# sourceMappingURL=multiset.js.map