1use std::fmt::Write as FmtWrite;
29use std::fs;
30use std::io::{self, BufRead};
31
32use crate::Error as IoError;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum TecplotZoneType {
41 Ordered,
43 FETriangle,
45 FEQuad,
47 FETetra,
49 FEBrick,
51}
52
53impl TecplotZoneType {
54 pub fn as_tecplot_str(&self) -> &'static str {
56 match self {
57 TecplotZoneType::Ordered => "ORDERED",
58 TecplotZoneType::FETriangle => "FETRIANGLE",
59 TecplotZoneType::FEQuad => "FEQUADRILATERAL",
60 TecplotZoneType::FETetra => "FETETRAHEDRON",
61 TecplotZoneType::FEBrick => "FEBRICK",
62 }
63 }
64
65 pub fn from_keyword(s: &str) -> Self {
67 Self::from(s)
68 }
69}
70
71impl From<&str> for TecplotZoneType {
72 fn from(s: &str) -> Self {
73 match s.trim().to_uppercase().as_str() {
74 "ORDERED" => TecplotZoneType::Ordered,
75 "FETRIANGLE" | "FE_TRIANGLE" => TecplotZoneType::FETriangle,
76 "FEQUADRILATERAL" | "FEQUAD" | "FE_QUAD" => TecplotZoneType::FEQuad,
77 "FETETRAHEDRON" | "FETETRA" | "FE_TETRA" => TecplotZoneType::FETetra,
78 "FEBRICK" | "FE_BRICK" => TecplotZoneType::FEBrick,
79 _ => TecplotZoneType::Ordered,
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq)]
90pub struct TecplotVariable {
91 pub name: String,
93 pub data: Vec<f64>,
95}
96
97impl TecplotVariable {
98 pub fn new(name: impl Into<String>, data: Vec<f64>) -> Self {
100 Self {
101 name: name.into(),
102 data,
103 }
104 }
105
106 pub fn len(&self) -> usize {
108 self.data.len()
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.data.is_empty()
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct TecplotZone {
124 pub title: String,
126 pub zone_type: TecplotZoneType,
128 pub i_dim: usize,
130 pub j_dim: usize,
132 pub k_dim: usize,
134 pub variables: Vec<TecplotVariable>,
136 pub n_nodes: usize,
138 pub n_elements: usize,
140}
141
142impl TecplotZone {
143 pub fn new_ordered(title: impl Into<String>, i_dim: usize, j_dim: usize, k_dim: usize) -> Self {
145 Self {
146 title: title.into(),
147 zone_type: TecplotZoneType::Ordered,
148 i_dim,
149 j_dim,
150 k_dim,
151 variables: Vec::new(),
152 n_nodes: i_dim * j_dim * k_dim,
153 n_elements: 0,
154 }
155 }
156
157 pub fn new_fe(
159 title: impl Into<String>,
160 zone_type: TecplotZoneType,
161 n_nodes: usize,
162 n_elements: usize,
163 ) -> Self {
164 Self {
165 title: title.into(),
166 zone_type,
167 i_dim: 0,
168 j_dim: 0,
169 k_dim: 0,
170 variables: Vec::new(),
171 n_nodes,
172 n_elements,
173 }
174 }
175
176 pub fn point_count(&self) -> usize {
178 match self.zone_type {
179 TecplotZoneType::Ordered => self.i_dim * self.j_dim * self.k_dim,
180 _ => self.n_nodes,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Default)]
191pub struct TecplotDataset {
192 pub title: String,
194 pub variables: Vec<String>,
196 pub zones: Vec<TecplotZone>,
198}
199
200impl TecplotDataset {
201 pub fn new(title: impl Into<String>) -> Self {
203 Self {
204 title: title.into(),
205 variables: Vec::new(),
206 zones: Vec::new(),
207 }
208 }
209
210 pub fn write(&self, path: &str) -> Result<(), IoError> {
214 let writer = TecplotWriter::new();
215 writer.write(self, path)
216 }
217
218 pub fn read(path: &str) -> Result<Self, IoError> {
222 TecplotReader::new().parse(path)
223 }
224}
225
226#[derive(Debug, Clone, Default)]
232pub struct TecplotWriter;
233
234impl TecplotWriter {
235 pub fn new() -> Self {
237 Self
238 }
239
240 pub fn write(&self, dataset: &TecplotDataset, path: &str) -> Result<(), IoError> {
243 let mut buf = String::new();
244
245 let _ = writeln!(buf, "TITLE = \"{}\"", dataset.title);
247
248 if !dataset.variables.is_empty() {
249 let vars: Vec<String> = dataset
250 .variables
251 .iter()
252 .map(|v| format!("\"{v}\""))
253 .collect();
254 let _ = writeln!(buf, "VARIABLES = {}", vars.join(", "));
255 }
256
257 for zone in &dataset.zones {
259 self.write_zone(&mut buf, zone);
260 }
261
262 fs::write(path, buf).map_err(IoError::Io)
263 }
264
265 fn write_zone(&self, buf: &mut String, zone: &TecplotZone) {
267 match zone.zone_type {
268 TecplotZoneType::Ordered => {
269 writeln!(
270 buf,
271 "ZONE T=\"{}\", I={}, J={}, K={}, DATAPACKING=POINT",
272 zone.title, zone.i_dim, zone.j_dim, zone.k_dim
273 )
274 .expect("operation should succeed");
275 self.write_point_data(buf, zone);
276 }
277 _ => {
278 writeln!(
279 buf,
280 "ZONE T=\"{}\", N={}, E={}, ZONETYPE={}, DATAPACKING=POINT",
281 zone.title,
282 zone.n_nodes,
283 zone.n_elements,
284 zone.zone_type.as_tecplot_str()
285 )
286 .expect("operation should succeed");
287 self.write_point_data(buf, zone);
288 }
289 }
290 }
291
292 fn write_point_data(&self, buf: &mut String, zone: &TecplotZone) {
294 if zone.variables.is_empty() {
295 return;
296 }
297 let n = zone.point_count();
298 for idx in 0..n {
299 let row: Vec<String> = zone
300 .variables
301 .iter()
302 .map(|v| {
303 if idx < v.data.len() {
304 format!("{:.15e}", v.data[idx])
305 } else {
306 "0.000000000000000e0".to_string()
307 }
308 })
309 .collect();
310 let _ = writeln!(buf, "{}", row.join(" "));
311 }
312 }
313}
314
315#[derive(Debug, Clone, Default)]
324pub struct TecplotReader;
325
326impl TecplotReader {
327 pub fn new() -> Self {
329 Self
330 }
331
332 pub fn parse(&self, path: &str) -> Result<TecplotDataset, IoError> {
334 let file = fs::File::open(path).map_err(IoError::Io)?;
335 let reader = io::BufReader::new(file);
336
337 let mut dataset = TecplotDataset::new("");
338 let mut current_zone: Option<TecplotZone> = None;
339 let mut remaining_points: usize = 0;
341 let mut zone_floats: Vec<f64> = Vec::new();
343
344 for line_res in reader.lines() {
345 let line = line_res.map_err(IoError::Io)?;
346 let trimmed = line.trim();
347
348 if trimmed.is_empty() || trimmed.starts_with('#') {
349 continue;
350 }
351
352 let upper = trimmed.to_uppercase();
353
354 if upper.starts_with("TITLE") {
356 dataset.title =
357 Self::extract_quoted_value(trimmed).unwrap_or_else(|| trimmed.to_string());
358 continue;
359 }
360
361 if upper.starts_with("VARIABLES") {
363 dataset.variables = Self::parse_variables_line(trimmed);
364 continue;
365 }
366
367 if upper.starts_with("ZONE") {
369 if let Some(mut z) = current_zone.take() {
371 Self::distribute_floats(&mut z, &zone_floats);
372 dataset.zones.push(z);
373 }
374 zone_floats.clear();
375
376 let zone = Self::parse_zone_header(trimmed, &dataset.variables);
377 remaining_points = zone.point_count();
378 current_zone = Some(zone);
379 continue;
380 }
381
382 if current_zone.is_some() && remaining_points > 0 {
384 for token in trimmed.split_whitespace() {
385 if let Ok(v) = token.parse::<f64>() {
386 zone_floats.push(v);
387 }
388 }
389 let nvars = dataset.variables.len().max(1);
391 let rows_so_far = zone_floats.len() / nvars;
392 if rows_so_far >= remaining_points {
393 remaining_points = 0;
394 }
395 }
396 }
397
398 if let Some(mut z) = current_zone.take() {
400 Self::distribute_floats(&mut z, &zone_floats);
401 dataset.zones.push(z);
402 }
403
404 Ok(dataset)
405 }
406
407 fn distribute_floats(zone: &mut TecplotZone, floats: &[f64]) {
409 let nvars = zone.variables.len();
410 if nvars == 0 {
411 return;
412 }
413 for (idx, &v) in floats.iter().enumerate() {
414 let var_idx = idx % nvars;
415 if var_idx < zone.variables.len() {
416 zone.variables[var_idx].data.push(v);
417 }
418 }
419 }
420
421 fn parse_zone_header(line: &str, var_names: &[String]) -> TecplotZone {
423 let title = Self::extract_param_value(line, "T")
424 .or_else(|| Self::extract_param_value(line, "TITLE"))
425 .unwrap_or_default();
426
427 let i_dim = Self::extract_param_value(line, "I")
428 .and_then(|s| s.parse().ok())
429 .unwrap_or(1);
430 let j_dim = Self::extract_param_value(line, "J")
431 .and_then(|s| s.parse().ok())
432 .unwrap_or(1);
433 let k_dim = Self::extract_param_value(line, "K")
434 .and_then(|s| s.parse().ok())
435 .unwrap_or(1);
436
437 let n_nodes = Self::extract_param_value(line, "N")
438 .and_then(|s| s.parse().ok())
439 .unwrap_or(0);
440 let n_elements = Self::extract_param_value(line, "E")
441 .and_then(|s| s.parse().ok())
442 .unwrap_or(0);
443
444 let zone_type_str = Self::extract_param_value(line, "ZONETYPE")
445 .or_else(|| Self::extract_param_value(line, "F"))
446 .unwrap_or_else(|| "ORDERED".to_string());
447 let zone_type = TecplotZoneType::from_keyword(&zone_type_str);
448
449 let mut zone = if zone_type == TecplotZoneType::Ordered {
450 TecplotZone::new_ordered(title, i_dim, j_dim, k_dim)
451 } else {
452 TecplotZone::new_fe(title, zone_type, n_nodes, n_elements)
453 };
454
455 for name in var_names {
457 zone.variables
458 .push(TecplotVariable::new(name.clone(), Vec::new()));
459 }
460
461 zone
462 }
463
464 fn extract_param_value(line: &str, key: &str) -> Option<String> {
468 let upper = line.to_uppercase();
469 let key_eq = format!("{key}=");
470 let pos = upper.find(&key_eq.to_uppercase())?;
472 let rest = &line[pos + key_eq.len()..];
473 let rest_trimmed = rest.trim_start();
474 if let Some(inner) = rest_trimmed.strip_prefix('"') {
475 let end = inner.find('"').unwrap_or(inner.len());
477 Some(inner[..end].to_string())
478 } else {
479 let end = rest_trimmed.find([',', ' ']).unwrap_or(rest_trimmed.len());
481 Some(rest_trimmed[..end].trim().to_string())
482 }
483 }
484
485 fn extract_quoted_value(line: &str) -> Option<String> {
487 let start = line.find('"')?;
488 let rest = &line[start + 1..];
489 let end = rest.find('"').unwrap_or(rest.len());
490 Some(rest[..end].to_string())
491 }
492
493 fn parse_variables_line(line: &str) -> Vec<String> {
495 let after_eq = if let Some(pos) = line.find('=') {
497 &line[pos + 1..]
498 } else {
499 line
500 };
501 let mut names = Vec::new();
502 let mut chars = after_eq.chars().peekable();
503 while let Some(&c) = chars.peek() {
504 if c == '"' {
505 chars.next(); let mut name = String::new();
507 for ch in chars.by_ref() {
508 if ch == '"' {
509 break;
510 }
511 name.push(ch);
512 }
513 if !name.is_empty() {
514 names.push(name);
515 }
516 } else {
517 chars.next();
518 }
519 }
520 names
521 }
522}
523
524#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
535 fn test_zone_type_as_str_ordered() {
536 assert_eq!(TecplotZoneType::Ordered.as_tecplot_str(), "ORDERED");
537 }
538
539 #[test]
540 fn test_zone_type_as_str_fe_triangle() {
541 assert_eq!(TecplotZoneType::FETriangle.as_tecplot_str(), "FETRIANGLE");
542 }
543
544 #[test]
545 fn test_zone_type_as_str_fe_quad() {
546 assert_eq!(TecplotZoneType::FEQuad.as_tecplot_str(), "FEQUADRILATERAL");
547 }
548
549 #[test]
550 fn test_zone_type_as_str_fe_tetra() {
551 assert_eq!(TecplotZoneType::FETetra.as_tecplot_str(), "FETETRAHEDRON");
552 }
553
554 #[test]
555 fn test_zone_type_as_str_fe_brick() {
556 assert_eq!(TecplotZoneType::FEBrick.as_tecplot_str(), "FEBRICK");
557 }
558
559 #[test]
560 fn test_zone_type_from_str_ordered() {
561 assert_eq!(
562 TecplotZoneType::from_keyword("ORDERED"),
563 TecplotZoneType::Ordered
564 );
565 }
566
567 #[test]
568 fn test_zone_type_from_str_fe_triangle() {
569 assert_eq!(
570 TecplotZoneType::from_keyword("FETRIANGLE"),
571 TecplotZoneType::FETriangle
572 );
573 }
574
575 #[test]
576 fn test_zone_type_from_str_fe_tetra() {
577 assert_eq!(
578 TecplotZoneType::from_keyword("FETETRAHEDRON"),
579 TecplotZoneType::FETetra
580 );
581 }
582
583 #[test]
584 fn test_zone_type_from_str_case_insensitive() {
585 assert_eq!(
586 TecplotZoneType::from_keyword("ordered"),
587 TecplotZoneType::Ordered
588 );
589 }
590
591 #[test]
592 fn test_zone_type_from_str_unknown_defaults_to_ordered() {
593 assert_eq!(
594 TecplotZoneType::from_keyword("UNKNOWN"),
595 TecplotZoneType::Ordered
596 );
597 }
598
599 #[test]
602 fn test_variable_new() {
603 let v = TecplotVariable::new("Pressure", vec![1.0, 2.0, 3.0]);
604 assert_eq!(v.name, "Pressure");
605 assert_eq!(v.len(), 3);
606 assert!(!v.is_empty());
607 }
608
609 #[test]
610 fn test_variable_empty() {
611 let v = TecplotVariable::new("X", vec![]);
612 assert!(v.is_empty());
613 }
614
615 #[test]
616 fn test_variable_data_values() {
617 let v = TecplotVariable::new("T", vec![100.0, 200.0]);
618 assert!((v.data[0] - 100.0).abs() < 1e-12);
619 assert!((v.data[1] - 200.0).abs() < 1e-12);
620 }
621
622 #[test]
625 fn test_zone_new_ordered() {
626 let z = TecplotZone::new_ordered("Zone 1", 5, 3, 2);
627 assert_eq!(z.zone_type, TecplotZoneType::Ordered);
628 assert_eq!(z.i_dim, 5);
629 assert_eq!(z.j_dim, 3);
630 assert_eq!(z.k_dim, 2);
631 assert_eq!(z.point_count(), 30);
632 }
633
634 #[test]
635 fn test_zone_new_fe() {
636 let z = TecplotZone::new_fe("FE Zone", TecplotZoneType::FETetra, 8, 2);
637 assert_eq!(z.zone_type, TecplotZoneType::FETetra);
638 assert_eq!(z.n_nodes, 8);
639 assert_eq!(z.n_elements, 2);
640 assert_eq!(z.point_count(), 8);
641 }
642
643 #[test]
644 fn test_zone_point_count_ordered_1d() {
645 let z = TecplotZone::new_ordered("1D", 10, 1, 1);
646 assert_eq!(z.point_count(), 10);
647 }
648
649 #[test]
650 fn test_zone_stores_variables() {
651 let mut z = TecplotZone::new_ordered("Z", 2, 1, 1);
652 z.variables.push(TecplotVariable::new("X", vec![0.0, 1.0]));
653 assert_eq!(z.variables.len(), 1);
654 assert_eq!(z.variables[0].name, "X");
655 }
656
657 #[test]
660 fn test_dataset_new() {
661 let ds = TecplotDataset::new("Test");
662 assert_eq!(ds.title, "Test");
663 assert!(ds.zones.is_empty());
664 assert!(ds.variables.is_empty());
665 }
666
667 #[test]
668 fn test_dataset_default() {
669 let ds = TecplotDataset::default();
670 assert!(ds.title.is_empty());
671 }
672
673 fn make_1d_dataset(path: &str, n: usize) -> TecplotDataset {
676 let _ = path; let mut ds = TecplotDataset::new("Test Dataset");
678 ds.variables = vec!["X".to_string(), "P".to_string()];
679 let mut zone = TecplotZone::new_ordered("Zone 1", n, 1, 1);
680 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
681 let p: Vec<f64> = (0..n).map(|i| (i as f64) * 2.0).collect();
682 zone.variables.push(TecplotVariable::new("X", x));
683 zone.variables.push(TecplotVariable::new("P", p));
684 ds.zones.push(zone);
685 ds
686 }
687
688 #[test]
689 fn test_write_creates_file() {
690 let path = std::env::temp_dir().join("oxiphysics_tecplot_write_test.dat");
691 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 5);
692 TecplotWriter::new()
693 .write(&ds, path.to_str().unwrap_or(""))
694 .expect("write failed");
695 assert!(path.exists());
696 }
697
698 #[test]
699 fn test_write_contains_title() {
700 let path = std::env::temp_dir().join("oxiphysics_tecplot_title.dat");
701 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
702 TecplotWriter::new()
703 .write(&ds, path.to_str().unwrap_or(""))
704 .unwrap();
705 let content = fs::read_to_string(&path).unwrap();
706 assert!(content.contains("Test Dataset"), "no title in output");
707 }
708
709 #[test]
710 fn test_write_contains_variables() {
711 let path = std::env::temp_dir().join("oxiphysics_tecplot_vars.dat");
712 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
713 TecplotWriter::new()
714 .write(&ds, path.to_str().unwrap_or(""))
715 .unwrap();
716 let content = fs::read_to_string(&path).unwrap();
717 assert!(content.contains("VARIABLES"), "no VARIABLES header");
718 assert!(content.contains('"'), "variables not quoted");
719 }
720
721 #[test]
722 fn test_write_contains_zone_keyword() {
723 let path = std::env::temp_dir().join("oxiphysics_tecplot_zone_kw.dat");
724 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
725 TecplotWriter::new()
726 .write(&ds, path.to_str().unwrap_or(""))
727 .unwrap();
728 let content = fs::read_to_string(&path).unwrap();
729 assert!(content.contains("ZONE"), "no ZONE header");
730 }
731
732 #[test]
733 fn test_write_ordered_has_ijk() {
734 let path = std::env::temp_dir().join("oxiphysics_tecplot_ijk.dat");
735 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
736 TecplotWriter::new()
737 .write(&ds, path.to_str().unwrap_or(""))
738 .unwrap();
739 let content = fs::read_to_string(&path).unwrap();
740 assert!(content.contains("I="), "no I= in ZONE header");
741 assert!(content.contains("J="), "no J= in ZONE header");
742 assert!(content.contains("K="), "no K= in ZONE header");
743 }
744
745 #[test]
746 fn test_write_data_lines_count() {
747 let path = std::env::temp_dir().join("oxiphysics_tecplot_datarows.dat");
749 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 4);
750 TecplotWriter::new()
751 .write(&ds, path.to_str().unwrap_or(""))
752 .unwrap();
753 let content = fs::read_to_string(&path).unwrap();
754 let data_lines = content
756 .lines()
757 .filter(|l| {
758 let t = l.trim();
759 t.starts_with(|c: char| c.is_ascii_digit() || c == '-')
760 })
761 .count();
762 assert_eq!(data_lines, 4, "expected 4 data rows, got {data_lines}");
763 }
764
765 #[test]
766 fn test_write_fe_zone() {
767 let path = std::env::temp_dir().join("oxiphysics_tecplot_fe_zone.dat");
768 let mut ds = TecplotDataset::new("FE Dataset");
769 ds.variables = vec!["X".to_string()];
770 let mut zone = TecplotZone::new_fe("FE Zone", TecplotZoneType::FETetra, 4, 1);
771 zone.variables
772 .push(TecplotVariable::new("X", vec![0.0, 1.0, 0.0, 0.0]));
773 ds.zones.push(zone);
774 TecplotWriter::new()
775 .write(&ds, path.to_str().unwrap_or(""))
776 .unwrap();
777 let content = fs::read_to_string(&path).unwrap();
778 assert!(content.contains("FETETRAHEDRON"));
779 }
780
781 #[test]
784 fn test_roundtrip_title() {
785 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_title.dat");
786 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
787 ds.write(path.to_str().unwrap_or("")).unwrap();
788 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
789 assert_eq!(parsed.title, "Test Dataset");
790 }
791
792 #[test]
793 fn test_roundtrip_variable_names() {
794 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_varnames.dat");
795 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
796 ds.write(path.to_str().unwrap_or("")).unwrap();
797 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
798 assert_eq!(parsed.variables, vec!["X", "P"]);
799 }
800
801 #[test]
802 fn test_roundtrip_zone_count() {
803 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_nzones.dat");
804 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
805 ds.write(path.to_str().unwrap_or("")).unwrap();
806 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
807 assert_eq!(parsed.zones.len(), 1, "expected 1 zone");
808 }
809
810 #[test]
811 fn test_roundtrip_zone_type_ordered() {
812 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_ztype.dat");
813 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
814 ds.write(path.to_str().unwrap_or("")).unwrap();
815 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
816 assert_eq!(parsed.zones[0].zone_type, TecplotZoneType::Ordered);
817 }
818
819 #[test]
820 fn test_roundtrip_zone_i_dim() {
821 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_idim.dat");
822 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 5);
823 ds.write(path.to_str().unwrap_or("")).unwrap();
824 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
825 assert_eq!(parsed.zones[0].i_dim, 5);
826 }
827
828 #[test]
829 fn test_roundtrip_data_values() {
830 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_data.dat");
831 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
832 ds.write(path.to_str().unwrap_or("")).unwrap();
833 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
834 let zone = &parsed.zones[0];
835 let x_var = zone.variables.iter().find(|v| v.name == "X").unwrap();
837 assert!((x_var.data[0]).abs() < 1e-6);
838 assert!((x_var.data[1] - 1.0).abs() < 1e-6);
839 assert!((x_var.data[2] - 2.0).abs() < 1e-6);
840 }
841
842 #[test]
843 fn test_roundtrip_second_variable() {
844 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_p.dat");
845 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 3);
846 ds.write(path.to_str().unwrap_or("")).unwrap();
847 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
848 let zone = &parsed.zones[0];
849 let p_var = zone.variables.iter().find(|v| v.name == "P").unwrap();
850 assert!((p_var.data[0]).abs() < 1e-6, "P[0]={}", p_var.data[0]);
851 assert!((p_var.data[1] - 2.0).abs() < 1e-6, "P[1]={}", p_var.data[1]);
852 assert!((p_var.data[2] - 4.0).abs() < 1e-6, "P[2]={}", p_var.data[2]);
853 }
854
855 #[test]
856 fn test_roundtrip_multi_zone() {
857 let path = std::env::temp_dir().join("oxiphysics_tecplot_rt_mz.dat");
858 let mut ds = TecplotDataset::new("Multi");
859 ds.variables = vec!["X".to_string()];
860 for i in 0..3usize {
861 let mut z = TecplotZone::new_ordered(format!("Zone {i}"), 2, 1, 1);
862 z.variables
863 .push(TecplotVariable::new("X", vec![i as f64, i as f64 + 1.0]));
864 ds.zones.push(z);
865 }
866 ds.write(path.to_str().unwrap_or("")).unwrap();
867 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
868 assert_eq!(parsed.zones.len(), 3, "expected 3 zones");
869 }
870
871 #[test]
872 fn test_read_missing_file_returns_error() {
873 let path = std::env::temp_dir().join("no_such_file_oxiphysics_tec.dat");
874 let result = TecplotDataset::read(path.to_str().unwrap_or(""));
875 assert!(result.is_err());
876 }
877
878 #[test]
879 fn test_roundtrip_large_ordered_zone() {
880 let path = std::env::temp_dir().join("oxiphysics_tecplot_large.dat");
881 let n = 50;
882 let ds = make_1d_dataset(path.to_str().unwrap_or(""), n);
883 ds.write(path.to_str().unwrap_or("")).unwrap();
884 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
885 let zone = &parsed.zones[0];
886 assert_eq!(zone.i_dim, n);
887 let x_var = zone.variables.iter().find(|v| v.name == "X").unwrap();
888 assert_eq!(x_var.data.len(), n);
889 }
890
891 #[test]
892 fn test_roundtrip_3d_zone() {
893 let path = std::env::temp_dir().join("oxiphysics_tecplot_3d.dat");
894 let mut ds = TecplotDataset::new("3D");
895 ds.variables = vec!["X".to_string()];
896 let n = 2usize;
897 let total = n * n * n;
898 let mut zone = TecplotZone::new_ordered("3D Zone", n, n, n);
899 let x: Vec<f64> = (0..total).map(|i| i as f64).collect();
900 zone.variables.push(TecplotVariable::new("X", x));
901 ds.zones.push(zone);
902 ds.write(path.to_str().unwrap_or("")).unwrap();
903 let parsed = TecplotDataset::read(path.to_str().unwrap_or("")).unwrap();
904 assert_eq!(parsed.zones[0].i_dim, n);
905 assert_eq!(parsed.zones[0].j_dim, n);
906 assert_eq!(parsed.zones[0].k_dim, n);
907 }
908
909 #[test]
912 fn test_writer_datapacking_point_keyword() {
913 let path = std::env::temp_dir().join("oxiphysics_tecplot_dpkw.dat");
914 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 2);
915 TecplotWriter::new()
916 .write(&ds, path.to_str().unwrap_or(""))
917 .unwrap();
918 let content = fs::read_to_string(&path).unwrap();
919 assert!(content.contains("DATAPACKING=POINT"));
920 }
921
922 #[test]
923 fn test_writer_zone_title_in_output() {
924 let path = std::env::temp_dir().join("oxiphysics_tecplot_ztitle.dat");
925 let ds = make_1d_dataset(path.to_str().unwrap_or(""), 2);
926 TecplotWriter::new()
927 .write(&ds, path.to_str().unwrap_or(""))
928 .unwrap();
929 let content = fs::read_to_string(&path).unwrap();
930 assert!(content.contains("Zone 1"), "zone title missing");
931 }
932
933 #[test]
936 fn test_parse_variables_line_three_vars() {
937 let line = r#"VARIABLES = "X" "Y" "Z""#;
938 let vars = TecplotReader::parse_variables_line(line);
939 assert_eq!(vars, vec!["X", "Y", "Z"]);
940 }
941
942 #[test]
943 fn test_parse_variables_line_with_commas() {
944 let line = r#"VARIABLES = "X", "P""#;
945 let vars = TecplotReader::parse_variables_line(line);
946 assert_eq!(vars, vec!["X", "P"]);
947 }
948
949 #[test]
950 fn test_parse_variables_line_single() {
951 let line = r#"VARIABLES = "T""#;
952 let vars = TecplotReader::parse_variables_line(line);
953 assert_eq!(vars, vec!["T"]);
954 }
955}