/**
* Module for parsing strings that represent wells and labware.
* @module
*/
var _ = require('lodash');
var assert = require('assert');
var random = require('random-js');
var expect = require('../expectCore.js');
var misc = require('../misc.js');
var wellsParser0 = require('./wellsParser0.js');
/**
* Take a well identifier (e.g. A01) and returns an integer array
* representing the row and column of that well.
* @param {string} location - a well identifier starting with a capital letter and followed by a number (e.g. A01)
* @return {array} an integer array of `[row, col]`. The values are 1-based (i.e. row 1 is the first row)
* @static
*/
function locationTextToRowCol(location) {
var row = location.charCodeAt(0) - "A".charCodeAt(0) + 1;
var col = parseInt(location.substr(1));
return [row, col];
}
/**
* Converts a row and column index to a string.
* For example, `[1, 1] => A01`, and `[8, 12] => H12`.
* @param {number} row - row of well
* @param {number} col - column of well
* @return {string} string representation of location of well on labware
* @static
*/
function locationRowColToText(row, col) {
var colText = col.toString();
if (colText.length == 1) colText = "0"+colText;
return String.fromCharCode("A".charCodeAt(0) + row - 1) + colText;
}
/**
* Parses a text which should represent one or more labwares and wells.
* If the `objects` parameter is passed, this function will return an array of the individual wells;
* otherwise it will return the raw parser results.
*
* @param {string} text - text to parse
* @param {object} objects - map of protocol objects, in order to find labwares and number of rows and columns on labware.
* @param {object} [config] - optional object that contains properties for 'rows' and 'columns', in case we want to expand something like 'A1 down C3' without having specified a plate
* @return {array} If the `objects` parameter is passed, this function will return an array of the individual wells; otherwise it will return the raw parser results.
* @static
*/
function parse(text, objects, config) {
assert(_.isString(text), "wellsParser.parse() expected a string, received: "+text)
var result;
try {
result = wellsParser0.parse(text);
} catch (e) {
expect.rethrow(e);
}
if (!objects)
return result;
return processParserResult(result, objects, text, config);
}
/**
* Take the raw parser results and return an array of location names,
* one entry for each well.
* @param {array} result - raw parser results.
* @param {object} objects - map of protocol objects, in order to find labwares and number of rows and columns on labware.
* @param {string} text - the original text that was parsed; this is merely used for error output.
* @param {object} [config] - optional object that contains properties for 'rows' and 'columns', in case we want to expand something like 'A1 down C3' without having specified a plate
* @return {array} array of names for each plate + well (e.g. `plate1(C04)`)
*/
function processParserResult(result, objects, text, config = {}) {
var commandHelper = require('../commandHelper.js');
//console.log("text", text)
//console.log("result", result)
//console.log("result:\n"+JSON.stringify(result, null, ' '));
var ll = _.map(result, function(clause) {
const data = {objects};
// Get source or labware objects
//console.log({commandHelper})
const parsed = commandHelper.parseParams(clause, data, {
properties: {
source: {},
labware: {},
//subject: 'Any?'
}
});
// console.log({clause, parsed});
if (parsed.value.source) {
// If this was a source, return its wells
if (parsed.value.source.type === 'Source' || parsed.value.source.type === 'Liquid')
return [parsed.value.source.wells];
// Handle the case of when a variable is used and here we can substitute in its value
else if (_.isString(parsed.value.source) && parsed.value.source !== clause.source)
return parse(parsed.value.source, objects);
// Else
else {
// console.log({clause, parsed})
expect.throw({}, "unrecognized source specifier: "+JSON.stringify(parsed.value.source));
}
}
else if (parsed.value.labware || (config.rows && config.columns)) {
// Get number of rows and columns
let rows, columns;
let labwareName;
if (parsed.value.labware) {
const labware = parsed.value.labware;
labwareName = parsed.objectName.labware;
var modelName = labware.model;
assert(modelName, "`"+labwareName+".model` missing");
var model = misc.getObjectsValue(modelName, objects);
assert(model.rows, "`"+modelName+".rows` missing");
assert(model.columns, "`"+modelName+".columns` missing");
rows = model.rows;
columns = model.columns;
}
else {
rows = config.rows;
columns = config.columns;
}
var l = [];
if (clause.subject === 'all') {
for (var col = 1; col <= columns; col++) {
for (var row = 1; row <= rows; row++) {
l.push([row, col]);
}
}
}
else {
l.push(locationTextToRowCol(clause.subject));
}
if (clause.phrases) {
_.forEach(clause.phrases, function(phrase) {
switch (phrase[0]) {
case "down":
assert(l.length == 1, "`down` can only be used with a single well");
var rc0 = l[0];
var n = phrase[1];
assert(n >= 1, "`down n` must be positive: "+text);
var row = rc0[0];
var col = rc0[1];
for (var i = 1; i < n; i++) {
row++;
if (row > rows) {
row = 1;
col++;
if (col > columns) {
throw {name: "RangeError", message: "`"+text+"` extends beyond range of labware `"+labwareName+"`"};
}
}
l.push([row, col])
}
break;
case "down-to":
assert(l.length == 1, "`down` can only be used with a single well");
var rc0 = l[0];
var rc1 = locationTextToRowCol(phrase[1]);
assert(rc0[1] < rc1[1] || rc0[0] <= rc1[0], "invalid target for `down`: "+text)
assert(rc0[1] <= rc1[1], "column of `"+phrase[1]+"` must be equal or greater than origin: "+text);
var row = rc0[0];
var col = rc0[1];
while (row !== rc1[0] || col != rc1[1]) {
row++;
if (row > rows) {
row = 1;
col++;
if (col > columns) {
throw {name: "RangeError", message: "`"+text+"` extends beyond range of labware `"+labwareName+"`"};
}
}
l.push([row, col])
}
break;
case "down-block":
assert(l.length == 1, "`block` can only be used with a single well");
var rc0 = l[0];
var rc1 = locationTextToRowCol(phrase[1]);
assert(rc0[0] <= rc1[0], "row of `"+phrase[1]+"` must be equal or greater than origin: "+text);
assert(rc0[1] <= rc1[1], "column of `"+phrase[1]+"` must be equal or greater than origin: "+text);
var row = rc0[0];
var col = rc0[1];
while (row !== rc1[0] || col != rc1[1]) {
row++;
if (row > rc1[0]) {
row = rc0[0];
col++;
}
l.push([row, col])
}
break;
case "right":
assert(l.length == 1, "`right` can only be used with a single well");
var rc0 = l[0];
var n = phrase[1];
assert(n >= 1, "`right n` must be positive: "+text);
var row = rc0[0];
var col = rc0[1];
for (var i = 1; i < n; i++) {
col++;
if (col > columns) {
row++;
col = 1;
if (row > rows) {
throw {name: "RangeError", message: "`"+text+"` extends beyond range of labware `"+labwareName+"`"};
}
}
l.push([row, col])
}
break;
case "right-to":
assert(l.length == 1, "`right` can only be used with a single well");
var rc0 = l[0];
var rc1 = locationTextToRowCol(phrase[1]);
assert(rc0[0] < rc1[0] || rc0[1] <= rc1[1], "invalid target for `right`: "+text)
assert(rc0[0] <= rc1[0], "row of `"+phrase[1]+"` must be equal or greater than origin: "+text);
var row = rc0[0];
var col = rc0[1];
while (row !== rc1[0] || col != rc1[1]) {
col++;
if (col > columns) {
col = 1;
row++;
if (row > rows) {
throw {name: "RangeError", message: "`"+text+"` extends beyond range of labware `"+labwareName+"`"};
}
}
l.push([row, col])
}
break;
case "right-block":
assert(l.length == 1, "`block` can only be used with a single well");
var rc0 = l[0];
var rc1 = locationTextToRowCol(phrase[1]);
assert(rc0[0] <= rc1[0], "row of `"+phrase[1]+"` must be equal or greater than origin: "+text);
assert(rc0[1] <= rc1[1], "column of `"+phrase[1]+"` must be equal or greater than origin: "+text);
var row = rc0[0];
var col = rc0[1];
while (row !== rc1[0] || col != rc1[1]) {
col++;
if (col > rc1[1]) {
col = rc0[1];
row++;
}
l.push([row, col])
}
break;
case "random":
// Initialize randomizing engine
var mt = random.engines.mt19937();
if (phrase.length == 2) {
mt.seed(phrase[1]);
}
else {
mt.autoSeed();
}
// Randomize the list
var rest = _.clone(l);
random.shuffle(mt, rest);
//console.log("rest:", rest);
// Now try to not repeated pick the sames rows or columns
var l2 = [];
var choices = [];
while (rest.length > 0) {
if (choices.length == 0)
choices = _.clone(rest);
//console.log("choices:", JSON.stringify(choices));
// Pick the first choice in list
var rc = _.pullAt(choices, 0)[0];
// Add it to our new list
l2.push(rc);
// Remove it from 'rest'
rest = _.without(rest, rc);
// Remove all items from choices with the same row or column
_.remove(choices, function(rc2) {
return (rc2[0] == rc[0] || rc2[1] == rc[1]);
});
}
l = l2;
break;
case "take":
var n = phrase[1];
assert(n >= 0);
l = _.take(l, n);
break;
case "row-jump":
// Number of rows of space to leave between rows
var n = phrase[1];
expect.truthy(null, n >= 0, "row-jump value must be >= 0");
var cycleLen = n + 1;
var l2 = [];
while (l.length > 0) {
// Get consecutive rc's that are in the same col
var col = l[0][1];
var sameCol = _.takeWhile(l, function(rc) { return rc[1] == col; });
l = _.drop(l, sameCol.length);
//console.dir(sameCol);
while (sameCol.length > 0) {
var row = sameCol[0][0];
var l3 = _.remove(sameCol, function(rc) {
return (((rc[0] - row) % cycleLen) === 0);
});
//console.log(row, l3, sameCol);
l2 = l2.concat(l3);
}
}
l = l2;
break;
default:
assert(false, "unhandled verb: "+phrase[0]);
}
});
}
// Convert the list of row/col back to text
return _.map(l, function(rc) {
var location = locationRowColToText(rc[0], rc[1]);
return (labwareName) ? labwareName+'('+location+')' : location;
});
}
else if (clause.subject) {
assert(_.isEmpty(clause.phrases));
return clause.subject;
}
else {
assert(false);
}
});
//console.log("ll:")
//console.log(ll);
return _.flatten(ll);
}
/**
* Parses a string which should represent a single well, and return the raw
* parser results.
*
* @param {string} text - text to parse
* @return {object} an object representing the raw parser results.
* @static
*/
function parseOne(text) {
assert(_.isString(text), "wellsParser.parseOne() expected a string, received: "+JSON.stringify(text))
try {
return wellsParser0.parse(text, {startRule: 'startOne'});
} catch (e) {
throw e;
//throw Error(e.toString());
}
}
module.exports = {
locationRowColToText,
locationTextToRowCol,
parse,
parseOne,
processParserResult,
};