use std::fs;
use std::io::Write;
use std::path::Path;
use calamine::{Data, Reader, open_workbook_auto};
use serde_json::{Map, Value};
use crate::{Error, Job, Result};
pub fn to_csv(job: &Job) -> Result<()> {
let rows = read_first_sheet(job.input)?;
let mut writer = csv::Writer::from_path(job.output)?;
for row in &rows {
writer.write_record(row)?;
}
writer.flush()?;
Ok(())
}
pub fn to_tsv(job: &Job) -> Result<()> {
let rows = read_first_sheet(job.input)?;
let mut writer = csv::WriterBuilder::new()
.delimiter(b'\t')
.from_path(job.output)?;
for row in &rows {
writer.write_record(row)?;
}
writer.flush()?;
Ok(())
}
pub fn to_json(job: &Job) -> Result<()> {
let rows = read_first_sheet(job.input)?;
let (headers, records) = match rows.split_first() {
Some(split) => split,
None => {
fs::write(job.output, "[]\n")?;
return Ok(());
}
};
let mut file = fs::File::create(job.output)?;
writeln!(file, "[")?;
let mut first = true;
for record in records {
let mut obj = Map::with_capacity(headers.len());
for (h, v) in headers.iter().zip(record.iter()) {
obj.insert(h.clone(), Value::String(v.clone()));
}
if !first {
writeln!(file, ",")?;
}
first = false;
let line = serde_json::to_string(&Value::Object(obj))
.map_err(|e| Error::other(format!("json serialize: {e}")))?;
write!(file, "{line}")?;
}
writeln!(file)?;
writeln!(file, "]")?;
Ok(())
}
fn read_first_sheet(input: &Path) -> Result<Vec<Vec<String>>> {
let mut workbook = open_workbook_auto(input)
.map_err(|e| Error::other(format!("spreadsheet open: {e}")))?;
let sheet_name = workbook
.sheet_names()
.first()
.cloned()
.ok_or_else(|| Error::other("spreadsheet: no worksheets"))?;
let range = workbook
.worksheet_range(&sheet_name)
.map_err(|e| Error::other(format!("spreadsheet read: {e}")))?;
let mut rows: Vec<Vec<String>> = Vec::with_capacity(range.height());
for row in range.rows() {
let cells = row.iter().map(cell_to_string).collect();
rows.push(cells);
}
Ok(rows)
}
fn cell_to_string(d: &Data) -> String {
match d {
Data::Empty => String::new(),
Data::String(s) => s.clone(),
Data::Float(f) => {
if f.fract() == 0.0 && f.is_finite() {
format!("{}", *f as i64)
} else {
format!("{f}")
}
}
Data::Int(i) => i.to_string(),
Data::Bool(b) => b.to_string(),
Data::DateTime(dt) => dt.to_string(),
Data::DateTimeIso(s) => s.clone(),
Data::DurationIso(s) => s.clone(),
Data::Error(e) => format!("#ERROR: {e:?}"),
}
}