r-integration
Version:
Simple portable library used to interact with pre-installed R compiler by running commands or scripts(files)
444 lines (368 loc) • 12.7 kB
JavaScript
const fs = require("fs");
const pt = require("path");
var child_process = require("child_process");
/**
* get the current Operating System name
*
* @returns {string} the operating system short name
* - "win" -> for Windows based Systems
* - "lin" -> for GNU/Linux based Systems
* - "mac" -> for MacOS based Systems
*/
getCurrentOs = () => {
var processPlatform = process.platform;
var currentOs;
if (processPlatform === "win32") {
currentOs = "win";
} else if (processPlatform === "linux" || processPlatform === "openbsd" || processPlatform === "freebsd") {
currentOs = "lin";
} else {
currentOs = "mac"
}
return currentOs;
};
/**
* execute a command in the OS shell (used to execute R command)
*
* @param {string} command the command to execute
* @returns {{string, string}} the command execution result
*/
executeShellCommand = (command) => {
let stdout;
let stderr;
try {
stdout = child_process.execSync(command, {
stdio: "pipe"
}).toString();
} catch (error) {
stderr = error;
}
return {
stdout,
stderr
};
};
/**
* execute a command in the OS shell (used to execute R command) asynchronously
*
* @param {string} command the command to execute
* @returns {{string, string}} the command execution result
*/
executeShellCommandAsync = (command) => {
return new Promise((resolve, reject) => {
child_process.exec(command, (error, stdout, stderr) => {
if (error) {
reject(stderr);
}
resolve(stdout);
});
});
};
/**
* checks if Rscript(R) is installed od the system and returns
* the path where the binary is installed
*
* @param {string} path alternative path to use as binaries directory
* @returns {string} the path where the Rscript binary is installed, 0 otherwise
*/
isRscriptInstallaed = (path) => {
var installationDir = 0;
switch (getCurrentOs()) {
case "win":
if (!path) {
path = pt.join("C:\\Program Files\\R");
}
if (fs.existsSync(path)) {
// Rscript is installed, let's find the path (version problems)
let dirContent = fs.readdirSync(path);
if (dirContent.length != 0) {
let lastVersion = dirContent[dirContent.length - 1];
installationDir = pt.join(path, lastVersion, "bin", "Rscript.exe");
}
}
break;
case "mac":
case "lin":
if (!path) {
// the command "which" is used to find the Rscript installation dir
path = executeShellCommand("which Rscript").stdout;
if (path) {
// Rscript is installed
installationDir = path.replace("\n", "");
}
} else {
path = pt.join(path, "Rscript");
if (fs.existsSync(path)) {
//file Rscript exists
installationDir = path;
}
}
break;
default:
break;
}
return installationDir;
};
/**
* Execute in R a specific one line command
*
* @param {string} command the single line R command
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {String[]} an array containing all the results from the command execution output, 0 if there was an error
*/
executeRCommand = (command, RBinariesLocation) => {
let RscriptBinaryPath = isRscriptInstallaed(RBinariesLocation);
let output = 0;
if (RscriptBinaryPath) {
var commandToExecute = `"${RscriptBinaryPath}" -e "${command}"`;
var commandResult = executeShellCommand(commandToExecute);
if (commandResult.stdout) {
output = commandResult.stdout;
output = filterMultiline(output);
} else {
throw Error(`[R: compile error] ${commandResult.stderr}`);
}
} else {
throw Error("R not found, maybe not installed.\nSee www.r-project.org");
}
return output;
};
/**
* Execute in R a specific one line command - async
*
* @param {string} command the single line R command
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {String[]} an array containing all the results from the command execution output, null if there was an error
*/
executeRCommandAsync = (command, RBinariesLocation) => {
return new Promise(function(resolve, reject) {
let RscriptBinaryPath = isRscriptInstallaed(RBinariesLocation);
let output = 0;
if (RscriptBinaryPath) {
var commandToExecute = `"${RscriptBinaryPath}" -e "${command}"`;
executeShellCommandAsync(commandToExecute).then((output) => {
output = filterMultiline(output);
resolve(output);
}).catch((stderr) => {
reject(`[R: compile error] ${stderr}`);
});
} else {
reject("R not found, maybe not installed.\nSee www.r-project.org");
}
});
};
/**
* execute in R all the commands in the file specified by the parameter fileLocation
* NOTE: the function reads only variables printed to stdout by the cat() or print() function.
* It is recommended to use the print() function insted of the cat() to avoid line break problem.
* If you use the cat() function remember to add the newline character "\n" at the end of each cat:
* for example cat(" ... \n")
*
* @param {string} fileLocation where the file to execute is stored
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {String[]} an array containing all the results from the command execution output, 0 if there was an error
*/
executeRScript = (fileLocation, RBinariesLocation) => {
let RscriptBinaryPath = isRscriptInstallaed(RBinariesLocation);
let output = 0;
if (!fs.existsSync(fileLocation)) {
//file doesn't exist
throw Error(`ERROR: the file "${fileLocation}" doesn't exist`);
}
if (RscriptBinaryPath) {
var commandToExecute = `"${RscriptBinaryPath}" "${fileLocation}"`;
var commandResult = executeShellCommand(commandToExecute);
if (commandResult.stdout) {
output = commandResult.stdout;
output = filterMultiline(output);
} else {
throw Error(`[R: compile error] ${commandResult.stderr}`);
}
} else {
throw Error("R not found, maybe not installed.\nSee www.r-project.org");
}
return output;
};
/**
* Formats the parameters so R could read them
*/
convertParamsArray = (params) => {
var methodSyntax = ``;
if (Array.isArray(params)) {
methodSyntax += "c(";
for (let i = 0; i < params.length; i++) {
methodSyntax += convertParamsArray(params[i]);
}
methodSyntax = methodSyntax.slice(0, -1);
methodSyntax += "),";
} else if (typeof params == "string") {
methodSyntax += `'${params}',`;
} else if (params == undefined) {
methodSyntax += `NA,`;
} else {
methodSyntax += `${params},`;
}
return methodSyntax;
};
/**
* calls a R function located in an external script with parameters and returns the result
*
* @param {string} fileLocation where the file containing the function is stored
* @param {string} methodName the name of the method to execute
* @param {Object} params an object containing a binding between parameter names and value to pass to the function or an array
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {string} the execution output of the function, 0 in case of error
*/
callMethod = (fileLocation, methodName, params, RBinariesLocation) => {
let output = 0;
if (!methodName || !fileLocation || !params) {
throw Error("ERROR: please provide valid parameters - methodName, fileLocation and params cannot be null");
}
var methodSyntax = `${methodName}(`;
// check if params is an array of parameters or an object
if (Array.isArray(params)) {
methodSyntax += convertParamsArray(params);
} else {
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
methodSyntax += `${key}=${convertParamsArray(value)}`;
} else if (typeof value == "string") {
methodSyntax += `${key}='${value}',`; // string preserve quotes
} else if (value == undefined) {
methodSyntax += `${key}=NA,`;
} else {
methodSyntax += `${key}=${value},`;
}
}
}
var methodSyntax = methodSyntax.slice(0, -1);
methodSyntax += ")";
output = executeRCommand(`source('${fileLocation}') ; print(${methodSyntax})`, RBinariesLocation);
return output;
};
/**
* calls a R function with parameters and returns the result - async
*
* @param {string} fileLocation where the file containing the function is stored
* @param {string} methodName the name of the method to execute
* @param {String []} params a list of parameters to pass to the function
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {string} the execution output of the function
*/
callMethodAsync = (fileLocation, methodName, params, RBinariesLocation) => {
return new Promise(function(resolve, reject) {
if (!methodName || !fileLocation || !params) {
throw Error("ERROR: please provide valid parameters - methodName, fileLocation and params cannot be null");
}
var methodSyntax = `${methodName}(`;
// check if params is an array of parameters or an object
if (Array.isArray(params)) {
methodSyntax += convertParamsArray(params);
} else {
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
methodSyntax += `${key}=${convertParamsArray(value)}`;
} else if (typeof value == "string") {
methodSyntax += `${key}='${value}',`; // string preserve quotes
} else if (value == undefined) {
methodSyntax += `${key}=NA,`;
} else {
methodSyntax += `${key}=${value},`;
}
}
}
var methodSyntax = methodSyntax.slice(0, -1);
methodSyntax += ")";
executeRCommandAsync(`source('${fileLocation}') ; print(${methodSyntax})`, RBinariesLocation).then((res) => {
resolve(res);
}).catch((error) => {
reject(`${error}`);
});
})
};
/**
* calls a standard R function with parameters and returns the result
*
* @param {string} methodName the name of the method to execute
* @param {Object} params an object containing a binding between parameter names and value to pass to the function or an array
* @param {string} RBinariesLocation optional parameter to specify an alternative location for the Rscript binary
* @returns {string} the execution output of the function, 0 in case of error
*/
callStandardMethod = (methodName, params, RBinariesLocation) => {
let output = 0;
if (!methodName || !params) {
throw Error("ERROR: please provide valid parameters - methodName and params cannot be null");
}
var methodSyntax = `${methodName}(`;
// check if params is an array of parameters or an object
if (Array.isArray(params)) {
methodSyntax += convertParamsArray(params);
} else {
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
methodSyntax += `${key}=${convertParamsArray(value)}`;
} else if (typeof value == "string") {
methodSyntax += `${key}='${value}',`; // string preserve quotes
} else if (value == undefined) {
methodSyntax += `${key}=NA,`;
} else {
methodSyntax += `${key}=${value},`;
}
}
}
var methodSyntax = methodSyntax.slice(0, -1);
methodSyntax += ")";
output = executeRCommand(`print(${methodSyntax})`, RBinariesLocation);
return output;
};
/**
* filters the multiline output from the executeRcommand and executeRScript functions
* using regular expressions
*
* @param {string} commandResult the multiline result of RScript execution
* @returns {String[]} an array containing all the results
*/
filterMultiline = (commandResult) => {
let data;
// remove last newline to avoid empty results
// NOTE: windows newline is composed by \r\n, GNU/Linux and Mac OS newline is \n
var currentOS = getCurrentOs();
commandResult = commandResult.replace(/\[\d+\] /g, "");
if (currentOS == "win") {
commandResult = commandResult.replace(/\t*\s*[\r\n]*$/g, "");
commandResult = commandResult.replace(/[\s\t]+/g, "\r\n");
} else {
commandResult = commandResult.replace(/\t*\s*\n*$/g, "");
commandResult = commandResult.replace(/[\s\t]+/g, "\n");
}
// check if data is JSON parsable
try {
data = [JSON.parse(commandResult)];
} catch (e) {
// the result is not json parsable -> split
if (currentOS == "win") {
data = commandResult.split(/[\r\n]+/)
} else {
data = commandResult.split(/[\n]+/)
}
// find undefined or NaN and remove quotes
for (let i = 0; i < data.length; i++) {
if (data[i] == "NA") {
data[i] = undefined;
} else if (data[i] == "NaN") {
data[i] = NaN;
} else {
data[i] = data[i].replace(/\"/g, "")
}
}
}
return data;
};
module.exports = {
executeRCommand,
executeRCommandAsync,
executeRScript,
callMethod,
callMethodAsync,
callStandardMethod
}