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