use std::collections::HashMap;
use std::path::Path;
use serde_json::{Map, Value};
use crate::Result;
pub fn rows_to_csv(rows: &[Vec<String>]) -> String {
let mut out = String::new();
for row in rows {
let line: Vec<String> = row.iter().map(|f| csv_field(f)).collect();
out.push_str(&line.join(","));
out.push_str("\r\n");
}
out
}
pub fn records_to_csv(records: &[HashMap<String, String>], headers: &[String]) -> String {
let mut rows: Vec<Vec<String>> = Vec::with_capacity(records.len() + 1);
rows.push(headers.to_vec());
for rec in records {
rows.push(
headers
.iter()
.map(|h| rec.get(h).cloned().unwrap_or_default())
.collect(),
);
}
rows_to_csv(&rows)
}
pub fn records_to_json(records: &[HashMap<String, String>]) -> String {
let arr: Vec<Value> = records
.iter()
.map(|r| {
let mut m = Map::new();
for (k, v) in r {
m.insert(k.clone(), Value::String(v.clone()));
}
Value::Object(m)
})
.collect();
serde_json::to_string_pretty(&Value::Array(arr)).unwrap_or_else(|_| "[]".to_string())
}
pub async fn write_csv(path: impl AsRef<Path>, rows: &[Vec<String>]) -> Result<()> {
write_text(path, &rows_to_csv(rows)).await
}
pub async fn write_json(path: impl AsRef<Path>, records: &[HashMap<String, String>]) -> Result<()> {
write_text(path, &records_to_json(records)).await
}
async fn write_text(path: impl AsRef<Path>, content: &str) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(path, content).await?;
Ok(())
}
fn csv_field(f: &str) -> String {
if f.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", f.replace('"', "\"\""))
} else {
f.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csv_escaping() {
let rows = vec![
vec!["a".into(), "b,c".into(), "d\"e".into()],
vec!["x\ny".into(), "z".into(), "".into()],
];
let csv = rows_to_csv(&rows);
assert_eq!(csv, "a,\"b,c\",\"d\"\"e\"\r\n\"x\ny\",z,\r\n");
}
#[test]
fn records_csv_orders_by_headers() {
let mut r0 = HashMap::new();
r0.insert("name".to_string(), "苹果".to_string());
r0.insert("price".to_string(), "3".to_string());
let headers = vec!["name".to_string(), "price".to_string()];
let csv = records_to_csv(&[r0], &headers);
assert_eq!(csv, "name,price\r\n苹果,3\r\n");
}
#[test]
fn records_json_shape() {
let mut r0 = HashMap::new();
r0.insert("k".to_string(), "v".to_string());
let j = records_to_json(&[r0]);
let parsed: Value = serde_json::from_str(&j).unwrap();
assert_eq!(parsed[0]["k"], "v");
}
}