use std::collections::HashMap;
use std::fmt::Write;
use std::fs;
use std::process::Command;
pub struct RIntegration {
#[allow(dead_code)]
r_home: Option<String>,
#[allow(dead_code)]
library_paths: Vec<String>,
loaded_packages: Vec<String>,
workspace_variables: HashMap<String, RValue>,
}
#[derive(Debug, Clone)]
pub enum RValue {
Null,
Logical(bool),
Integer(i32),
Double(f64),
Character(String),
IntegerVector(Vec<i32>),
DoubleVector(Vec<f64>),
CharacterVector(Vec<String>),
LogicalVector(Vec<bool>),
Matrix {
data: Vec<f64>,
nrows: usize,
ncols: usize,
},
DataFrame {
columns: HashMap<String, RValue>,
nrows: usize,
},
List(HashMap<String, RValue>),
}
#[derive(Debug, Clone)]
pub struct RDataFrame {
pub columns: HashMap<String, RValue>,
pub nrows: usize,
pub column_names: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RMatrix {
pub data: Vec<f64>,
pub nrows: usize,
pub ncols: usize,
pub row_names: Option<Vec<String>>,
pub col_names: Option<Vec<String>>,
}
pub struct RScriptBuilder {
script_lines: Vec<String>,
variables: HashMap<String, RValue>,
packages: Vec<String>,
}
pub struct RPackageManager {
installed_packages: Vec<String>,
#[allow(dead_code)]
available_packages: HashMap<String, String>, cran_mirror: String,
}
pub struct RStatisticalFunctions;
impl RIntegration {
pub fn new() -> Result<Self, RError> {
let r_home = Self::detect_r_installation()?;
Ok(Self {
r_home: Some(r_home),
library_paths: vec![],
loaded_packages: ["base", "stats", "utils", "graphics", "grDevices"]
.iter()
.map(|s| s.to_string())
.collect(),
workspace_variables: HashMap::new(),
})
}
fn detect_r_installation() -> Result<String, RError> {
let output = Command::new("R")
.args(["--slave", "--vanilla", "-e", "cat(R.home())"])
.output()
.map_err(|_| RError::RNotFound)?;
if output.status.success() {
String::from_utf8(output.stdout).map_err(|_| RError::InvalidOutput)
} else {
Err(RError::RNotFound)
}
}
pub fn execute_script(&mut self, script: &str) -> Result<String, RError> {
let temp_path = std::env::temp_dir().join("sklears_r_script.R");
let temp_file = temp_path.to_string_lossy().into_owned();
fs::write(&temp_file, script).map_err(|e| RError::IoError(e.to_string()))?;
let output = Command::new("Rscript")
.arg(&temp_file)
.output()
.map_err(|e| RError::ExecutionError(e.to_string()))?;
let _ = fs::remove_file(&temp_file);
if output.status.success() {
String::from_utf8(output.stdout).map_err(|_| RError::InvalidOutput)
} else {
let error_msg =
String::from_utf8(output.stderr).unwrap_or_else(|_| "Unknown R error".to_string());
Err(RError::RScriptError(error_msg))
}
}
pub fn load_package(&mut self, package_name: &str) -> Result<(), RError> {
let script = format!("library({package_name})");
self.execute_script(&script)?;
self.loaded_packages.push(package_name.to_string());
Ok(())
}
pub fn array_to_r_vector(&self, data: &[f64]) -> RValue {
RValue::DoubleVector(data.to_vec())
}
pub fn matrix_to_r_matrix(&self, data: &[f64], nrows: usize, ncols: usize) -> RValue {
RValue::Matrix {
data: data.to_vec(),
nrows,
ncols,
}
}
pub fn r_vector_to_array(&self, r_value: &RValue) -> Result<Vec<f64>, RError> {
match r_value {
RValue::DoubleVector(vec) => Ok(vec.clone()),
RValue::IntegerVector(vec) => Ok(vec.iter().map(|&x| x as f64).collect()),
_ => Err(RError::TypeMismatch),
}
}
pub fn create_dataframe(&self, columns: HashMap<String, RValue>) -> Result<RDataFrame, RError> {
let mut nrows = 0;
let mut column_names = Vec::new();
for (name, value) in &columns {
let length = match value {
RValue::IntegerVector(v) => v.len(),
RValue::DoubleVector(v) => v.len(),
RValue::CharacterVector(v) => v.len(),
RValue::LogicalVector(v) => v.len(),
_ => return Err(RError::InvalidDataFrame),
};
if nrows == 0 {
nrows = length;
} else if nrows != length {
return Err(RError::InvalidDataFrame);
}
column_names.push(name.clone());
}
Ok(RDataFrame {
columns,
nrows,
column_names,
})
}
pub fn call_r_function(
&mut self,
function_name: &str,
args: &[RValue],
) -> Result<RValue, RError> {
let mut script = String::new();
for (i, arg) in args.iter().enumerate() {
let var_name = format!("arg{i}");
let r_code = self.r_value_to_r_code(arg)?;
writeln!(script, "{var_name} <- {r_code}")
.map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
let arg_names: Vec<String> = (0..args.len()).map(|i| format!("arg{i}")).collect();
writeln!(
script,
"result <- {}({})",
function_name,
arg_names.join(", ")
)
.map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
writeln!(script, "cat(paste(result, collapse=','))")
.map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
let output = self.execute_script(&script)?;
self.parse_r_output(&output)
}
fn r_value_to_r_code(&self, value: &RValue) -> Result<String, RError> {
match value {
RValue::Null => Ok("NULL".to_string()),
RValue::Logical(b) => Ok(if *b { "TRUE" } else { "FALSE" }.to_string()),
RValue::Integer(i) => Ok(format!("{i}L")),
RValue::Double(d) => Ok(d.to_string()),
RValue::Character(s) => Ok(format!("\"{}\"", s.replace("\"", "\\\""))),
RValue::IntegerVector(vec) => Ok(format!(
"c({})",
vec.iter()
.map(|x| format!("{x}L"))
.collect::<Vec<_>>()
.join(", ")
)),
RValue::DoubleVector(vec) => Ok(format!(
"c({})",
vec.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
)),
RValue::CharacterVector(vec) => Ok(format!(
"c({})",
vec.iter()
.map(|s| format!("\"{}\"", s.replace("\"", "\\\"")))
.collect::<Vec<_>>()
.join(", ")
)),
RValue::LogicalVector(vec) => Ok(format!(
"c({})",
vec.iter()
.map(|b| if *b { "TRUE" } else { "FALSE" })
.collect::<Vec<_>>()
.join(", ")
)),
RValue::Matrix { data, nrows, ncols } => Ok(format!(
"matrix(c({}), nrow={}, ncol={})",
data.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", "),
nrows,
ncols
)),
_ => Err(RError::UnsupportedType),
}
}
fn parse_r_output(&self, output: &str) -> Result<RValue, RError> {
let trimmed = output.trim();
if trimmed.is_empty() {
return Ok(RValue::Null);
}
if trimmed.contains(',') {
let values: Result<Vec<f64>, _> = trimmed
.split(',')
.map(|s| s.trim().parse::<f64>())
.collect();
if let Ok(vec) = values {
return Ok(RValue::DoubleVector(vec));
}
}
if let Ok(value) = trimmed.parse::<f64>() {
return Ok(RValue::Double(value));
}
if let Ok(value) = trimmed.parse::<i32>() {
return Ok(RValue::Integer(value));
}
if trimmed == "TRUE" {
return Ok(RValue::Logical(true));
}
if trimmed == "FALSE" {
return Ok(RValue::Logical(false));
}
Ok(RValue::Character(trimmed.to_string()))
}
pub fn get_loaded_packages(&self) -> &[String] {
&self.loaded_packages
}
pub fn is_package_available(&mut self, package_name: &str) -> Result<bool, RError> {
let script = format!("cat(is.element('{package_name}', installed.packages()[,1]))");
let output = self.execute_script(&script)?;
Ok(output.trim() == "TRUE")
}
pub fn install_package(&mut self, package_name: &str) -> Result<(), RError> {
let script =
format!("install.packages('{package_name}', repos='https://cran.r-project.org')");
self.execute_script(&script)?;
Ok(())
}
pub fn save_variable(&mut self, name: &str, value: RValue) {
self.workspace_variables.insert(name.to_string(), value);
}
pub fn get_variable(&self, name: &str) -> Option<&RValue> {
self.workspace_variables.get(name)
}
pub fn clear_workspace(&mut self) {
self.workspace_variables.clear();
}
}
impl RScriptBuilder {
pub fn new() -> Self {
Self {
script_lines: Vec::new(),
variables: HashMap::new(),
packages: Vec::new(),
}
}
pub fn require_package(&mut self, package: &str) -> &mut Self {
self.packages.push(package.to_string());
self
}
pub fn assign_variable(&mut self, name: &str, value: RValue) -> &mut Self {
self.variables.insert(name.to_string(), value);
self
}
pub fn add_line(&mut self, line: &str) -> &mut Self {
self.script_lines.push(line.to_string());
self
}
pub fn add_comment(&mut self, comment: &str) -> &mut Self {
self.script_lines.push(format!("# {comment}"));
self
}
pub fn build(&self) -> Result<String, RError> {
let mut script = String::new();
for package in &self.packages {
writeln!(script, "library({package})")
.map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
if !self.packages.is_empty() {
writeln!(script).map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
for (name, value) in &self.variables {
let r_code = self.r_value_to_r_code(value)?;
writeln!(script, "{name} <- {r_code}")
.map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
if !self.variables.is_empty() {
writeln!(script).map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
for line in &self.script_lines {
writeln!(script, "{line}").map_err(|e| RError::ScriptGenerationError(e.to_string()))?;
}
Ok(script)
}
fn r_value_to_r_code(&self, value: &RValue) -> Result<String, RError> {
match value {
RValue::Null => Ok("NULL".to_string()),
RValue::Logical(b) => Ok(if *b { "TRUE" } else { "FALSE" }.to_string()),
RValue::Integer(i) => Ok(format!("{i}L")),
RValue::Double(d) => Ok(d.to_string()),
RValue::Character(s) => Ok(format!("\"{}\"", s.replace("\"", "\\\""))),
RValue::DoubleVector(vec) => Ok(format!(
"c({})",
vec.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
)),
_ => Err(RError::UnsupportedType),
}
}
}
impl RPackageManager {
pub fn new() -> Self {
Self {
installed_packages: Vec::new(),
available_packages: HashMap::new(),
cran_mirror: "https://cran.r-project.org".to_string(),
}
}
pub fn refresh(&mut self) -> Result<(), RError> {
let script = "cat(paste(installed.packages()[,1], collapse=','))";
let output = Command::new("Rscript")
.args(["-e", script])
.output()
.map_err(|e| RError::ExecutionError(e.to_string()))?;
if output.status.success() {
let packages_str =
String::from_utf8(output.stdout).map_err(|_| RError::InvalidOutput)?;
self.installed_packages = packages_str
.trim()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
Ok(())
}
pub fn is_installed(&self, package_name: &str) -> bool {
self.installed_packages.contains(&package_name.to_string())
}
pub fn install(&mut self, package_name: &str) -> Result<(), RError> {
let script = format!(
"install.packages('{}', repos='{}')",
package_name, self.cran_mirror
);
let output = Command::new("Rscript")
.args(["-e", &script])
.output()
.map_err(|e| RError::ExecutionError(e.to_string()))?;
if output.status.success() {
self.installed_packages.push(package_name.to_string());
Ok(())
} else {
let error_msg = String::from_utf8(output.stderr)
.unwrap_or_else(|_| "Package installation failed".to_string());
Err(RError::PackageInstallationError(error_msg))
}
}
pub fn remove(&mut self, package_name: &str) -> Result<(), RError> {
let script = format!("remove.packages('{package_name}')");
let output = Command::new("Rscript")
.args(["-e", &script])
.output()
.map_err(|e| RError::ExecutionError(e.to_string()))?;
if output.status.success() {
self.installed_packages.retain(|p| p != package_name);
Ok(())
} else {
let error_msg = String::from_utf8(output.stderr)
.unwrap_or_else(|_| "Package removal failed".to_string());
Err(RError::PackageRemovalError(error_msg))
}
}
pub fn get_installed_packages(&self) -> &[String] {
&self.installed_packages
}
pub fn set_cran_mirror(&mut self, mirror_url: &str) {
self.cran_mirror = mirror_url.to_string();
}
}
impl RStatisticalFunctions {
pub fn mean(r: &mut RIntegration, data: &[f64]) -> Result<f64, RError> {
let r_vector = r.array_to_r_vector(data);
let result = r.call_r_function("mean", &[r_vector])?;
match result {
RValue::Double(mean_val) => Ok(mean_val),
_ => Err(RError::TypeMismatch),
}
}
pub fn sd(r: &mut RIntegration, data: &[f64]) -> Result<f64, RError> {
let r_vector = r.array_to_r_vector(data);
let result = r.call_r_function("sd", &[r_vector])?;
match result {
RValue::Double(sd_val) => Ok(sd_val),
_ => Err(RError::TypeMismatch),
}
}
pub fn t_test(r: &mut RIntegration, x: &[f64], y: &[f64]) -> Result<RValue, RError> {
r.load_package("stats")?;
let x_vector = r.array_to_r_vector(x);
let y_vector = r.array_to_r_vector(y);
r.call_r_function("t.test", &[x_vector, y_vector])
}
pub fn lm(r: &mut RIntegration, x: &[f64], y: &[f64]) -> Result<RValue, RError> {
r.load_package("stats")?;
let mut columns = HashMap::new();
columns.insert("x".to_string(), r.array_to_r_vector(x));
columns.insert("y".to_string(), r.array_to_r_vector(y));
let script = "
data <- data.frame(x=arg0, y=arg1)
model <- lm(y ~ x, data=data)
coefficients <- coef(model)
cat(paste(coefficients, collapse=','))
";
let mut script_with_args = String::new();
writeln!(
script_with_args,
"arg0 <- {}",
r.r_value_to_r_code(&columns["x"])?
)
.expect("operation should succeed");
writeln!(
script_with_args,
"arg1 <- {}",
r.r_value_to_r_code(&columns["y"])?
)
.expect("operation should succeed");
script_with_args.push_str(script);
let output = r.execute_script(&script_with_args)?;
r.parse_r_output(&output)
}
pub fn cor(r: &mut RIntegration, x: &[f64], y: &[f64]) -> Result<f64, RError> {
let x_vector = r.array_to_r_vector(x);
let y_vector = r.array_to_r_vector(y);
let result = r.call_r_function("cor", &[x_vector, y_vector])?;
match result {
RValue::Double(cor_val) => Ok(cor_val),
_ => Err(RError::TypeMismatch),
}
}
pub fn anova(r: &mut RIntegration, groups: &[Vec<f64>]) -> Result<RValue, RError> {
r.load_package("stats")?;
let mut script = String::new();
let mut all_values = Vec::new();
let mut group_labels = Vec::new();
for (i, group) in groups.iter().enumerate() {
for &value in group {
all_values.push(value);
let group_num = i + 1;
group_labels.push(format!("Group{group_num}"));
}
}
writeln!(
script,
"values <- c({})",
all_values
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
)
.expect("operation should succeed");
writeln!(
script,
"groups <- factor(c({}))",
group_labels
.iter()
.map(|s| format!("\"{s}\""))
.collect::<Vec<_>>()
.join(", ")
)
.expect("operation should succeed");
writeln!(script, "result <- aov(values ~ groups)").expect("operation should succeed");
writeln!(script, "summary_result <- summary(result)").expect("operation should succeed");
writeln!(script, "cat('ANOVA completed')").expect("operation should succeed");
let output = r.execute_script(&script)?;
Ok(RValue::Character(output))
}
pub fn plot(r: &mut RIntegration, x: &[f64], y: &[f64], filename: &str) -> Result<(), RError> {
let script = format!(
"
x <- c({})
y <- c({})
png('{}')
plot(x, y, main='Scatter Plot', xlab='X', ylab='Y')
dev.off()
",
x.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", "),
y.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", "),
filename
);
r.execute_script(&script)?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum RError {
#[error("R installation not found")]
RNotFound,
#[error("Invalid R output")]
InvalidOutput,
#[error("I/O error: {0}")]
IoError(String),
#[error("R script execution error: {0}")]
ExecutionError(String),
#[error("R script error: {0}")]
RScriptError(String),
#[error("Type mismatch")]
TypeMismatch,
#[error("Invalid data frame")]
InvalidDataFrame,
#[error("Unsupported type")]
UnsupportedType,
#[error("Script generation error: {0}")]
ScriptGenerationError(String),
#[error("Package installation error: {0}")]
PackageInstallationError(String),
#[error("Package removal error: {0}")]
PackageRemovalError(String),
}
impl Default for RIntegration {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
r_home: None,
library_paths: Vec::new(),
loaded_packages: Vec::new(),
workspace_variables: HashMap::new(),
})
}
}
impl Default for RScriptBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for RPackageManager {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_r_script_builder() {
let mut builder = RScriptBuilder::new();
builder
.require_package("stats")
.assign_variable("x", RValue::DoubleVector(vec![1.0, 2.0, 3.0]))
.add_comment("Calculate mean")
.add_line("mean_x <- mean(x)")
.add_line("cat(mean_x)");
let script = builder.build().expect("operation should succeed");
assert!(script.contains("library(stats)"));
assert!(script.contains("x <- c(1, 2, 3)"));
assert!(script.contains("# Calculate mean"));
assert!(script.contains("mean_x <- mean(x)"));
}
#[test]
fn test_r_value_conversions() {
let r = RIntegration::default();
let data = vec![1.0, 2.0, 3.0, 4.0];
let r_vector = r.array_to_r_vector(&data);
match r_vector {
RValue::DoubleVector(ref vec) => assert_eq!(vec, &data),
_ => panic!("Expected DoubleVector"),
}
let array = r
.r_vector_to_array(&r_vector)
.expect("operation should succeed");
assert_eq!(array, data);
}
#[test]
fn test_dataframe_creation() {
let r = RIntegration::default();
let mut columns = HashMap::new();
columns.insert("x".to_string(), RValue::DoubleVector(vec![1.0, 2.0, 3.0]));
columns.insert("y".to_string(), RValue::DoubleVector(vec![4.0, 5.0, 6.0]));
let df = r
.create_dataframe(columns)
.expect("operation should succeed");
assert_eq!(df.nrows, 3);
assert_eq!(df.column_names.len(), 2);
}
#[test]
fn test_r_code_generation() {
let r = RIntegration::default();
assert_eq!(
r.r_value_to_r_code(&RValue::Double(std::f64::consts::PI))
.expect("operation should succeed"),
format!("{}", std::f64::consts::PI)
);
assert_eq!(
r.r_value_to_r_code(&RValue::Integer(42))
.expect("operation should succeed"),
"42L"
);
assert_eq!(
r.r_value_to_r_code(&RValue::Logical(true))
.expect("operation should succeed"),
"TRUE"
);
assert_eq!(
r.r_value_to_r_code(&RValue::Character("test".to_string()))
.expect("operation should succeed"),
"\"test\""
);
let vec_result = r
.r_value_to_r_code(&RValue::DoubleVector(vec![1.0, 2.0, 3.0]))
.expect("operation should succeed");
assert_eq!(vec_result, "c(1, 2, 3)");
let matrix_result = r
.r_value_to_r_code(&RValue::Matrix {
data: vec![1.0, 2.0, 3.0, 4.0],
nrows: 2,
ncols: 2,
})
.expect("operation should succeed");
assert_eq!(matrix_result, "matrix(c(1, 2, 3, 4), nrow=2, ncol=2)");
}
#[test]
fn test_package_manager() {
let mut manager = RPackageManager::new();
assert_eq!(manager.get_installed_packages().len(), 0);
assert!(!manager.is_installed("ggplot2"));
manager.installed_packages.push("base".to_string());
manager.installed_packages.push("stats".to_string());
assert!(manager.is_installed("base"));
assert!(manager.is_installed("stats"));
assert!(!manager.is_installed("ggplot2"));
}
#[test]
fn test_workspace_variables() {
let mut r = RIntegration::default();
let value = RValue::Double(42.0);
r.save_variable("test_var", value.clone());
let retrieved = r.get_variable("test_var");
assert!(retrieved.is_some());
match retrieved.expect("operation should succeed") {
RValue::Double(val) => assert_eq!(*val, 42.0),
_ => panic!("Expected Double value"),
}
r.clear_workspace();
assert!(r.get_variable("test_var").is_none());
}
#[test]
fn test_output_parsing() {
let r = RIntegration::default();
let result = r.parse_r_output("42.5").expect("operation should succeed");
match result {
RValue::Double(val) => assert_eq!(val, 42.5),
_ => panic!("Expected Double"),
}
let result = r
.parse_r_output("1,2,3,4")
.expect("operation should succeed");
match result {
RValue::DoubleVector(vec) => assert_eq!(vec, vec![1.0, 2.0, 3.0, 4.0]),
_ => panic!("Expected DoubleVector"),
}
let result = r.parse_r_output("TRUE").expect("operation should succeed");
match result {
RValue::Logical(val) => assert!(val),
_ => panic!("Expected Logical"),
}
let result = r
.parse_r_output("test string")
.expect("operation should succeed");
match result {
RValue::Character(val) => assert_eq!(val, "test string"),
_ => panic!("Expected Character"),
}
}
#[test]
fn test_matrix_operations() {
let r = RIntegration::default();
let matrix = r.matrix_to_r_matrix(&[1.0, 2.0, 3.0, 4.0], 2, 2);
match matrix {
RValue::Matrix { data, nrows, ncols } => {
assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(nrows, 2);
assert_eq!(ncols, 2);
}
_ => panic!("Expected Matrix"),
}
}
}