mingo
Version:
MongoDB query language for in-memory objects
165 lines (164 loc) • 5.75 kB
JavaScript
import { concat, Lazy } from "../../lazy";
import {
assert,
HashMap,
isArray,
isDate,
isNil,
isNumber,
isObject,
isString,
resolve
} from "../../util";
import { TIME_UNITS } from "../expression/date/_internal";
import { $dateAdd } from "../expression/date/dateAdd";
import { $sort } from "./sort";
const $densify = (collection, expr, options) => {
const { step, bounds, unit } = expr.range;
if (unit) {
assert(TIME_UNITS.includes(unit), "");
assert(
Number.isInteger(step) && step > 0,
"The step parameter in a range statement must be a whole number when densifying a date range."
);
} else {
assert(
isNumber(step) && step > 0,
"The step parameter in a range statement must be a strictly positive numeric value."
);
}
if (isArray(bounds)) {
assert(
!!bounds && bounds.length === 2,
"A bounding array in a range statement must have exactly two elements."
);
assert(
(bounds.every(isNumber) || bounds.every(isDate)) && bounds[0] < bounds[1],
"A bounding array must be an ascending array of either two dates or two numbers."
);
assert(
unit && !bounds.some(isNumber),
"Numeric bounds may not have unit parameter."
);
}
if (expr.partitionByFields) {
assert(
isArray(expr.partitionByFields),
"$densify: `partitionByFields` must be an array of strings"
);
}
collection = $sort(collection, { [expr.field]: 1 }, options);
const computeNextValue = (value) => {
return isNumber(value) ? value + step : $dateAdd(null, { startDate: value, unit, amount: step }, options);
};
const isValidUnit = !!unit && TIME_UNITS.includes(unit);
const getFieldValue = (o) => {
const v = resolve(o, expr.field);
assert(
isNil(v) || isDate(v) && isValidUnit || isNumber(v) && !isValidUnit,
"$densify: Densify field type must be numeric with 'unit' unspecified, or a date with 'unit' specified."
);
return v;
};
const peekItem = new Array();
const nilFieldsIterator = Lazy(() => {
const item = collection.next();
const fieldValue = getFieldValue(item.value);
if (isNil(fieldValue)) return item;
peekItem.push(item);
return { done: true };
});
const nextDensifyValueMap = HashMap.init();
const [lower, upper] = isArray(bounds) ? bounds : [bounds, bounds];
let maxFieldValue = void 0;
const updateMaxFieldValue = (value) => {
maxFieldValue = maxFieldValue === void 0 || maxFieldValue < value ? value : maxFieldValue;
};
const rootKey = [];
const densifyIterator = Lazy(() => {
const item = peekItem.length > 0 ? peekItem.pop() : collection.next();
if (item.done) return item;
let partitionKey = rootKey;
if (isArray(expr.partitionByFields)) {
partitionKey = expr.partitionByFields.map(
(k) => resolve(item.value, k)
);
assert(
partitionKey.every(isString),
"$densify: Partition fields must evaluate to string values."
);
}
assert(isObject(item.value), "$densify: collection must contain documents");
const itemValue = getFieldValue(item.value);
if (!nextDensifyValueMap.has(partitionKey)) {
if (lower == "full") {
if (!nextDensifyValueMap.has(rootKey)) {
nextDensifyValueMap.set(rootKey, itemValue);
}
nextDensifyValueMap.set(partitionKey, nextDensifyValueMap.get(rootKey));
} else if (lower == "partition") {
nextDensifyValueMap.set(partitionKey, itemValue);
} else {
nextDensifyValueMap.set(partitionKey, lower);
}
}
const densifyValue = nextDensifyValueMap.get(partitionKey);
if (
// current item field value is lower than current densify value.
itemValue <= densifyValue || // range value equals or exceeds upper bound
upper != "full" && upper != "partition" && densifyValue >= upper
) {
if (densifyValue <= itemValue) {
nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
}
updateMaxFieldValue(itemValue);
return item;
}
nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
updateMaxFieldValue(densifyValue);
const denseObj = { [expr.field]: densifyValue };
if (partitionKey) {
partitionKey.forEach((v, i) => {
denseObj[expr.partitionByFields[i]] = v;
});
}
peekItem.push(item);
return { done: false, value: denseObj };
});
if (lower !== "full") return concat(nilFieldsIterator, densifyIterator);
let paritionIndex = -1;
let partitionKeysSet = void 0;
const fullBoundsIterator = Lazy(() => {
if (paritionIndex === -1) {
const fullDensifyValue = nextDensifyValueMap.get(rootKey);
nextDensifyValueMap.delete(rootKey);
partitionKeysSet = Array.from(nextDensifyValueMap.keys());
if (partitionKeysSet.length === 0) {
partitionKeysSet.push(rootKey);
nextDensifyValueMap.set(rootKey, fullDensifyValue);
}
paritionIndex++;
}
do {
const partitionKey = partitionKeysSet[paritionIndex];
const partitionMaxValue = nextDensifyValueMap.get(partitionKey);
if (partitionMaxValue < maxFieldValue) {
nextDensifyValueMap.set(
partitionKey,
computeNextValue(partitionMaxValue)
);
const denseObj = { [expr.field]: partitionMaxValue };
partitionKey.forEach((v, i) => {
denseObj[expr.partitionByFields[i]] = v;
});
return { done: false, value: denseObj };
}
paritionIndex++;
} while (paritionIndex < partitionKeysSet.length);
return { done: true };
});
return concat(nilFieldsIterator, densifyIterator, fullBoundsIterator);
};
export {
$densify
};