Source: EvowareUtils.js

/**
 * Roboliq: Automation for liquid-handling robots
 * @copyright 2017, ETH Zurich, Ellis Whitehead
 * @license GPL-3.0
 */

/**
 * A collection of helper utilities for our Evoware compiler.
 * @module
 */

import _ from 'lodash';
import fs from 'fs';
import iconv from 'iconv-lite';

/**
 * Encode an integer as an ASCII character.
 * Evoware uses this to generate a string representing a list of wells or sites.
 * @param  {number} n - integer to encode as a character
 * @return {string} a single-character string that represents the number
 */
export function encode(n) {
	return String.fromCharCode("0".charCodeAt(0) + n);
}

/**
 * Decode a character to an integer.
 */
export function decode(c) {
	return c.charCodeAt(0) - "0".charCodeAt(0);
}

/**
 * Convert the number to hex.  Number should be between 0 and 15.
 * @param  {integer} n - number between 0 and 15
 * @return {char}
 */
export function hex(n) {
	return n.toString(16).toUpperCase()[0];
}

/**
 * Takes an encoding of indexes on a 2D surface (as found in the file Carrier.cfg)
 * and
 * @param  {string} encoded - an encoded list of indexes
 * @return {array} tuple of [rows on surface, columns on surface, selected indexes on surface]
 */
export function parseEncodedIndexes(encoded) {
	// HACK: for some reason, there is this strange sequence "�" that shows
	// up in some places.  It appears to simply indicate 7 bits, e.g. "0"+127, e.g. '¯'
	encoded = encoded.replace(/�/g, String.fromCharCode(48+127));
	const col_n = decode(encoded.charAt(1));
	const row_n = decode(encoded.charAt(3));
	const s = encoded.substring(4);
	//console.log({col_n, row_n, s})
	const indexes = _.flatMap(s, (c, c_i) => {
		const n = decode(c);
		//const bit_l = (0 to 7).flatMap(bit_i => if ((n & (1 << bit_i)) > 0) Some(bit_i) else None)
		const bit_l = _.filter(_.times(7, bit_i => ((n & (1 << bit_i)) > 0) ? bit_i : undefined), x => !_.isUndefined(x));
		//console.log({c, c_i, n, bit_l});
		return bit_l.map(bit_i => c_i * 7 + bit_i);
	});
	return [col_n, row_n, indexes];
}


/**
 * Split an evoware carrier line into its components.
 * The first component is an integer that identifies the type of line.
 * The remaining components are returned as a list of string.
 *
 * @param  {string} line - a text line from Evoware's carrier file
 * @return {array} Returns a pair [kind, items], where kind is an integer
 *   identifying the type of line, and items is a string array of the remaining
 *   components of the line.
 */
export function splitSemicolons(line) {
	const l = line.split(";");
	const kind = parseInt(l[0]);
	return [kind, _.tail(l)];
}

/**
 * A class to handle Evoware's semicolon-based file format.
 * @class module:evoware/EvowareUtils.EvowareSemicolonFile
 * @param  {string} filename - path to semicolon file
 * @param  {number} skip - number of lines to initially skip at the top of the file
 */
export class EvowareSemicolonFile {
	constructor(filename, skip) {
		const raw = fs.readFileSync(filename);
		const filedata = iconv.decode(raw, "ISO-8859-1");
		this.lines = filedata.split("\n");
		//console.log("lines:\n"+lines)
		//console.log(lines.length);
		this.lineIndex = skip;
	}

	/**
	 * Get the next line
	 * @return {string} next line in semicolon file
	 */
	next() {
		if (this.lineIndex >= this.lines.length)
			return undefined;
		const line = this.lines[this.lineIndex];
		this.lineIndex++;
		return line;
	}

	/**
	 * Get the next line in the file and split it on semicolons.
	 * @return {array} array of strings resulting from splitting the line at semicolons.
	 */
	nextSplit() {
		const line = this.next();
		if (_.isUndefined(line))
			return undefined;
		const result = splitSemicolons(line);
		return result;
	}

	/**
	 * Whether there are any more lines in the file.
	 * @return {boolean} true if there are more lines to read
	 */
	hasNext() {
		return (this.lineIndex < this.lines.length);
	}

	/**
	 * Return a line that is `skip` lines ahead of the last line read.
	 * @param  {number} skip - number of lines to skip over
	 * @return {string} line in file
	 */
	peekAhead(skip) {
		const i = this.lineIndex + skip;
		if (i >= this.lines.length)
			return undefined;
		const line = this.lines[i];
		return line;
	}

	/**
	 * Skip `n` lines ahead
	 * @param  {number} n - number of lines to skip
	 */
	skip(n) {
		this.lineIndex += n;
	}

	/**
	 * Get the next `n` lines from the file.
	 * @param  {number} n - number of lines to read.
	 * @return {array} array of strings read.
	 */
	take(n) {
		const l = this.lines.slice(this.lineIndex, this.lineIndex + n);
		this.lineIndex += n;
		return l;
	}
}