/**
* Roboliq: Automation for liquid-handling robots
* @copyright 2017, ETH Zurich, Ellis Whitehead
* @license GPL-3.0
*/
'use babel';
/**
* Functions for processing design specifications.
* In particular, it can take concise design specifications and expand them
* into a long table of factor values.
* @module
*/
import _ from 'lodash';
import assert from 'assert';
// import Immutable, {Map, fromJS} from 'immutable';
import math from 'mathjs';
import naturalSort from 'javascript-natural-sort';
import Random from 'random-js';
import stableSort from 'stable';
//import yaml from 'yamljs';
import wellsParser from './parsers/wellsParser.js';
const DEBUG = false;
//import {locationRowColToText} from './parsers/wellsParser.js';
// FIXME: HACK: this function is included here temporarily, to make usage in react component easier for the moment
function locationRowColToText(row, col) {
var colText = col.toString();
if (colText.length == 1) colText = "0"+colText;
return String.fromCharCode("A".charCodeAt(0) + row - 1) + colText;
}
/**
* Print a text representation of the table
* @param {array} rows - array of rows
* @param {Boolean} [hideRedundancies] - suppress printing of values that haven't changed from the previous row
*/
export function printRows(rows, hideRedundancies = false) {
const data = _.flattenDeep(rows);
// Get column names
const columnMap = {};
_.forEach(data, row => _.forEach(_.keys(row), key => { columnMap[key] = true; } ));
const columns = _.keys(columnMap);
// console.log({columns})
// Convert data to array of lines (which are arrays of columns)
const lines = [];
_.forEach(data, group => {
if (!_.isArray(group)) {
group = [group];
}
else {
lines.push(["---"]);
}
_.forEach(group, row => {
// console.log(JSON.stringify(row))
const line = _.map(columns, key => {
const x1 = _.get(row, key, "");
const x2 = (_.isNull(x1)) ? "" : x1;
return x2.toString();
});
lines.push(line);
});
});
// Calculate column widths
const widths = _.map(columns, key => key.length);
// console.log({widths})
_.forEach(lines, line => {
_.forEach(line, (s, i) => { if (!_.isEmpty(s)) widths[i] = Math.max(widths[i], s.length); });
});
// console.log({widths})
console.log(columns.map((s, i) => _.padEnd(s, widths[i])).join(" "));
console.log(columns.map((s, i) => _.repeat("=", widths[i])).join(" "));
let linePrev;
_.forEach(lines, line => {
const s = line.map((s, i) => {
const s2 = (s === "") ? "-"
: (hideRedundancies && linePrev && s === linePrev[i]) ? ""
: s;
return _.padEnd(s2, widths[i]);
}).join(" ");
console.log(s);
linePrev = line;
});
console.log(columns.map((s, i) => _.repeat("=", widths[i])).join(" "));
}
/**
* Print a TAB-formatted representation of the table
* @param {array} rows - array of rows
*/
export function printTAB(rows) {
const hideRedundancies = false;
const data = _.flattenDeep(rows);
// Get column names
const columnMap = {};
_.forEach(data, row => _.forEach(_.keys(row), key => { columnMap[key] = true; } ));
const columns = _.keys(columnMap);
// console.log({columns})
// Convert data to array of lines (which are arrays of columns)
const lines = [];
_.forEach(data, group => {
if (!_.isArray(group)) {
group = [group];
}
else {
lines.push(["---"]);
}
_.forEach(group, row => {
// console.log(JSON.stringify(row))
const line = _.map(columns, key => {
const x1 = _.get(row, key, "");
const x2 = (_.isNull(x1)) ? "" : x1;
return x2.toString();
});
lines.push(line);
});
});
console.log(columns.join("\t"));
_.forEach(lines, line => {
const s = line.join("\t");
console.log(s);
});
}
/**
* Print a markdown pipe table
* @param {array} rows - array of rows
*/
export function printMarkdown(rows) {
const hideRedundancies = false;
const data = _.flattenDeep(rows);
// Get column names
const columnMap = {};
_.forEach(data, row => _.forEach(_.keys(row), key => { columnMap[key] = true; } ));
const columns = _.keys(columnMap);
// console.log({columns})
// Convert data to array of lines (which are arrays of columns)
const lines = [];
_.forEach(data, group => {
if (!_.isArray(group)) {
group = [group];
}
else {
lines.push(["---"]);
}
_.forEach(group, row => {
// console.log(JSON.stringify(row))
const line = _.map(columns, key => {
const x1 = _.get(row, key, "");
const x2 = (_.isNull(x1)) ? "" : x1;
return x2.toString();
});
lines.push(line);
});
});
console.log(columns.join(" | "));
console.log(columns.map(s => ":-----:").join(" | "));
_.forEach(lines, line => {
const s = line.join(" | ");
console.log(s);
});
console.log();
}
/**
* Turn a design specification into a design table.
* @param {object} design - the design specification.
*/
export function flattenDesign(design, randomEngine) {
if (_.isEmpty(design)) {
return [];
}
randomEngine = randomEngine || Random.engines.mt19937();
const randomSeed = _.isNumber(design.randomSeed) ? design.randomSeed : 0;
randomEngine.seed(randomSeed);
let children;
if (_.isArray(design.children)) {
children = design.children.map(child => flattenDesign(child, randomEngine));
}
else {
const conditionsList = _.isArray(design.design) ? design.design : [design.design];
children = conditionsList.map(conditions => expandConditions(conditions, randomEngine, design.initialRows));
}
let rows;
if (children.length == 1) {
rows = children[0];
}
// If there were multiple children, we'll need to join them: either merge columns or concat rows
else {
// console.log(JSON.stringify(children, null, '\t'))
const joinMethod = design.join || "concat";
if (joinMethod === "merge") {
rows = _.merge.apply(_, [[]].concat(children));
}
else {
rows = [].concat(...children);
}
}
if (design.where) {
rows = filterOnWhere(rows, design.where);
}
if (design.orderBy) {
rows = _.orderBy(rows, design.orderBy);
}
if (design.select) {
rows = rows.map(row => _.pick(row, design.select));
}
return rows;
}
export function getCommonValues(table) {
if (_.isEmpty(table)) return {};
assert(_.isArray(table), `required an array: ${JSON.stringify(table)}`);
let common = _.clone(table[0]);
for (let i = 1; i < table.length; i++) {
// Remove any value from common which aren't shared with this row.
_.forEach(table[i], (value, name) => {
if (common.hasOwnProperty(name) && !_.isEqual(common[name], value)) {
delete common[name];
}
});
}
return common;
}
export function getCommonValuesNested(nestedRows, rowIndexes, common) {
if (_.isEmpty(rowIndexes)) return {};
for (let i = 0; i < rowIndexes.length; i++) {
const rowIndex = rowIndexes[i];
const row = nestedRows[rowIndex];
if (_.isArray(row)) {
getCommonValuesNested(row, _.range(row.length), common);
}
else if (_.isUndefined(common)) {
common = _.clone(row);
}
else {
// Remove any value from common which aren't shared with this row.
_.forEach(row, (value, name) => {
if (common.hasOwnProperty(name) && !_.isEqual(common[name], value)) {
delete common[name];
}
});
}
}
return common;
}
/**
* Is like _.flattenDeep, but it mutates the array in-place.
*
* @param {array} rows - array to flatten
*/
export function flattenArrayM(rows) {
let i = rows.length;
while (i > 0) {
i--;
const item = rows[i];
if (_.isArray(item)) {
// Flatten the sub-array
flattenArrayM(item);
// Splice the original sub-array back into the parent array
rows.splice(i, 1, ...item);
}
}
return rows;
}
/**
* Is like _.flattenDeep, but only for the given rows, and it mutates both the rows array and rowIndexes array in-place.
*
* @param {array} rows - array to flatten
* @param {array} rowIndexes - array of row indexes to flatten
* @param {array} [otherRowIndexes] - a second, optional array of row indexes that should have the same modifications made to it as rowIndexes
* @param {integer} rowIndexesOffset - index in rowIndexes to start at
*/
export function flattenArrayAndIndexes(rows, rowIndexes, otherRowIndexes = []) {
if (DEBUG) {
console.log(`flattenArrayAndIndexes:`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(rows)}`)
}
let i = 0;
while (i < rowIndexes.length) {
const rowIndex = rowIndexes[i];
const item = rows[rowIndex];
if (_.isArray(item)) {
// Flatten the sub-array
flattenArrayM(item);
// Splice the original sub-array back into the parent array
rows.splice(rowIndex, 1, ...item);
// Update rowIndexes
for (let j = i + 1; j < rowIndexes.length; j++) {
rowIndexes[j] += item.length - 1;
}
// console.log(` 1: ${rowIndexes.join(",")}`)
const x = _.range(rowIndex, rowIndex + item.length);
// console.log({x})
rowIndexes.splice(i, 1, ...x);
// console.log(` 2: ${rowIndexes.join(",")}`)
for (let m = 0; m < otherRowIndexes.length; m++) {
const rowIndexes2 = otherRowIndexes[m];
let k = -1;
for (let j = 0; j < rowIndexes2.length; j++) {
if (rowIndexes2[j] === rowIndex) {
k = j;
}
else if (rowIndexes2[j] > rowIndex) {
rowIndexes2[j] += item.length - 1;
}
}
if (k >= 0) {
const x = _.range(rowIndex, rowIndex + item.length);
// console.log({x})
rowIndexes2.splice(k, 1, ...x);
}
// console.log({m, len: otherRowIndexes.length, k})
// console.log(` 3 otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
}
i += item.length;
}
else {
i++;
}
// console.log(` 4 otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
}
if (DEBUG) { console.log(" (flattenArrayAndIndexes) otherRowIndexes: "+JSON.stringify(otherRowIndexes)); }
}
/**
* If conditions is an array, then each element will be processed individually and then the results will be merged together.
* @param {object|array} conditions - an object of conditions or an array of such objects.
* @param {array} table0 - the initial rows to start expanding conditions on (default `[{}]`)
*/
export function expandConditions(conditions, randomEngine, table0 = [{}]) {
// console.log("expandConditions: "+JSON.stringify(conditions))
const conditionsList = _.isArray(conditions) ? conditions : [conditions];
const conditionsRows = conditionsList.map(conditions => {
const table = _.cloneDeep(table0);
expandRowsByObject(table, _.range(0, table.length), [], conditions, randomEngine);
flattenArrayM(table);
return table;
});
let table = (conditionsRows.length == 1)
? conditionsRows[0]
: _.merge.apply(_, [[]].concat(conditionsRows)); // should probably be `_.merge([], ...conditionsRows)`
return table;
}
/**
* expandRowsByObject:
* for each key/value pair, call expandRowsByNamedValue
*/
function expandRowsByObject(nestedRows, rowIndexes, otherRowIndexes, conditions, randomEngine) {
if (DEBUG) {
console.log("expandRowsByObject: "+JSON.stringify(conditions));
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${rowIndexes}\n ${JSON.stringify(nestedRows)}`)
assertNoDuplicates(otherRowIndexes);
}
for (let name in conditions) {
expandRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, conditions[name], randomEngine);
}
}
/**
* // REQUIRED by: expandRowsByObject
* expandRowsByNamedValue:
* TODO: turn the name/value into an action in order to allow for more sophisticated expansion
* if has star-suffix, call branchRowsByNamedValue
* else call assignRowsByNamedValue
*/
export function expandRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, value, randomEngine) {
if (DEBUG) {
console.log(`expandRowsByNamedValue: ${name}, ${JSON.stringify(value)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(nestedRows)}`)
assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat([rowIndexes]));
}
// If an action is specified using the "=" symbol:
const iEquals = name.indexOf("=");
if (iEquals >= 0) {
// Need to flatten the rows in case the action uses groupBy or sameBy
flattenArrayAndIndexes(nestedRows, rowIndexes, otherRowIndexes);
// console.log(` 'otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
// console.log(` 'rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(nestedRows)}`)
const actionType = name.substr(iEquals + 1) || "assign";
const actionHandler = actionHandlers[actionType];
assert(actionHandler, `unknown action type: ${actionType} in ${name}`)
name = name.substr(0, iEquals);
const result = actionHandler(nestedRows, rowIndexes, otherRowIndexes, name, value, randomEngine);
// If no result was returned, the action handled modified the rows directly:
if (_.isUndefined(result)) {
return;
}
// Otherwise, continue processing using the action's results
else {
value = result;
}
}
const starIndex = name.indexOf("*");
if (starIndex >= 0) {
// Remove the branching suffix from the name
name = name.substr(0, starIndex);
// If the name is empty, automatically pick a dummy name that will be omitted
if (_.isEmpty(name)) {
name = ".HIDDEN";
}
// If the branching value is just a number, then assume it means the number of replicates
if (_.isNumber(value)) {
value = _.range(1, value + 1);
}
// console.log({loc: "A", rowIndexes, nestedRows})
branchRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, value, randomEngine);
// console.log("B")
}
else {
assignRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, value, randomEngine, true);
}
}
/*
* // REQUIRED by: expandRowsByNamedValue, branchRowsByNamedValue
* assignRowsByNamedValue: (REQUIRED FOR ASSIGNING ARRAY TO ROWS)
* if value is array:
* for i in count:
* rowIndex = rowIndexes[i]
* assignRowByNamedKeyItem(nestedRows, rowIndex, name, i+1, value[i])
* else if value is object:
* keys = _.keys(value)
* for each i in keys.length:
* key = keys[i]
* item = value[key]
* assignRowByNamedKeyItem(nestedRows, rowIndex, name, key, item)
* else:
* for each row:
* setColumnValue(row, name, value)
*/
function assignRowsByNamedValue(nestedRows, rowIndexesGroups, otherRowIndexes, name, value, randomEngine, doUnnest = true) {
const l = (_.every(rowIndexesGroups, l => _.isArray(l))) ? rowIndexesGroups : [rowIndexesGroups];
if (DEBUG) {
console.log(`assignRowsByNamedValue: ${name}, ${JSON.stringify(value)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexesGroups: ${JSON.stringify(rowIndexesGroups)}\n ${JSON.stringify(nestedRows)}`)
assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat(l));
//printRows(nestedRows)
}
const otherRowIndexes2 = otherRowIndexes.concat(l);
for (let il = 0; il < l.length; il++) {
const rowIndexes = _.clone(l[il]);
// console.log({il, rowIndexes, l})
let valueIndex = 0;
const isSpecial = value instanceof Special;
if (isSpecial) {
value.reset();
}
/*// If value is an array of objects
if (_.isArray(value) && _.every(value, x => _.isObject(x))) {
// Assign indexes
const valueIndexes = _.range(1, value.length + 1);
assignRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, valueIndexes, randomEngine, doUnnest);
// Assign objects
expandRowsByObject(nestedRows, rowIndexes, otherRowIndexes, item, randomEngine);
}
else*/ if (isSpecial || _.isArray(value)) {
for (let i = 0; i < rowIndexes.length; i++) {
const rowIndex = rowIndexes[i];
const rowIndexes2 = [rowIndex];
const n = assignRowByNamedValuesKey(nestedRows, rowIndex, otherRowIndexes.concat([rowIndexes, rowIndexes2]), name, value, valueIndex, undefined, randomEngine, doUnnest);
valueIndex += n;
i += rowIndexes2.length - 1;
if (DEBUG) {
console.log({rowIndex, dRowIndex: rowIndexes2.length, dValueIndex: n, valueIndex, i, rowIndexes})
console.log(` (assignRowsByNamedValue:) rowIndexes: ${rowIndexes.join(", ")}`)
}
}
}
else if (_.isPlainObject(value)) {
let valueIndex = 0;
const keys = _.keys(value);
for (let i = 0; i < rowIndexes.length; i++) {
const rowIndex = rowIndexes[i];
const rowIndexes2 = [rowIndex];
const n = assignRowByNamedValuesKey(nestedRows, rowIndex, otherRowIndexes.concat([rowIndexes, rowIndexes2]), name, value, valueIndex, keys, randomEngine, doUnnest);
valueIndex += n;
i += rowIndexes2.length - 1;
}
}
else {
for (let i = 0; i < rowIndexes.length; i++) {
const rowIndex = rowIndexes[i];
setColumnValue(nestedRows[rowIndex], name, value);
// console.log(JSON.stringify(nestedRows))
}
}
}
}
/*
* // REQUIRED by: assignRowsByNamedValue
* assignRowByNamedValuesKey:
* if item is array:
* setColumnValue(row, name, key)
* branchRowByArray(nestdRows, rowIndex, item)
* else if item is object:
* setColumnValue(row, name, key)
* expandRowsByObject(nestedRows, [rowIndex], item)
* else:
* setColumnValue(row, name, item)
* Returns number of values actually assigned (may be more than one for nested rows)
*/
function assignRowByNamedValuesKey(nestedRows, rowIndex, otherRowIndexes, name, values, valueKeyIndex, valueKeys, randomEngine, doUnnest) {
assert(!_.isUndefined(doUnnest));
if (DEBUG) {
console.log(`assignRowByNamedValuesKey: ${name}, ${JSON.stringify(values)}, ${valueKeyIndex}, ${valueKeys}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndex: ${rowIndex}\n ${JSON.stringify(nestedRows)}`)
assertNoDuplicates(otherRowIndexes);
}
const row = nestedRows[rowIndex];
let n = (doUnnest) ? 0 : 1;
if (_.isArray(row)) {
// console.log("0")
const otherRowIndexes2 = otherRowIndexes.concat([_.range(row.length)]);
for (let i = 0; i < row.length; i++) {
// console.log(` (assignRowByNamedValuesKey) #${i} of ${row.length}`)
const n2 = assignRowByNamedValuesKey(row, i, otherRowIndexes2, name, values, valueKeyIndex, valueKeys, randomEngine, doUnnest);
if (doUnnest) {
n += n2;
valueKeyIndex += n2;
}
// console.log({i, n2, n, valueKeyIndex, row})
}
flattenArrayAndIndexes(nestedRows, [rowIndex], otherRowIndexes);
}
else {
// Error.stackTraceLimit = Infinity;
// console.log("A")
let item, key;
if (values instanceof Special) {
// console.log("B: "+rowIndex)
// console.log(nestedRows[rowIndex])
const result = values.next(nestedRows, rowIndex);
// console.log({result})
key = result[0];
item = result[1];
// [key, item] = result;
}
else {
// console.log("C")
assert(valueKeyIndex < _.size(values), `fewer values (${_.size(values)}) than rows: `+JSON.stringify({name, values}));
const valueKey = (valueKeys) ? valueKeys[valueKeyIndex] : valueKeyIndex;
key = (valueKeys) ? valueKey : valueKey + 1;
item = values[valueKey];
}
// console.log("D")
// console.log({item})
const rowIndexes2 = [rowIndex];
if (_.isArray(item)) {
setColumnValue(row, name, key);
branchRowByArray(nestedRows, rowIndex, otherRowIndexes, item, randomEngine);
}
else if (_.isPlainObject(item)) {
setColumnValue(row, name, key);
expandRowsByObject(nestedRows, rowIndexes2, otherRowIndexes, item, randomEngine);
}
else {
setColumnValue(row, name, item);
}
// n = rowIndexes2.length
n = 1;
}
if (DEBUG) {
console.log(` (assignRowByNamedValuesKey): ${JSON.stringify(nestedRows)}`)
}
return n;
}
/*
* // REQUIRED by: expandRowsByNamedValue
* branchRowsByNamedValue:
* size
* = (value is array) ? value.length
* : (value is object) ? _.size(value)
* : 1
* row0 = nestedRows[rowIndex];
* rows2 = Array(size)
* for each rowIndex2 in _.range(size):
* rows2[rowIndex] = _.cloneDeep(row0)
*
* expandRowsByNamedValue(rows2, _.range(size), name, value);
* nestedRows[rowIndex] = _.flattenDeep(rows2);
*/
function branchRowsByNamedValue(nestedRows, rowIndexes, otherRowIndexes, name, value, randomEngine) {
if (DEBUG) {
console.log(`branchRowsByNamedValue: ${name}, ${JSON.stringify(value)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(nestedRows)}`)
}
const isSpecial = (value instanceof Special);
const size
= (_.isArray(value)) ? value.length
: (_.isPlainObject(value)) ? _.size(value)
: (isSpecial) ? value.valueCount
: 1;
//flattenArrayAndIndexes(nestedRows, rowIndexes, otherRowIndexes);
// Create 'size' copies of each row in rowIndexes.
const rows2 = Array(size * rowIndexes.length);
const rowIndexesGroups2 = Array(rowIndexes.length);
const rowIndexesGroups2Transposed = _.range(size).map(i => Array(rowIndexes.length));
for (let j = 0; j < rowIndexes.length; j++) {
const rowIndex = rowIndexes[j];
rowIndexesGroups2[j] = Array(size);
for (let i = 0; i < size; i++) {
const k = j * size + i;
rowIndexesGroups2[j][i] = k;
rows2[k] = _.cloneDeep(nestedRows[rowIndex]);
rowIndexesGroups2Transposed[i][j] = k;
}
}
// console.log({rows2, rowIndexesGroups2, rowIndexesGroups2Transposed});
if (_.isArray(value) && _.every(value, x => _.isPlainObject(x))) {
// Assign indexes
const valueIndexes = _.range(1, value.length + 1);
assignRowsByNamedValue(rows2, rowIndexesGroups2, rowIndexesGroups2Transposed, name, valueIndexes, randomEngine, false);
// Assign objects
const otherRowIndexes3 = rowIndexesGroups2Transposed.concat(rowIndexesGroups2);
for (let i = 0; i < size; i++) {
const rowIndexes3 = _.clone(rowIndexesGroups2Transposed[i]);
expandRowsByObject(rows2, rowIndexes3, otherRowIndexes3, value[i], randomEngine);
}
}
else if (_.isPlainObject(value)) {
// Assign indexes
const valueIndexes = _.keys(value);
assignRowsByNamedValue(rows2, rowIndexesGroups2, rowIndexesGroups2Transposed, name, valueIndexes, randomEngine, false);
// Assign objects
const otherRowIndexes3 = rowIndexesGroups2Transposed.concat(rowIndexesGroups2);
for (let i = 0; i < size; i++) {
const key = valueIndexes[i];
const rowIndexes3 = _.clone(rowIndexesGroups2Transposed[i]);
expandRowsByObject(rows2, rowIndexes3, otherRowIndexes3, value[key], randomEngine);
}
}
else {
// Assign to those copies
assignRowsByNamedValue(rows2, rowIndexesGroups2, [], name, value, randomEngine, false);
}
// console.log({rows2, rowIndexesGroups2, rowIndexesGroups2Transposed});
flattenArrayAndIndexes(rows2, [], rowIndexesGroups2);
// console.log({rows2, rowIndexesGroups2});
// console.log({nestedRows})
// Transpose back into nestedRows
for (let j = 0; j < rowIndexes.length; j++) {
const rowIndexes3 = rowIndexesGroups2[j];
const rows3 = rowIndexes3.map(i => rows2[i]);
const rowIndex = rowIndexes[j];
// console.log({rows3, rowIndex})
nestedRows[rowIndex] = rows3;
}
// console.log({nestedRows})
// console.log({loc: "B", otherRowIndexes})
// console.log(JSON.stringify(nestedRows))
flattenArrayAndIndexes(nestedRows, rowIndexes, otherRowIndexes);
// console.log({loc: "C", otherRowIndexes})
}
function branchRowByArray(nestedRows, rowIndex, otherRowIndexes, values, randomEngine) {
if (DEBUG) {
console.log(`branchRowByArray: ${JSON.stringify(values)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndex: ${rowIndex}\n ${JSON.stringify(nestedRows)}`)
assertNoDuplicates(otherRowIndexes);
}
const size = values.length;
// Make replicates of row
const row0 = nestedRows[rowIndex];
const rows2 = Array(size);
for (let rowIndex2 = 0; rowIndex2 < size; rowIndex2++) {
const value = values[rowIndex2];
rows2[rowIndex2] = _.cloneDeep(row0);
expandRowsByObject(rows2, [rowIndex2], [], values[rowIndex2], randomEngine);
}
nestedRows[rowIndex] = _.flattenDeep(rows2);
flattenArrayAndIndexes(nestedRows, [rowIndex], otherRowIndexes);
if (DEBUG) {
console.log(` (branchRowByArray): ${JSON.stringify(nestedRows)}`)
}
}
// Set the given value, but only if the name doesn't start with a period
function setColumnValue(row, name, value) {
if (DEBUG) { console.log(`setColumnValue: ${name}, ${JSON.stringify(value)}`); console.log("row: "+JSON.stringify(row)); }
if (name.length >= 1 && name[0] != ".") {
// Recurse into sub-rows
if (_.isArray(row)) {
// console.log("isArray")
for (let i = 0; i < row.length; i++) {
setColumnValue(row[i], name, value);
}
}
// Set the value in the row
else {
row[name] = value;
// console.log(`row[name] = ${row[name]}`)
}
}
}
class Special {
constructor({action, draw, reuse, randomEngine}, next, initGroup) {
this.action = action;
this.draw = draw;
this.reuse = reuse;
this.randomEngine = randomEngine;
this.next = (next || this.defaultNext);
this.initGroup = (initGroup || this.defaultInitGroup);
this.reset = this.defaultReset;
}
defaultInitGroup(nestedRows, rowIndexes) {
this.nextIndex = 0;
this.valueCount = _.size(this.action.values);
// Initialize this.indexes
switch (this.draw) {
case "direct":
this.indexes = _.range(this.valueCount);
break;
case "shuffle":
if (this.action.shuffleOnce !== true || !this.indexes) {
this.indexes = Random.sample(this.randomEngine, _.range(this.valueCount), this.valueCount);
} else {
// FIXME: if this.valueCount is now larger than this.indexes.length, then generate more indexes
}
break;
}
}
defaultNext() {
// if (DEBUG) { console.log("defaultNext: "+)}
// console.log({this});
if (this.nextIndex >= this.valueCount) {
// console.log(`next: this.nextIndex >= this.valueCount, ${this.nextIndex} >= ${this.valueCount}`)
switch (this.reuse) {
case "repeat":
this.nextIndex = 0;
break;
case "reverse":
this.indexes = _.reverse(this.indexes);
this.nextIndex = 0;
break;
case "reshuffle":
this.indexes = Random.sample(this.randomEngine, _.range(this.valueCount), this.valueCount);
this.nextIndex = 0;
// console.log("shuffled indexes: "+this.indexes)
break;
default:
assert(false, "not enough values supplied to fill the rows: "+JSON.stringify(this.action));
}
// console.log("this.nextIndex = "+this.nextIndex)
}
let index, key;
switch (this.draw) {
case "direct":
index = this.indexes[this.nextIndex];
key = index + 1;
break;
case "shuffle":
index = this.indexes[this.nextIndex];
key = index + 1;
break;
case "sample":
index = Random.integer(0, this.valueCount - 1)(this.randomEngine);
key = index + 1;
break;
default:
assert(false, "unknown 'draw' value: "+JSON.stringify(this.draw)+" in "+JSON.stringify(this.action));
}
// console.log({index, key})
const value = this.action.values[index];
if (this.draw !== "sample") {
this.nextIndex++;
}
return [key, value];
}
defaultReset() {
if (this.nextIndex) {
this.nextIndex = 0;
}
}
}
/*
function countRows(nestedRows, rowIndexes) {
let sum = 0;
for (let i = 0; i < rowIndexes.length; i++) {
const rowIndex = rowIndexes[i];
const row = nestedRows[rowIndex];
if (_.isPlainObject(row)) {
sum++;
}
else {
sum += countRows(row, _.range(row.length));
}
}
return sum;
}*/
/**
* If an action handler return 'undefined', it means that the handler took care of the action already.
* @type {Object}
*/
const actionHandlers = {
"allocatePlates": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
const action2 = _.cloneDeep(action);
return assign(rows, rowIndexes, otherRowIndexes, name, action2, randomEngine, undefined, assign_allocatePlates_initGroup);
},
"allocateWells": (_rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
const rows = action.rows;
const cols = action.columns;
const rowJump = action.rowJump || 0; // whether to jump over rows (and then return to the skipped ones), and by how much
assert(_.isNumber(rows) && rows > 0, "missing required positive number `rows`");
assert(_.isNumber(cols) && cols > 0, "missing required positive number `columns`");
assert(_.isNumber(rowJump) && rowJump >= 0, "`rowJump`, if specified, must be a number >= 0");
const from0 = action.from || 1;
let iFrom;
if (_.isInteger(from0)) {
iFrom = from0 - 1;
}
else {
const [rowFrom, colFrom] = wellsParser.locationTextToRowCol(from0);
iFrom = (colFrom - 1) * rows + (rowFrom - 1);
// console.log({from0, rowFrom, colFrom, rows, iFrom})
}
let values;
if (action.wells) {
values = wellsParser.parse(action.wells, {}, {rows, columns: cols});
// TODO: handle `from` for both cases of well name or for integer
}
else {
const byColumns = _.get(action, "byColumns", true);
values = _.range(iFrom, rows * cols).map(i => {
const [row0, col] = (byColumns) ? [i % rows, Math.floor(i / rows)] : [Math.floor(i / cols), i % cols];
// Calculate row when jumping
const row1 = row0 * (rowJump + 1);
const rowLayer = Math.floor(row1 / rows);
const row = (row1 + rowLayer) % rows;
const s = locationRowColToText(row + 1, col + 1);
// console.log({row, col, s});
return s;
});
}
// console.log({values})
const action2 = _.cloneDeep(action);
action2.values = values;
// console.log({values})
return assign(_rows, rowIndexes, otherRowIndexes, name, action2, randomEngine);
},
"assign": assign,
"case": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
if (DEBUG) {
console.log(`=case: ${name}=${JSON.stringify(action)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(rows)}`)
assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat([rowIndexes]));
}
const cases = _.isArray(action) ? action : action.cases;
const caseMap = _.isArray(cases)
? cases.map((v, i) => [i + 1, v])
: _.toPairs(cases);
// Group the rows according to the first case they satisfy
const rowIndexesGroups = caseMap.map(x => []);
for (let j = 0; j < rowIndexes.length; j++) {
const rowIndex = rowIndexes[j];
const row = rows[rowIndex];
const table1 = [row];
for (let i = 0; i < caseMap.length; i++) {
const [caseName, caseSpec] = caseMap[i];
if (!caseSpec.where || filterOnWhere(table1, caseSpec.where).length === 1) {
rowIndexesGroups[i].push(rowIndex);
break;
}
}
}
if (DEBUG) { console.log({caseMap, rowIndexesGroups}); }
const otherRowIndexes2 = otherRowIndexes.concat([rowIndexes]).concat(rowIndexesGroups);
for (let i = 0; i < caseMap.length; i++) {
const [caseName, caseSpec] = caseMap[i];
const rowIndexes2 = _.clone(rowIndexesGroups[i]);
expandRowsByNamedValue(rows, rowIndexes2, otherRowIndexes2, name, caseName, randomEngine)
expandRowsByObject(rows, rowIndexes2, otherRowIndexes2, caseSpec.design, randomEngine);
}
},
"calculate": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
const expr = _.isString(action) ? action : action.expression;
const action2 = _.isString(action) ? {} : action;
return assign(rows, rowIndexes, otherRowIndexes, name, {}, randomEngine, assign_calculate_next(expr, action2));
},
// TODO: consider renaming to `calculateWellColumn`
"calculateColumn": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
return assign(rows, rowIndexes, otherRowIndexes, name, {}, randomEngine, assign_calculateColumn_next(action, {}));
},
"calculateRow": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
return assign(rows, rowIndexes, otherRowIndexes, name, {}, randomEngine, assign_calculateRow_next(action, {}));
},
"calculateWell": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
return assign(rows, rowIndexes, otherRowIndexes, name, {}, randomEngine, assign_calculateWell_next(action, {}));
},
"concat": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
if (DEBUG) {
console.log(`=concat: ${name}=${JSON.stringify(action)}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(rows)}`)
assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat([rowIndexes]));
}
// find groups (either from `groupBy` or just use everything in rowIndexes)
const groupBy = _.isArray(action.groupBy)
? action.groupBy
: _.isEmpty(action.groupBy)
? [] : [action.groupBy];
const rowIndexesGroups = (!_.isEmpty(groupBy))
? query_groupBy(rows, rowIndexes, groupBy)
: [_.clone(rowIndexes)];
// for each group, create a temporary row with the group variables and then expand the conditions on that row
for (let i = 0; i < rowIndexesGroups.length; i++) {
// create a temporary row with the group variables
const rowIndexesGroup = rowIndexesGroups[i];
const row0 = (_.isEmpty(groupBy)) ? {} : _.pick(rows[rowIndexesGroup[0]], groupBy);
const rows2 = [row0];
const rowIndexes2 = [0];
// console.log({groupBy, rowIndexesGroup, row0})
// expand the conditions on that row
expandRowsByObject(rows2, rowIndexes2, [], action.design, randomEngine);
// Add the rows back to the original table
rows.push(...rows2);
rowIndexes.push(..._.range(rowIndexes.length, rowIndexes.length + rows2.length));
}
return undefined;
},
"range": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
const action2 = _.cloneDeep(_.isNull(action) ? {} : action);
_.defaults(action2, {from: 1, step: 1});
return assign(rows, rowIndexes, otherRowIndexes, name, action2, randomEngine, undefined, assign_range_initGroup);
},
"rotateColumn": (rows, rowIndexes, otherRowIndexes, name, action, randomEngine) => {
const action2 = _.isString(action) ? ({column: action, n: 1}) : _.cloneDeep(action);
return assign(rows, rowIndexes, otherRowIndexes, name, action2, randomEngine, undefined, assign_rotateColumn_initGroup);
},
// "sample": {
//
// }
}
function assign(rows, rowIndexes, otherRowIndexes, name, action, randomEngine, next, initGroup) {
assert(_.isPlainObject(action), "expect an object for assignment")
// Handle order in which to assign values
let draw = "direct";
let reuse = "none";
if (!_.isEmpty(action.order)) {
switch (action.order) {
case "direct": case "direct/none": break;
case "direct/repeat": case "repeat": draw = "direct"; reuse = "repeat"; break;
case "direct/reverse": case "reverse": draw = "direct"; reuse = "reverse"; break;
case "shuffle": draw = "shuffle"; reuse = "none"; break;
case "reshuffle": draw = "shuffle"; reuse = "reshuffle"; break;
case "shuffle/reshuffle": draw = "shuffle"; reuse = "reshuffle"; break;
case "shuffle/repeat": draw = "shuffle"; reuse = "repeat"; break;
case "shuffle/reverse": draw = "shuffle"; reuse = "reverse"; break;
case "sample": draw = "sample"; break;
default: assert(false, "unrecognized 'order' value: "+action.order);
}
}
if (_.isString(action.calculate)) {
next = assign_calculate_next(action.calculate, action)
}
const randomEngine2 = (_.isNumber(action.randomSeed))
? Random.engines.mt19937().seed(action.randomSeed)
: randomEngine;
const value2 = ((_.isArray(action.values)) && draw === "direct" && reuse === "none" && !initGroup)
? action.values
: new Special({action, draw, reuse, randomEngine: randomEngine2}, next, initGroup);
return handleAssignmentWithQueries(rows, rowIndexes, otherRowIndexes, name, action, randomEngine2, value2);
}
function handleAssignmentWithQueries(rows, rowIndexes0, otherRowIndexes, name, action, randomEngine, value0) {
if (DEBUG) {
console.log(`handleAssignmentWithQueries: ${JSON.stringify({name, action, value0})}`);
console.log(` otherRowIndexes: ${JSON.stringify(otherRowIndexes)}`)
console.log(` rowIndexes: ${JSON.stringify(rowIndexes0)}\n ${JSON.stringify(rows)}`)
assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat([rowIndexes0]));
}
const isSpecial = value0 instanceof Special;
const hasGroupOrSame = action.groupBy || action.sameBy;
if (!action.groupBy && !action.sameBy && !action.orderBy) {
if (isSpecial) {
// console.log({name})
value0.initGroup(rows, rowIndexes0);
}
return value0;
}
const rowIndexesGroups = (action.groupBy)
? query_groupBy(rows, rowIndexes0, action.groupBy)
: [_.clone(rowIndexes0)];
// console.log({rowIndexesGroups})
for (let i = 0; i < rowIndexesGroups.length; i++) {
let rowIndexes = _.clone(rowIndexesGroups[i]);
let value = value0;
// If 'orderBy' is set, we should re-order the values (if just returning 'value') or rowIndexes (otherwise)
if (action.orderBy) {
// console.log("A")
if (_.isArray(value)) {
// This is a copy of 'makeComparer', but with more indirection in assignment of row1 and row2
const propertyNames = action.orderBy;
function comparer(i1, i2) {
const row1 = rows[rowIndexes[i1]];
const row2 = rows[rowIndexes[i2]];
const l = (_.isArray(propertyNames)) ? propertyNames : [propertyNames];
for (let j = 0; j < l.length; j++) {
const propertyName = l[j];
const value1 = row1[propertyName];
const value2 = row2[propertyName];
const cmp = naturalSort(value1, value2);
if (cmp !== 0)
return cmp;
}
return 0;
};
const is1 = stableSort(_.range(rowIndexes.length), comparer);
// console.log({is1});
// Allocate a new value array
const value1 = new Array(rowIndexes.length);
// Insert values into the new array according to the new desired order
for (let i = 0; i < rowIndexes.length; i++) {
const j = is1[i];
value1[j] = value[i];
}
// console.log({is1, rowIndexes, rows, value, value1});
value = value1;
}
//if (hasGroupOrSame)
else {
const rowIndexes2 = query_orderBy(rows, rowIndexes, action.orderBy);
// console.log({orderBy: action.orderBy, rowIndexes, rowIndexes2})
// console.log({rowIndexes})
rowIndexes = rowIndexes2;
}
}
// const rowIndexes2 = _.clone(rowIndexes);
// console.log({rowIndexes, rowIndexesGroups})
const otherRowIndexes2 = otherRowIndexes.concat([rowIndexes0]).concat(rowIndexesGroups);
if (DEBUG) {
assertNoDuplicates(otherRowIndexes2);
}
// console.log({otherRowIndexes, rowIndexes, rowIndexesGroups, otherRowIndexes2})
if (action.sameBy) {
assignSameBy(rows, rowIndexes, otherRowIndexes2, name, action, randomEngine, value);
}
else {
if (isSpecial) {
// console.log({rows, rowIndexes2})
value.initGroup(rows, rowIndexes);
}
// console.log({rows, rowIndexes2, otherRowIndexes2, name, value})
expandRowsByNamedValue(rows, rowIndexes, otherRowIndexes2, name, value, randomEngine);
}
}
return undefined;
}
function assignSameBy(rows, rowIndexes, otherRowIndexes, name, action, randomEngine, value) {
if (DEBUG) {
console.log(`assignSameBy: ${JSON.stringify({name, action, value})}`);
console.log(` rowIndexes: ${JSON.stringify(rowIndexes)}\n ${JSON.stringify(rows)}`)
// assertNoDuplicates(otherRowIndexes);
assertNoDuplicates(otherRowIndexes.concat([rowIndexes]));
}
const isArray = _.isArray(value);
const isObject = _.isPlainObject(value);
const isSpecial = value instanceof Special;
const rowIndexesSame = query_groupBy(rows, rowIndexes, action.sameBy);
// console.log({rowIndexesSame})
/*
for (let i = 0; i < rowIndexesSame.length; i++) {
const rowIndexes2 = rowIndexesSame[i];
const rowIndex = rowIndexes2[0];
const rows2 = rowIndexes2.map(i => rows[i]);
rows.splice(rowIndex, 1, ..rows2);
for (let j = 0; j < rowIndexesSame.length;)
}
*/
if (isSpecial) {
const rowIndexesFirst = rowIndexesSame.map(l => l[0]);
value.initGroup(rows, rowIndexesFirst);
}
const keys = (isObject) ? _.keys(value) : 0;
const table2 = _.zip.apply(_, table2);
for (let i = 0; i < rowIndexesSame.length; i++) {
const rowIndexes3 = rowIndexesSame[i];
// if (isSpecial) {
// value.nextIndex = i;
// }
const value2
= (isArray) ? value[i]
: (isObject) ? value[keys[i]]
: (isSpecial) ? value.next(rows, [rowIndexes3[0]])[1]
: value;
// console.log({i, rowIndexes3, value2, value})
for (let i = 0; i < rowIndexes3.length; i++) {
const rowIndex = rowIndexes3[i];
expandRowsByNamedValue(rows, [rowIndex], otherRowIndexes, name, value2, randomEngine);
}
}
}
function assign_allocatePlates_initGroup(rows, rowIndexes) {
if (DEBUG) {
console.log(`assign_allocatePlates_initGroup: ${rows}, ${rowIndexes}`)
}
const action = this.action;
if (_.isUndefined(this.plateIndex))
this.plateIndex = 0;
if (_.isUndefined(this.wellsUsed))
this.wellsUsed = 0;
// If the wells on the plate should be segmented by some grouping
if (action.groupBy) {
assert(rowIndexes.length <= action.wellsPerPlate, `too many positions in group for plate to accomodate: ${rowIndexes.length} rows, ${action.wellsPerPlate} wells per plate`);
// If they fit on the current plate:
if (this.wellsUsed + rowIndexes.length <= action.wellsPerPlate) {
this.wellsUsed += rowIndexes.length;
}
// Otherwise, skip to next plate
else {
this.plateIndex++;
assert(this.plateIndex < action.plates.length, `require more plates than the ${action.plates.length} supplied: ${action.plates.join(", ")}`);
this.wellsUsed = rowIndexes.length;
}
// TODO: allow for rotating plates for each group rather than assigning each plate until its full
// console.log()
// console.log({this})
this.action.values = _.fill(Array(rowIndexes.length), action.plates[this.plateIndex]);
// console.log({this_action_values: this.action.values});
}
else {
// assert(rowIndexes.length <= action.plates.length * action.wellsPerPlate, `too many row for plates to accomodate: ${rowIndexes.length} rows, ${action.wellsPerPlate} wells per plate, ${action.plates.length} plates`);
// If all the rows can be assigned to the current plate:
if (this.wellsUsed + rowIndexes.length <= action.wellsPerPlate) {
this.wellsUsed += rowIndexes.length;
this.action.values = _.fill(Array(rowIndexes.length), action.plates[this.plateIndex]);
}
// Otherwise, just allocate each plate until its full
else {
this.action.values = Array(rowIndexes.length);
let i = 0;
while (i < rowIndexes.length) {
const n = Math.min(rowIndexes.length - i, action.wellsPerPlate - this.wellsUsed);
if (n == 0) {
this.plateIndex++;
assert(this.plateIndex < action.plates.length, `require more plates than the ${action.plates.length} supplied: ${action.plates.join(", ")}`);
this.wellsUsed = 0;
}
else {
for (let j = 0; j < n; j++) {
this.action.values[i + j] = action.plates[this.plateIndex];
}
this.wellsUsed += n;
}
i += n;
}
}
// TODO: allow for rotating plates for each group rather than assigning each plate until its full
// console.log()
// console.log({this})
// console.log({this_action_values: this.action.values});
}
this.defaultInitGroup(rows, rowIndexes);
}
/**
* Calculate `expr` using variables in `row`, with optional `action` object specifying `units` and/or `decimals`
*/
export function calculate(expr, row, action = {}) {
const scope = _.mapValues(row, x => {
// console.log({x})
try {
const result = math.eval(x);
// If evaluation succeeds, but it was just a unit name, then set value as string instead
if (result.type === "Unit" && result.value === null)
return x;
else {
return result;
}
}
catch (e) {}
return x;
});
assert(!_.isUndefined(expr), "`expression` property must be specified");
// console.log({expr, scope})
// console.log("scope:"+JSON.stringify(scope, null, '\t'))
let value = math.eval(expr, scope);
// console.log({type: value.type, value})
if (_.isString(value) || _.isNumber(value) || _.isBoolean(value)) {
return value;
}
// Get units to use in the end, and the unitless value
const {units0, units, unitless} = (() => {
const result = {
units0: undefined,
units: action.units,
unitless: value
};
// If the result has units:
if (value.type === "Unit") {
result.units0 = value.formatUnits();
if (_.isUndefined(result.units))
result.units = result.units0;
const conversionUnits = (_.isEmpty(result.units)) ? result.units0 : result.units;
// If the units dissappeared, e.g. when dividing 30ul/1ul = 30:
if (_.isEmpty(conversionUnits)) {
// TODO: find a better way to get the unit-less quantity from `value`
// console.log({action})
// console.log({result, conversionUnits});
result.unitless = math.eval(value.format());
}
else {
result.unitless = value.toNumeric(conversionUnits);
}
}
return result;
})();
// console.log(`unitless: ${JSON.stringify(unitless)}`)
// Restrict decimal places
// console.log({unitless})
const unitlessText = (_.isNumber(action.decimals))
? unitless.toFixed(action.decimals)
: _.isNumber(unitless) ? unitless : unitless.toNumber();
// Set units
const valueText = (!_.isEmpty(units))
? unitlessText + " " + units
: unitlessText;
return valueText;
}
function assign_calculate_next(expr, action) {
return function(nestedRows, rowIndex) {
const row0 = nestedRows[rowIndex];
const row = (_.isArray(row0)) ? _.head(_.flattenDeep(row0)) : row0;
// Build the scope for evaluating the math expression from the current data row
const valueText = calculate(expr, row, action);
this.nextIndex++;
return [this.nextIndex, valueText];
}
}
function assign_calculateColumn_next(expr, action) {
return function(nestedRows, rowIndex) {
const row0 = nestedRows[rowIndex];
const row = (_.isArray(row0)) ? _.head(_.flattenDeep(row0)) : row0;
// Build the scope for evaluating the math expression from the current data row
const valueText = _.get(row, expr, expr);
const [r, c] = wellsParser.locationTextToRowCol(valueText);
this.nextIndex++;
return [this.nextIndex, c];
}
}
function assign_calculateRow_next(expr, action) {
return function(nestedRows, rowIndex) {
const row0 = nestedRows[rowIndex];
const row = (_.isArray(row0)) ? _.head(_.flattenDeep(row0)) : row0;
// Build the scope for evaluating the math expression from the current data row
const valueText = _.get(row, expr, expr);
const [r, c] = wellsParser.locationTextToRowCol(valueText);
this.nextIndex++;
return [this.nextIndex, r];
}
}
function assign_calculateWell_next(action) {
return function(nestedRows, rowIndex) {
const row0 = nestedRows[rowIndex];
const row = (_.isArray(row0)) ? _.head(_.flattenDeep(row0)) : row0;
// Build the scope for evaluating the math expression from the current data row
const scope = _.mapValues(row, x => {
// console.log({x})
try {
const result = math.eval(x);
// If evaluation succeeds, but it was just a unit name, then set value as string instead
if (result.type === "Unit" && result.value === null)
return x;
else {
return result;
}
}
catch (e) {}
return x;
});
// console.log("scope:"+JSON.stringify(scope, null, '\t'))
let wellRow = math.eval(action.row, scope).toNumber();
let wellCol = math.eval(action.column, scope).toNumber();
const wellName = locationRowColToText(wellRow, wellCol);
// console.log({wellRow, wellCol, wellName})
this.nextIndex++;
return [this.nextIndex, wellName];
}
}
function assign_range_initGroup(rows, rowIndexes) {
// console.log(`assign_range_initGroup:`, {rows, rowIndexes})
const commonHolder = []; // cache for common values, if needed
const from = getOrCalculateNumber(this.action, "from", rowIndexes.length, rows, rowIndexes, commonHolder);
const till = getOrCalculateNumber(this.action, "till", rowIndexes.length, rows, rowIndexes, commonHolder);
const end = till + 1;
let values;
if (_.isNumber(this.action.count)) {
const diff = till - from;
values = _.range(this.action.count).map(i => {
const d = diff * i / (this.action.count - 1);
return from + d;
});
}
else {
values = _.range(from, end, this.action.step);
}
if (values) {
if (_.isNumber(this.action.decimals)) {
values = values.map(n => Number(n.toFixed(this.action.decimals)));
}
if (_.isString(this.action.units)) {
values = values.map(n => `${n} ${this.action.units}`);
}
}
this.action.values = values;
// console.log({this_action_values: this.action.values});
this.defaultInitGroup(rows, rowIndexes);
}
function assign_rotateColumn_initGroup(rows, rowIndexes) {
const l = rowIndexes.map(i => rows[i][this.action.column]);
if (this.action.n > 0) {
for (let i = 0; i < this.action.n; i++) {
const x = l.pop();
l.unshift(x);
}
}
else {
for (let i = 0; i < -this.action.n; i++) {
const x = l.shift();
l.push(x);
}
}
this.action.values = l;
this.defaultInitGroup(rows, rowIndexes);
}
/*
function assign_range_next(nestedRows, rowIndex) {
const commonHolder = []; // cache for common values, if needed
const from = getOrCalculateNumber(this.action, "from", rowIndexes.length, nestedRows, [rowIndex], commonHolder);
const till = getOrCalculateNumber(this.action, "till", rowIndexes.length, nestedRows, [rowIndex], commonHolder);
const end = till + 1;
let values;
if (_.isNumber(this.action.count)) {
const diff = till - from;
values = _.range(this.action.count).map(i => {
const d = diff * i / (this.action.count - 1);
return from + d;
});
}
else {
values = _.range(from, end, this.action.step);
}
if (values) {
if (_.isNumber(this.action.decimals)) {
values = values.map(n => Number(n.toFixed(this.action.decimals)));
}
if (_.isString(this.action.units)) {
values = values.map(n => `${n} ${this.action.units}`);
}
}
console.log({values});
this.nextIndex++;
return [this.nextIndex, values];
}
*/
function getOrCalculateNumber(action, propertyName, dflt, nestedRows, rowIndexes, commonHolder) {
const value = _.get(action, propertyName);
if (_.isUndefined(value)) {
return dflt;
}
else if (_.isNumber(value)) {
return value;
}
else if (_.isString(value)) {
const options = {};
const next = assign_calculate_next(value, options);
const fakethis = {nextIndex: 0};
if (commonHolder.length === 0) {
// console.log({nestedRows, rowIndexes})
commonHolder.push(commonHolder.push(getCommonValuesNested(nestedRows, rowIndexes)));
}
const common = commonHolder[0];
// console.log({common})
const [dummyIndex, result] = next.bind(fakethis)([common], [0]);
return result;
}
}
/*
const assign_range_next = (expr, action) => function(nestedRows, rowIndex) {
console.log("assign_range_next: "+JSON.stringify(this));
const n = this.action.from + this.nextIndex * this.action.step;
assert(!_.isNumber(this.action.till) || n < this.action.till, "range could not fill rows");
this.nextIndex++;
return [this.nextIndex, n];
}*/
export function query_groupBy(rows, rowIndexes, groupBy) {
const groupKeys = (_.isArray(groupBy)) ? groupBy : [groupBy];
// console.log({groupBy, groupKeys, rowIndexes, rows});
return _.values(_.groupBy(rowIndexes, rowIndex => _.map(groupKeys, key => rows[rowIndex][key])));
}
/**
* Return an array of rowIndexes which are ordered by the `orderBy` criteria.
* @param {array} rows - a flat array of row objects
* @param {array} rowIndexes - array of row indexes to consider
* @param {string|array} orderBy - the column(s) to order by
* @return {array} a sorted ordering of rowIndexes
*/
export function query_orderBy(rows, rowIndexes, orderBy) {
// console.log({rows, rowLen: rows.length, rowIndexes, orderBy})
// console.log(rowIndexes.map(i => _.values(_.pick(rows[i], orderBy))))
return stableSort(rowIndexes, makeComparer(rows, orderBy));
}
function makeComparer(rows, propertyNames) {
return function(i1, i2) {
const row1 = rows[i1];
const row2 = rows[i2];
if (!row1 || !row2) {
console.log({i1, i2, row1, row2})
}
const l = (_.isArray(propertyNames)) ? propertyNames : [propertyNames];
for (let j = 0; j < l.length; j++) {
const propertyName = l[j];
const value1 = row1[propertyName];
const value2 = row2[propertyName];
const cmp = naturalSort(value1, value2);
if (cmp !== 0)
return cmp;
}
return 0;
};
}
export function query(table, q, SCOPE = undefined) {
let table2 = _.clone(table);
if (q.where) {
// console.log({where: q.where})
table2 = filterOnWhere(table2, q.where, SCOPE);
}
if (q.shuffle) {
table2 = _.shuffle(table);
}
if (q.orderBy) {
table2 = _.orderBy(table2, q.orderBy);
}
if (q.distinctBy) {
const groupKeys = (_.isArray(q.distinctBy)) ? q.distinctBy : [q.distinctBy];
const groups = _.map(_.groupBy(table2, row => _.map(groupKeys, key => row[key])), _.identity);
//console.log({groupsLength: groups.length})
table2 = _.flatMap(groups, group => {
const first = group[0];
// Find the properties that are the same for all items in the group
const uniqueKeys = [];
_.forEach(first, (value, key) => {
const isUnique = _.every(group, row => _.isEqual(row[key], value));
if (isUnique) {
uniqueKeys.push(key);
}
});
return _.pick(first, uniqueKeys);
});
}
else if (q.unique) {
table2 = _.uniqWith(table2, _.isEqual);
}
if (q.groupBy) {
const groupKeys = (_.isArray(q.groupBy)) ? q.groupBy : [q.groupBy];
table2 = _.map(_.groupBy(table2, row => _.map(groupKeys, key => row[key])), _.identity);
}
else {
table2 = [table2];
}
if (q.select) {
table2 = table2.map(rows => rows.map(row => _.pick(row, q.select)));
}
if (q.transpose) {
table2 = _.zip.apply(_, table2);
}
return table2;
}
const compareFunctions = {
"eq": _.isEqual,
"gt": _.gt,
"gte": _.gte,
"lt": _.lt,
"lte": _.lte,
"ne": (a, b) => !_.isEqual(a, b),
"in": _.includes
};
function filterOnWhere(table, where, SCOPE = undefined) {
let table2 = table;
if (_.isPlainObject(where)) {
_.forEach(where, (value, key) => {
if (_.isPlainObject(value)) {
_.forEach(value, (value2, op) => {
// Get compare function
assert(compareFunctions.hasOwnProperty(op), `unrecognized operator: ${op} in ${JSON.stringify(value)}`);
const fn = compareFunctions[op];
// console.log({op, fn})
table2 = filterOnWhereOnce(table2, key, value2, fn);
});
}
else {
table2 = filterOnWhereOnce(table2, key, value, _.isEqual);
}
});
}
else if (_.isString(where)) {
// console.log({where})
table2 = _.filter(table, row => {
const scope1 = _.mapValues(row, x => {
// console.log({x})
try {
const result = math.eval(x);
// If evaluation succeeds, but it was just a unit name, then set value as string instead
if (result.type === "Unit" && result.value === null)
return x;
else {
return result;
}
}
catch (e) {}
return x;
});
const scope = _.defaults({}, scope1, SCOPE);
// console.log({where, row, scope})
try {
const result = math.eval(where, scope);
// console.log({result});
return result;
} catch (e) {
console.log("WARNING: "+e);
console.log("where: "+JSON.stringify(where));
// console.log({scope})
process.exit()
}
return false;
});
}
return table2;
}
/**
* Sub-function that filters table on a single criterion.
* @param {array} table - table to filter
* @param {string} key - key of column in table
* @param {any} x - value to compare to
* @param {Function} fn - comparison function
* @return {array} filtered table
*/
function filterOnWhereOnce(table, key, x, fn) {
// console.log("filterOnWhereOnce: "); console.log({key, x, fn})
// If x is an array, do an array comparison
if (_.isArray(x)) {
return _.filter(table, (row, i) => fn(row[key], x[i]));
}
// If we need to
else if (_.isString(x)) {
if (_.startsWith(x, "\"")) {
const text = x.substr(1, x.length - 2);
return table.filter(row => fn(row[key], text));
}
else {
const key2 = x.substr(1);
return table.filter(row => fn(row[key], row[key2]));
}
}
else {
return table.filter(row => fn(row[key], x));
}
}
/**
* Check whether the same underlying array shows up more than once in otherRowIndexes.
* This should never be the case, because if we modify one, the "other" will also be modified.
*/
function assertNoDuplicates(otherRowIndexes) {
for (let i = 0; i < otherRowIndexes.length - 1; i++) {
for (let j = i + 1; j < otherRowIndexes.length; j++) {
assert(otherRowIndexes[i] != otherRowIndexes[j], `same underlying array appears in 'otherRowIndexs' at both ${i} and ${j}: ${JSON.stringify(otherRowIndexes)}`)
}
}
}