Skip to main content

linreg_core/wasm/
csv.rs

1//! CSV parsing for WASM
2//!
3//! Provides CSV parsing functionality that can be called from JavaScript.
4
5#![cfg(feature = "wasm")]
6
7use std::collections::HashSet;
8use serde::Serialize;
9use wasm_bindgen::prelude::*;
10
11use super::domain::check_domain;
12use crate::error::{error_json, error_to_json};
13
14#[derive(Serialize)]
15struct ParsedCsv {
16    headers: Vec<String>,
17    data: Vec<serde_json::Map<String, serde_json::Value>>,
18    numeric_columns: Vec<String>,
19}
20
21/// Parses CSV data and returns it as a JSON string.
22///
23/// Parses the CSV content and identifies numeric columns. Returns a JSON object
24/// with headers, data rows, and a list of numeric column names.
25///
26/// # Arguments
27///
28/// * `content` - CSV content as a string
29///
30/// # Returns
31///
32/// JSON string with structure:
33/// ```json
34/// {
35///   "headers": ["col1", "col2", ...],
36///   "data": [{"col1": 1.0, "col2": "text"}, ...],
37///   "numeric_columns": ["col1", ...]
38/// }
39/// ```
40///
41/// # Errors
42///
43/// Returns a JSON error object if parsing fails or domain check fails.
44#[wasm_bindgen]
45pub fn parse_csv(content: &str) -> String {
46    if let Err(e) = check_domain() {
47        return error_to_json(&e);
48    }
49
50    let mut reader = csv::ReaderBuilder::new()
51        .has_headers(true)
52        .flexible(true)
53        .from_reader(content.as_bytes());
54
55    // Get headers
56    let headers: Vec<String> = match reader.headers() {
57        Ok(h) => h.iter().map(|s| s.to_string()).collect(),
58        Err(e) => return error_json(&format!("Failed to read headers: {}", e)),
59    };
60
61    let mut data = Vec::new();
62    let mut numeric_col_set = HashSet::new();
63
64    for result in reader.records() {
65        let record = match result {
66            Ok(r) => r,
67            Err(e) => return error_json(&format!("Failed to parse CSV record: {}", e)),
68        };
69
70        if record.len() != headers.len() {
71            continue;
72        }
73
74        let mut row_map = serde_json::Map::new();
75
76        for (i, field) in record.iter().enumerate() {
77            if i >= headers.len() {
78                continue;
79            }
80
81            let header = &headers[i];
82            let val_trimmed = field.trim();
83
84            // Try to parse as f64
85            if let Ok(num) = val_trimmed.parse::<f64>() {
86                if num.is_finite() {
87                    row_map.insert(
88                        header.clone(),
89                        serde_json::Value::Number(serde_json::Number::from_f64(num).unwrap()),
90                    );
91                    numeric_col_set.insert(header.clone());
92                    continue;
93                }
94            }
95
96            // Fallback to string
97            row_map.insert(
98                header.clone(),
99                serde_json::Value::String(val_trimmed.to_string()),
100            );
101        }
102        data.push(row_map);
103    }
104
105    let mut numeric_columns: Vec<String> = numeric_col_set.into_iter().collect();
106    numeric_columns.sort();
107
108    let output = ParsedCsv {
109        headers,
110        data,
111        numeric_columns,
112    };
113
114    serde_json::to_string(&output).unwrap_or_else(|_| error_json("Failed to serialize CSV output"))
115}