1use std::io::Write;
29
30use energyml_geometry::{AttributeScope, Placement, SceneObject};
31
32use crate::error::{Error, Result};
33use crate::registry::{Exporter, Options};
34
35pub struct CsvExporter;
37
38impl Exporter for CsvExporter {
39 fn name(&self) -> &'static str {
40 "csv"
41 }
42
43 fn extension(&self) -> &'static str {
44 "csv"
45 }
46
47 fn mime(&self) -> &'static str {
48 "text/csv"
49 }
50
51 fn describe(&self) -> &'static str {
52 "long-format CSV: one row per element and attribute, placed and in metres"
53 }
54
55 fn placement(&self) -> Placement {
56 Placement::Metres
57 }
58
59 fn handles(&self, object: &SceneObject) -> bool {
60 !object.geometry.is_empty()
61 }
62
63 fn write(&self, object: &SceneObject, out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
64 if object.geometry.is_empty() {
65 return Err(Error::unsuitable(
66 "csv",
67 format!("`{}`, which has no points", object.title),
68 ));
69 }
70 self.write_all(std::slice::from_ref(object), out, options)
71 }
72
73 fn write_all(&self, objects: &[SceneObject], out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
74 writeln!(out, "object,uuid,element,index,x,y,z,property,kind,uom,value")?;
77 for object in objects {
78 if !self.handles(object) {
79 continue;
80 }
81 let object = crate::registry::prepare(object, options)?;
82 let object = object.as_ref();
83 let placement = options.placement.unwrap_or_else(|| self.placement());
84 let points = options.points(object, placement)?;
85 let name = field(&object.title);
86 let uuid = field(&object.uuid);
87
88 let of_scope = |scope: AttributeScope| {
89 object
90 .attributes
91 .iter()
92 .filter(move |attribute| attribute.scope == scope)
93 };
94
95 for (index, point) in points.iter().enumerate() {
98 let mut wrote = false;
99 for attribute in of_scope(AttributeScope::Points) {
100 for component in 0..attribute.components as usize {
101 let value = attribute
102 .values
103 .get(index * attribute.components as usize + component)
104 .copied();
105 writeln!(
106 out,
107 "{name},{uuid},node,{index},{},{},{},{},{},{},{}",
108 point[0],
109 point[1],
110 point[2],
111 field(&component_name(&attribute.name, attribute.components, component)),
112 field(attribute.kind.as_deref().unwrap_or("")),
113 field(attribute.uom.as_deref().unwrap_or("")),
114 cell(value),
115 )?;
116 wrote = true;
117 }
118 }
119 if !wrote {
120 writeln!(
121 out,
122 "{name},{uuid},node,{index},{},{},{},,,,",
123 point[0], point[1], point[2]
124 )?;
125 }
126 }
127
128 for attribute in of_scope(AttributeScope::Cells) {
131 let stride = attribute.components as usize;
132 for index in 0..attribute.values.len() / stride.max(1) {
133 for component in 0..stride {
134 let value = attribute.values.get(index * stride + component).copied();
135 writeln!(
136 out,
137 "{name},{uuid},cell,{index},,,,{},{},{},{}",
138 field(&component_name(&attribute.name, attribute.components, component)),
139 field(attribute.kind.as_deref().unwrap_or("")),
140 field(attribute.uom.as_deref().unwrap_or("")),
141 cell(value),
142 )?;
143 }
144 }
145 }
146 }
147 Ok(())
148 }
149}
150
151fn component_name(name: &str, components: u64, component: usize) -> String {
153 if components > 1 {
154 format!("{name}[{component}]")
155 } else {
156 name.to_owned()
157 }
158}
159
160fn cell(value: Option<f64>) -> String {
162 match value {
163 Some(v) if v.is_finite() => format!("{v}"),
164 Some(v) if v.is_infinite() => format!("{v}"),
167 _ => String::new(),
168 }
169}
170
171fn field(raw: &str) -> String {
173 if raw.contains([',', '"', '\n', '\r']) {
174 format!("\"{}\"", raw.replace('"', "\"\""))
175 } else {
176 raw.to_owned()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn a_field_with_a_comma_is_quoted_and_quotes_are_doubled() {
186 assert_eq!(field("plain"), "plain");
187 assert_eq!(field("a,b"), "\"a,b\"");
188 assert_eq!(field("say \"hi\""), "\"say \"\"hi\"\"\"");
189 }
190
191 #[test]
192 fn nan_is_an_empty_cell_and_infinity_is_not() {
193 assert_eq!(cell(Some(1.5)), "1.5");
194 assert_eq!(cell(Some(f64::NAN)), "");
195 assert_eq!(cell(None), "");
196 assert_eq!(cell(Some(f64::INFINITY)), "inf");
197 }
198
199 #[test]
200 fn a_vector_component_is_named_and_a_scalar_is_not() {
201 assert_eq!(component_name("Velocity", 3, 1), "Velocity[1]");
202 assert_eq!(component_name("Porosity", 1, 0), "Porosity");
203 }
204}