UNPKG

henrts-r-integration

Version:

Simple portable library used to interact with pre-installed R compiler by running commands or scripts(files)

487 lines (405 loc) 13.8 kB
const fs = require("fs"); const pt = require("path"); const events = require('events'); 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 * @param {String[]} args an array of parameters to be passed to the command * @returns {{string, string}} the command execution result */ executeShellCommand = (command, args) => { let stdout; let stderr; let exec_res = child_process.spawnSync(command, args, { encoding : 'utf8' }); stdout = exec_res.stdout; stderr = exec_res.stderr; return { stdout, stderr }; }; /** * Execute a command in the OS shell (used to execute R command) asynchronously * * @param {string} command the command to execute * @param {String[]} args an array of parameters to be passed to the command * @returns {Promise<string>} the command execution result */ executeShellCommandAsync = (command, args, emitter) => { return new Promise((resolve, reject) => { var stdout = ""; var stderr = ""; let process = child_process.spawn(command, args, { encoding : 'utf8' }); process.stdout.on('data', (data) => { data=data.toString(); if(data.indexOf("progress-") !== -1 && emitter) { const [_, progress, pct] = data.replaceAll('"', '').trim().split("-") emitter.emit("progress", progress + (pct ? '-' + pct : '')) } stdout+=data; }); process.stderr.on('data', (data) => { data=data.toString(); if(data.indexOf("progress-") !== -1) { console.log(data) } stderr+=data; }); process.on('exit', (code) => { if (code !== 0) { reject(command + " " + args.join(" ") + " exited with code " + code + " and error: " + stderr); } else { resolve(stdout); } }); }); }; /** * Check 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 // directory 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 args = ["-e", command]; var commandResult = executeShellCommand(RscriptBinaryPath, args); console.log("Command Result", commandResult.stdout) 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 - asynchronously * * @param {string} command the single line R command * @param {string} RBinariesLocation optional parameter to specify an * alternative location for the Rscript binary * @returns {Promise<string>} an array containing all the results from the command * execution output, null if there was an error */ executeRCommandAsync = (command, RBinariesLocation, emitter) => { return new Promise(function(resolve, reject) { let RscriptBinaryPath = isRscriptInstallaed(RBinariesLocation); if (RscriptBinaryPath) { var args = ["-e", command]; executeShellCommandAsync(RscriptBinaryPath, args, emitter).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 commandResult = executeShellCommand(RscriptBinaryPath, [fileLocation]); 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 that R could read them * * @param {Object} params an array of parameters * @returns {string} the parameters formatted in the proper way */ 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 {EventEmitter} emitter an event emitter to track function progress * @param {string} RBinariesLocation optional parameter to specify an * alternative location for the Rscript binary * @returns {Promise<string>} the execution output of the function */ callMethodAsync = (fileLocation, methodName, params, emitter, 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, emitter).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 }