#![allow(clippy::boxed_local)]
use crate::conditions::Condition;
use crate::dataframe::DataFrame;
use crate::series::Series;
use crate::types::Value;
use indexmap::IndexMap;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WasmDataFrame {
df: DataFrame,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl WasmDataFrame {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmDataFrame {
let rust_columns: IndexMap<String, Series> = IndexMap::new();
let df = DataFrame::new(rust_columns);
WasmDataFrame { df }
}
#[wasm_bindgen(js_name = fromObject, static_method_of = WasmDataFrame)]
pub fn from_object(data: &js_sys::Object) -> Result<WasmDataFrame, JsValue> {
let mut rust_columns: IndexMap<String, Series> = IndexMap::new();
let entries = js_sys::Object::entries(data);
for entry in entries.iter() {
let arr = js_sys::Array::from(&entry);
let name = arr
.get(0)
.as_string()
.ok_or("Column name must be a string")?;
let values_js = arr.get(1);
let values_array = js_sys::Array::from(&values_js);
let mut i32_values = Vec::new();
let mut f64_values = Vec::new();
let mut string_values = Vec::new();
let mut detected_type: Option<&str> = None;
for i in 0..values_array.length() {
let val = values_array.get(i);
if val.is_null() || val.is_undefined() {
i32_values.push(None);
f64_values.push(None);
string_values.push(None);
} else if let Some(num) = val.as_f64() {
if detected_type.is_none() {
detected_type =
Some(if num.fract() == 0.0 && num.abs() <= i32::MAX as f64 {
"i32"
} else {
"f64"
});
}
i32_values.push(Some(num as i32));
f64_values.push(Some(num));
string_values.push(Some(num.to_string()));
} else if let Some(s) = val.as_string() {
if detected_type.is_none() {
detected_type = Some("string");
}
i32_values.push(None);
f64_values.push(None);
string_values.push(Some(s));
}
}
let series = match detected_type.unwrap_or("string") {
"i32" => Series::new_i32(&name, i32_values),
"f64" => Series::new_f64(&name, f64_values),
_ => Series::new_string(&name, string_values),
};
rust_columns.insert(name, series);
}
let df = DataFrame::new(rust_columns);
Ok(WasmDataFrame { df })
}
#[wasm_bindgen(js_name = rowCount)]
pub fn row_count(&self) -> usize {
self.df.row_count()
}
#[wasm_bindgen(js_name = columnCount)]
pub fn column_count(&self) -> usize {
self.df.column_count()
}
#[wasm_bindgen(js_name = columnNames)]
pub fn column_names(&self) -> Box<[JsValue]> {
self.df
.column_names()
.iter()
.map(|name| JsValue::from_str(name))
.collect::<Vec<_>>()
.into_boxed_slice()
}
#[wasm_bindgen(js_name = filterGt)]
pub fn filter_gt(&self, column: &str, value: JsValue) -> Result<WasmDataFrame, JsValue> {
let condition = if let Some(num) = value.as_f64() {
if num.fract() == 0.0 && num.abs() <= i32::MAX as f64 {
Condition::Gt(column.to_string(), Value::I32(num as i32))
} else {
Condition::Gt(column.to_string(), Value::F64(num))
}
} else if let Some(s) = value.as_string() {
Condition::Gt(column.to_string(), Value::String(s))
} else {
return Err(JsValue::from_str("Unsupported value type"));
};
let filtered = self
.df
.filter(&condition)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmDataFrame { df: filtered })
}
#[wasm_bindgen(js_name = groupBy)]
pub fn group_by(&self, columns: Box<[JsValue]>) -> Result<WasmGroupedDataFrame, JsValue> {
let column_names: Result<Vec<String>, JsValue> = columns
.iter()
.map(|v| {
v.as_string()
.ok_or_else(|| JsValue::from_str("Column name must be a string"))
})
.collect();
Ok(WasmGroupedDataFrame {
dataframe: self.df.clone(),
group_columns: column_names?,
})
}
#[wasm_bindgen(js_name = addSeries)]
pub fn add_series(&mut self, name: &str, series: &WasmSeries) -> Result<(), JsValue> {
let mut new_columns = self.df.columns.clone();
let mut s = series.inner.clone();
s.set_name(name);
new_columns.insert(name.to_string(), s);
self.df = DataFrame::new(new_columns);
Ok(())
}
#[wasm_bindgen(js_name = getColumn)]
pub fn get_column(&self, name: &str) -> Option<WasmSeries> {
self.df
.get_column(name)
.map(|s| WasmSeries { inner: s.clone() })
}
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> String {
let mut json = String::from("{");
for (i, name) in self.df.column_names().iter().enumerate() {
if i > 0 {
json.push(',');
}
json.push_str(&format!("\"{}\":[", name));
if let Some(series) = self.df.get_column(name) {
for j in 0..series.len() {
if j > 0 {
json.push(',');
}
match series.get_value(j) {
Some(Value::I32(v)) => json.push_str(&v.to_string()),
Some(Value::F64(v)) => json.push_str(&v.to_string()),
Some(Value::String(v)) => json.push_str(&format!("\"{}\"", v)),
Some(Value::Bool(v)) => json.push_str(&v.to_string()),
_ => json.push_str("null"),
}
}
}
json.push(']');
}
json.push('}');
json
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WasmSeries {
inner: Series,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl WasmSeries {
#[wasm_bindgen(constructor)]
pub fn new(name: &str, values: Box<[JsValue]>) -> Result<WasmSeries, JsValue> {
if values.is_empty() {
return Err(JsValue::from_str("Cannot create series from empty array"));
}
let first_valid = values.iter().find(|v| !v.is_null() && !v.is_undefined());
let series = match first_valid {
Some(val) if val.as_f64().is_some() => {
let data: Vec<Option<f64>> = values
.iter()
.map(|v| {
if v.is_null() || v.is_undefined() {
None
} else {
v.as_f64()
}
})
.collect();
Series::new_f64(name, data)
}
Some(val) if val.as_string().is_some() => {
let data: Vec<Option<String>> = values
.iter()
.map(|v| {
if v.is_null() || v.is_undefined() {
None
} else {
v.as_string()
}
})
.collect();
Series::new_string(name, data)
}
_ => return Err(JsValue::from_str("Unsupported data type")),
};
Ok(WasmSeries { inner: series })
}
#[wasm_bindgen(js_name = length)]
pub fn len(&self) -> usize {
self.inner.len()
}
#[wasm_bindgen(js_name = name)]
pub fn name(&self) -> String {
self.inner.name().to_string()
}
#[wasm_bindgen(js_name = sum)]
pub fn sum(&self) -> Option<f64> {
match self.inner.sum() {
Ok(Value::F64(v)) => Some(v),
Ok(Value::I32(v)) => Some(v as f64),
_ => None,
}
}
#[wasm_bindgen(js_name = add)]
pub fn add(&self, other: &WasmSeries) -> Result<WasmSeries, JsValue> {
match self.inner.add(&other.inner) {
Ok(result) => Ok(WasmSeries { inner: result }),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
#[wasm_bindgen(js_name = multiply)]
pub fn multiply(&self, other: &WasmSeries) -> Result<WasmSeries, JsValue> {
match self.inner.multiply(&other.inner) {
Ok(result) => Ok(WasmSeries { inner: result }),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WasmGroupedDataFrame {
dataframe: DataFrame,
group_columns: Vec<String>,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl WasmGroupedDataFrame {
#[wasm_bindgen(js_name = sum)]
pub fn sum(&self) -> Result<WasmDataFrame, JsValue> {
let column_names = self.dataframe.column_names();
let mut agg_specs: Vec<(&str, &str)> = Vec::new();
for col in &column_names {
if !self.group_columns.contains(col) {
if let Some(series) = self.dataframe.get_column(col) {
if series.is_numeric() {
agg_specs.push((col.as_str(), "sum"));
}
}
}
}
if agg_specs.is_empty() {
return Err(JsValue::from_str(
"No numeric columns found for aggregation",
));
}
let grouped = self
.dataframe
.group_by(self.group_columns.clone())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let result = grouped
.agg(agg_specs)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmDataFrame { df: result })
}
#[wasm_bindgen(js_name = mean)]
pub fn mean(&self) -> Result<WasmDataFrame, JsValue> {
let column_names = self.dataframe.column_names();
let mut agg_specs: Vec<(&str, &str)> = Vec::new();
for col in &column_names {
if !self.group_columns.contains(col) {
if let Some(series) = self.dataframe.get_column(col) {
if series.is_numeric() {
agg_specs.push((col.as_str(), "mean"));
}
}
}
}
if agg_specs.is_empty() {
return Err(JsValue::from_str(
"No numeric columns found for aggregation",
));
}
let grouped = self
.dataframe
.group_by(self.group_columns.clone())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let result = grouped
.agg(agg_specs)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmDataFrame { df: result })
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = simdAddF64)]
pub fn simd_add_f64(a: Box<[f64]>, b: Box<[f64]>) -> Result<Box<[f64]>, JsValue> {
if a.len() != b.len() {
return Err(JsValue::from_str("Arrays must have the same length"));
}
let len = a.len();
let mut result = vec![0.0; len];
for i in 0..len {
result[i] = a[i] + b[i];
}
Ok(result.into_boxed_slice())
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = simdSumF64)]
pub fn simd_sum_f64(data: Box<[f64]>) -> f64 {
data.iter().copied().sum::<f64>()
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WasmValue {}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub enum WasmDataType {
I32 = 0,
F64 = 1,
Bool = 2,
String = 3,
DateTime = 4,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WasmExpr {}