use std::io::Write;
use energyml_geometry::{AttributeScope, Placement, SceneObject};
use crate::error::{Error, Result};
use crate::registry::{Exporter, Options};
pub struct CsvExporter;
impl Exporter for CsvExporter {
fn name(&self) -> &'static str {
"csv"
}
fn extension(&self) -> &'static str {
"csv"
}
fn mime(&self) -> &'static str {
"text/csv"
}
fn describe(&self) -> &'static str {
"long-format CSV: one row per element and attribute, placed and in metres"
}
fn placement(&self) -> Placement {
Placement::Metres
}
fn handles(&self, object: &SceneObject) -> bool {
!object.geometry.is_empty()
}
fn write(&self, object: &SceneObject, out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
if object.geometry.is_empty() {
return Err(Error::unsuitable(
"csv",
format!("`{}`, which has no points", object.title),
));
}
self.write_all(std::slice::from_ref(object), out, options)
}
fn write_all(&self, objects: &[SceneObject], out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
writeln!(out, "object,uuid,element,index,x,y,z,property,kind,uom,value")?;
for object in objects {
if !self.handles(object) {
continue;
}
let object = crate::registry::prepare(object, options)?;
let object = object.as_ref();
let placement = options.placement.unwrap_or_else(|| self.placement());
let points = options.points(object, placement)?;
let name = field(&object.title);
let uuid = field(&object.uuid);
let of_scope = |scope: AttributeScope| {
object
.attributes
.iter()
.filter(move |attribute| attribute.scope == scope)
};
for (index, point) in points.iter().enumerate() {
let mut wrote = false;
for attribute in of_scope(AttributeScope::Points) {
for component in 0..attribute.components as usize {
let value = attribute
.values
.get(index * attribute.components as usize + component)
.copied();
writeln!(
out,
"{name},{uuid},node,{index},{},{},{},{},{},{},{}",
point[0],
point[1],
point[2],
field(&component_name(&attribute.name, attribute.components, component)),
field(attribute.kind.as_deref().unwrap_or("")),
field(attribute.uom.as_deref().unwrap_or("")),
cell(value),
)?;
wrote = true;
}
}
if !wrote {
writeln!(
out,
"{name},{uuid},node,{index},{},{},{},,,,",
point[0], point[1], point[2]
)?;
}
}
for attribute in of_scope(AttributeScope::Cells) {
let stride = attribute.components as usize;
for index in 0..attribute.values.len() / stride.max(1) {
for component in 0..stride {
let value = attribute.values.get(index * stride + component).copied();
writeln!(
out,
"{name},{uuid},cell,{index},,,,{},{},{},{}",
field(&component_name(&attribute.name, attribute.components, component)),
field(attribute.kind.as_deref().unwrap_or("")),
field(attribute.uom.as_deref().unwrap_or("")),
cell(value),
)?;
}
}
}
}
Ok(())
}
}
fn component_name(name: &str, components: u64, component: usize) -> String {
if components > 1 {
format!("{name}[{component}]")
} else {
name.to_owned()
}
}
fn cell(value: Option<f64>) -> String {
match value {
Some(v) if v.is_finite() => format!("{v}"),
Some(v) if v.is_infinite() => format!("{v}"),
_ => String::new(),
}
}
fn field(raw: &str) -> String {
if raw.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", raw.replace('"', "\"\""))
} else {
raw.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_with_a_comma_is_quoted_and_quotes_are_doubled() {
assert_eq!(field("plain"), "plain");
assert_eq!(field("a,b"), "\"a,b\"");
assert_eq!(field("say \"hi\""), "\"say \"\"hi\"\"\"");
}
#[test]
fn nan_is_an_empty_cell_and_infinity_is_not() {
assert_eq!(cell(Some(1.5)), "1.5");
assert_eq!(cell(Some(f64::NAN)), "");
assert_eq!(cell(None), "");
assert_eq!(cell(Some(f64::INFINITY)), "inf");
}
#[test]
fn a_vector_component_is_named_and_a_scalar_is_not() {
assert_eq!(component_name("Velocity", 3, 1), "Velocity[1]");
assert_eq!(component_name("Porosity", 1, 0), "Porosity");
}
}