1use std::fmt::Write as _;
49use std::path::Path;
50
51use super::IoError;
52
53#[derive(Debug, Clone, PartialEq)]
59pub(crate) struct Token {
60 pub text: String,
63 pub glued: bool,
65 pub quoted: bool,
67}
68
69pub(crate) fn tokenize(text: &str) -> Vec<Token> {
73 let bytes = text.as_bytes();
74 let n = bytes.len();
75 let mut i = 0;
76 let mut tokens: Vec<Token> = Vec::new();
77 let mut pending_ws = true;
80 let mut word = String::new();
81 let mut word_glued = true;
82
83 let is_delim = |c: u8| matches!(c, b'(' | b')' | b'{' | b'}' | b';' | b'[' | b']');
84
85 macro_rules! flush_word {
86 () => {
87 if !word.is_empty() {
88 tokens.push(Token {
89 text: std::mem::take(&mut word),
90 glued: word_glued,
91 quoted: false,
92 });
93 }
94 };
95 }
96
97 while i < n {
98 let c = bytes[i];
99 if i + 1 < n && c == b'/' && bytes[i + 1] == b'/' {
100 flush_word!();
101 while i < n && bytes[i] != b'\n' {
102 i += 1;
103 }
104 pending_ws = true;
105 } else if i + 1 < n && c == b'/' && bytes[i + 1] == b'*' {
106 flush_word!();
107 i += 2;
108 while i + 1 < n && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
109 i += 1;
110 }
111 i += 2;
112 pending_ws = true;
113 } else if c == b'"' {
114 flush_word!();
115 i += 1;
116 let mut s = String::new();
117 while i < n && bytes[i] != b'"' {
118 s.push(bytes[i] as char);
119 i += 1;
120 }
121 i += 1; tokens.push(Token {
123 text: s,
124 glued: !pending_ws,
125 quoted: true,
126 });
127 pending_ws = false;
128 } else if is_delim(c) {
129 flush_word!();
130 tokens.push(Token {
131 text: (c as char).to_string(),
132 glued: !pending_ws,
133 quoted: false,
134 });
135 pending_ws = false;
136 i += 1;
137 } else if (c as char).is_whitespace() {
138 flush_word!();
139 pending_ws = true;
140 i += 1;
141 } else {
142 if word.is_empty() {
143 word_glued = !pending_ws;
144 }
145 word.push(c as char);
146 pending_ws = false;
147 i += 1;
148 }
149 }
150 flush_word!();
151 tokens
152}
153
154#[derive(Debug, Clone, PartialEq)]
167pub struct Dimensioned {
168 pub dims: [f64; 7],
171 pub value: Vec<FoamValue>,
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub enum FoamValue {
179 Scalar(f64),
181 Word(String),
184 Str(String),
186 List(Vec<FoamValue>),
188}
189
190impl FoamValue {
191 pub fn as_vector3(&self) -> Option<crate::primitives::Vector3> {
196 if let FoamValue::List(v) = self {
197 if let [FoamValue::Scalar(x), FoamValue::Scalar(y), FoamValue::Scalar(z)] = v.as_slice()
198 {
199 return Some(crate::primitives::Vector3::new(*x, *y, *z));
200 }
201 }
202 None
203 }
204
205 pub fn as_scalar(&self) -> Option<f64> {
207 match self {
208 FoamValue::Scalar(x) => Some(*x),
209 _ => None,
210 }
211 }
212
213 pub fn as_word(&self) -> Option<&str> {
215 match self {
216 FoamValue::Word(w) => Some(w.as_str()),
217 _ => None,
218 }
219 }
220}
221
222#[derive(Debug, Clone, PartialEq)]
224pub enum FoamEntry {
225 Scalar(f64),
227 Word(String),
230 Str(String),
232 Tokens(Vec<FoamValue>),
236 List(Vec<FoamValue>),
238 Dimensioned(Dimensioned),
240 SubDict(FoamDict),
242}
243
244#[derive(Debug, Clone, Default, PartialEq)]
250pub struct FoamDict {
251 entries: Vec<(String, FoamEntry)>,
252}
253
254impl FoamDict {
255 pub fn new() -> Self {
257 Self::default()
258 }
259
260 pub fn insert(&mut self, keyword: impl Into<String>, entry: FoamEntry) -> &mut Self {
264 let keyword = keyword.into();
265 if let Some(slot) = self.entries.iter_mut().find(|(k, _)| *k == keyword) {
266 slot.1 = entry;
267 } else {
268 self.entries.push((keyword, entry));
269 }
270 self
271 }
272
273 pub fn get(&self, keyword: &str) -> Option<&FoamEntry> {
275 self.entries
276 .iter()
277 .find(|(k, _)| k == keyword)
278 .map(|(_, v)| v)
279 }
280
281 pub fn get_dict(&self, keyword: &str) -> Option<&FoamDict> {
283 match self.get(keyword) {
284 Some(FoamEntry::SubDict(d)) => Some(d),
285 _ => None,
286 }
287 }
288
289 pub fn get_scalar(&self, keyword: &str) -> Option<f64> {
291 match self.get(keyword) {
292 Some(FoamEntry::Scalar(x)) => Some(*x),
293 _ => None,
294 }
295 }
296
297 pub fn get_word(&self, keyword: &str) -> Option<&str> {
299 match self.get(keyword) {
300 Some(FoamEntry::Word(w)) => Some(w.as_str()),
301 _ => None,
302 }
303 }
304
305 pub fn iter(&self) -> impl Iterator<Item = (&str, &FoamEntry)> {
307 self.entries.iter().map(|(k, v)| (k.as_str(), v))
308 }
309
310 pub fn len(&self) -> usize {
312 self.entries.len()
313 }
314
315 pub fn is_empty(&self) -> bool {
317 self.entries.is_empty()
318 }
319}
320
321#[derive(Debug, Clone, Default, PartialEq)]
331pub struct FoamHeader {
332 entries: Vec<(String, String)>,
333}
334
335impl FoamHeader {
336 pub fn new() -> Self {
338 Self::default()
339 }
340
341 pub fn standard(class: &str, object: &str) -> Self {
344 let mut h = Self::new();
345 h.set("version", "2.0");
346 h.set("format", "ascii");
347 h.set("class", class);
348 h.set("object", object);
349 h
350 }
351
352 pub fn standard_with_location(class: &str, location: &str, object: &str) -> Self {
354 let mut h = Self::new();
355 h.set("version", "2.0");
356 h.set("format", "ascii");
357 h.set("class", class);
358 h.set("location", &format!("\"{location}\""));
359 h.set("object", object);
360 h
361 }
362
363 pub fn set(&mut self, keyword: &str, value: &str) -> &mut Self {
365 if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == keyword) {
366 slot.1 = value.to_string();
367 } else {
368 self.entries.push((keyword.to_string(), value.to_string()));
369 }
370 self
371 }
372
373 pub fn get(&self, keyword: &str) -> Option<&str> {
375 self.entries
376 .iter()
377 .find(|(k, _)| k == keyword)
378 .map(|(_, v)| v.as_str())
379 }
380
381 pub fn class(&self) -> Option<&str> {
383 self.get("class")
384 }
385
386 pub fn object(&self) -> Option<&str> {
388 self.get("object")
389 }
390}
391
392struct Parser {
401 tokens: Vec<Token>,
402 pos: usize,
403 context: String,
404}
405
406impl Parser {
407 fn new(tokens: Vec<Token>, context: impl Into<String>) -> Self {
408 Self {
409 tokens,
410 pos: 0,
411 context: context.into(),
412 }
413 }
414
415 fn peek(&self) -> Option<&Token> {
416 self.tokens.get(self.pos)
417 }
418
419 fn peek_text(&self) -> Option<&str> {
420 self.tokens.get(self.pos).map(|t| t.text.as_str())
421 }
422
423 fn advance(&mut self) -> Option<Token> {
424 let t = self.tokens.get(self.pos).cloned();
425 if t.is_some() {
426 self.pos += 1;
427 }
428 t
429 }
430
431 fn err(&self, message: impl Into<String>) -> IoError {
432 IoError::parse(self.context.clone(), message.into())
433 }
434
435 fn expect(&mut self, tok: &str) -> Result<(), IoError> {
436 match self.advance() {
437 Some(t) if t.text == tok => Ok(()),
438 Some(t) => Err(self.err(format!("expected `{tok}`, found `{}`", t.text))),
439 None => Err(self.err(format!("expected `{tok}`, found end of input"))),
440 }
441 }
442
443 fn parse_keyword(&mut self) -> Result<String, IoError> {
446 let first = self
447 .advance()
448 .ok_or_else(|| self.err("expected a keyword, found end of input"))?;
449 if matches!(first.text.as_str(), "(" | ")" | "{" | "}" | ";" | "[" | "]") {
450 return Err(self.err(format!("expected a keyword, found `{}`", first.text)));
451 }
452 let mut kw = first.text.clone();
453 while matches!(self.peek(), Some(t) if t.text == "(" && t.glued) {
455 kw.push_str(&self.fold_paren_text());
456 }
457 Ok(kw)
458 }
459
460 fn fold_paren_text(&mut self) -> String {
463 let mut out = String::new();
464 self.advance();
466 out.push('(');
467 let mut depth = 1;
468 while depth > 0 {
469 let Some(t) = self.advance() else { break };
470 match t.text.as_str() {
471 "(" => {
472 depth += 1;
473 out.push('(');
474 }
475 ")" => {
476 depth -= 1;
477 out.push(')');
478 }
479 other => out.push_str(other),
480 }
481 }
482 out
483 }
484
485 fn parse_dict_body(&mut self, top_level: bool) -> Result<FoamDict, IoError> {
488 let mut dict = FoamDict::new();
489 loop {
490 match self.peek_text() {
491 None => {
492 if top_level {
493 break;
494 }
495 return Err(self.err("unexpected end of input inside `{ … }`"));
496 }
497 Some("}") => {
498 if top_level {
499 return Err(self.err("unexpected `}` at top level"));
500 }
501 self.advance();
502 break;
503 }
504 Some(";") => {
505 self.advance(); }
507 _ => {
508 let (kw, entry) = self.parse_entry()?;
509 dict.insert(kw, entry);
510 }
511 }
512 }
513 Ok(dict)
514 }
515
516 fn parse_entry(&mut self) -> Result<(String, FoamEntry), IoError> {
518 let keyword = self.parse_keyword()?;
519 match self.peek_text() {
520 Some("{") => {
521 self.advance();
522 let sub = self.parse_dict_body(false)?;
523 if self.peek_text() == Some(";") {
525 self.advance();
526 }
527 Ok((keyword, FoamEntry::SubDict(sub)))
528 }
529 Some("[") => {
530 let dim = self.parse_dimensioned()?;
531 self.expect(";")?;
532 Ok((keyword, FoamEntry::Dimensioned(dim)))
533 }
534 _ => {
535 let entry = self.parse_value_sequence()?;
536 self.expect(";")?;
537 Ok((keyword, entry))
538 }
539 }
540 }
541
542 fn parse_dimensioned(&mut self) -> Result<Dimensioned, IoError> {
544 self.expect("[")?;
545 let mut dims = [0.0f64; 7];
546 for (i, slot) in dims.iter_mut().enumerate() {
547 let t = self
548 .advance()
549 .ok_or_else(|| self.err("unexpected end of input inside `[ … ]`"))?;
550 *slot = t
551 .text
552 .parse::<f64>()
553 .map_err(|_| self.err(format!("dimension exponent {i} is not a number: `{}`", t.text)))?;
554 }
555 self.expect("]")?;
556 let mut value = Vec::new();
558 while !matches!(self.peek_text(), Some(";") | None) {
559 value.push(self.parse_leaf()?);
560 }
561 Ok(Dimensioned { dims, value })
562 }
563
564 fn parse_value_sequence(&mut self) -> Result<FoamEntry, IoError> {
567 let mut values = Vec::new();
568 while !matches!(self.peek_text(), Some(";") | None) {
569 values.push(self.parse_leaf()?);
570 }
571 Ok(match values.len() {
572 0 => return Err(self.err("empty entry value")),
573 1 => match values.into_iter().next().unwrap() {
574 FoamValue::Scalar(x) => FoamEntry::Scalar(x),
575 FoamValue::Word(w) => FoamEntry::Word(w),
576 FoamValue::Str(s) => FoamEntry::Str(s),
577 FoamValue::List(l) => FoamEntry::List(l),
578 },
579 _ => FoamEntry::Tokens(values),
580 })
581 }
582
583 fn parse_leaf(&mut self) -> Result<FoamValue, IoError> {
586 let t = self
587 .peek()
588 .ok_or_else(|| self.err("expected a value, found end of input"))?;
589 if t.text == "(" {
590 return self.parse_list();
591 }
592 let t = self.advance().unwrap();
594 if t.quoted {
595 return Ok(FoamValue::Str(t.text.clone()));
596 }
597 let text = t.text.clone();
598 if self.peek().is_some_and(|p| p.text == "(" && p.glued) {
600 let mut w = text;
601 while matches!(self.peek(), Some(p) if p.text == "(" && p.glued) {
602 w.push_str(&self.fold_paren_text());
603 }
604 return Ok(FoamValue::Word(w));
605 }
606 match text.parse::<f64>() {
607 Ok(x) => Ok(FoamValue::Scalar(x)),
608 Err(_) => Ok(FoamValue::Word(text)),
609 }
610 }
611
612 fn parse_list(&mut self) -> Result<FoamValue, IoError> {
615 self.expect("(")?;
616 let mut items = Vec::new();
617 loop {
618 match self.peek_text() {
619 Some(")") => {
620 self.advance();
621 break;
622 }
623 None => return Err(self.err("unexpected end of input inside `( … )`")),
624 _ => items.push(self.parse_leaf()?),
625 }
626 }
627 Ok(FoamValue::List(items))
628 }
629}
630
631fn parse_header(parser: &mut Parser) -> Result<Option<FoamHeader>, IoError> {
634 if parser.peek_text() != Some("FoamFile") {
635 return Ok(None);
636 }
637 parser.advance(); parser.expect("{")?;
639 let mut header = FoamHeader::new();
640 loop {
641 match parser.peek_text() {
642 Some("}") => {
643 parser.advance();
644 break;
645 }
646 None => return Err(parser.err("unexpected end of input inside FoamFile header")),
647 _ => {
648 let kw = parser.parse_keyword()?;
649 let vt = parser
651 .advance()
652 .ok_or_else(|| parser.err("expected a header value"))?;
653 let raw = if vt.quoted {
654 format!("\"{}\"", vt.text)
655 } else {
656 vt.text.clone()
657 };
658 parser.expect(";")?;
659 header.set(&kw, &raw);
660 }
661 }
662 }
663 Ok(Some(header))
664}
665
666#[derive(Debug, Clone, PartialEq)]
673pub struct FoamFile {
674 pub header: Option<FoamHeader>,
676 pub dict: FoamDict,
678}
679
680impl FoamFile {
681 pub fn parse(text: &str) -> Result<Self, IoError> {
683 Self::parse_named(text, "dictionary")
684 }
685
686 pub fn parse_named(text: &str, context: impl Into<String>) -> Result<Self, IoError> {
688 let tokens = tokenize(text);
689 let mut parser = Parser::new(tokens, context);
690 let header = parse_header(&mut parser)?;
691 let dict = parser.parse_dict_body(true)?;
692 Ok(Self { header, dict })
693 }
694
695 pub fn read(path: impl AsRef<Path>) -> Result<Self, IoError> {
697 let path = path.as_ref();
698 let text = std::fs::read_to_string(path)
699 .map_err(|e| IoError::io(path.display().to_string(), e))?;
700 Self::parse_named(&text, path.display().to_string())
701 }
702
703 pub fn to_foam_string(&self) -> String {
705 let mut out = String::new();
706 out.push_str(BANNER);
707 if let Some(h) = &self.header {
708 write_header(&mut out, h);
709 }
710 out.push_str(SEPARATOR);
711 out.push('\n');
712 write_dict_body(&mut out, &self.dict, 0);
713 out.push('\n');
714 out.push_str(FOOTER);
715 out
716 }
717
718 pub fn write(&self, path: impl AsRef<Path>) -> Result<(), IoError> {
720 let path = path.as_ref();
721 std::fs::write(path, self.to_foam_string())
722 .map_err(|e| IoError::io(path.display().to_string(), e))
723 }
724}
725
726pub(crate) const BANNER: &str = "\
730/*--------------------------------*- C++ -*----------------------------------*\\
731| ========= | |
732| \\\\ / F ield | OUTRAM PARK: independent OpenFOAM-format I/O |
733| \\\\ / O peration | (not the official OpenFOAM software) |
734| \\\\ / A nd | |
735| \\\\/ M anipulation | |
736\\*---------------------------------------------------------------------------*/
737";
738
739pub(crate) const SEPARATOR: &str =
740 "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //";
741
742pub(crate) const FOOTER: &str =
743 "// ************************************************************************* //\n";
744
745pub(crate) fn write_header(out: &mut String, header: &FoamHeader) {
747 out.push_str("FoamFile\n{\n");
748 for (k, v) in &header.entries {
749 let _ = writeln!(out, " {k:<12}{v};");
750 }
751 out.push_str("}\n");
752}
753
754pub(crate) fn fmt_scalar(x: f64) -> String {
756 if x.is_finite() && x == x.trunc() && x.abs() < 1e15 {
757 format!("{}", x as i64)
758 } else {
759 format!("{x}")
760 }
761}
762
763fn write_value(out: &mut String, v: &FoamValue) {
764 match v {
765 FoamValue::Scalar(x) => out.push_str(&fmt_scalar(*x)),
766 FoamValue::Word(w) => out.push_str(w),
767 FoamValue::Str(s) => {
768 out.push('"');
769 out.push_str(s);
770 out.push('"');
771 }
772 FoamValue::List(items) => {
773 out.push('(');
774 for (i, it) in items.iter().enumerate() {
775 if i > 0 {
776 out.push(' ');
777 }
778 write_value(out, it);
779 }
780 out.push(')');
781 }
782 }
783}
784
785fn write_dimensioned(out: &mut String, d: &Dimensioned) {
786 out.push('[');
787 for (i, e) in d.dims.iter().enumerate() {
788 if i > 0 {
789 out.push(' ');
790 }
791 out.push_str(&fmt_scalar(*e));
792 }
793 out.push(']');
794 for v in &d.value {
795 out.push(' ');
796 write_value(out, v);
797 }
798}
799
800fn indent(out: &mut String, level: usize) {
801 for _ in 0..level {
802 out.push_str(" ");
803 }
804}
805
806pub(crate) fn write_dict_body(out: &mut String, dict: &FoamDict, level: usize) {
808 for (k, entry) in dict.iter() {
809 match entry {
810 FoamEntry::SubDict(sub) => {
811 indent(out, level);
812 out.push_str(k);
813 out.push('\n');
814 indent(out, level);
815 out.push_str("{\n");
816 write_dict_body(out, sub, level + 1);
817 indent(out, level);
818 out.push_str("}\n");
819 }
820 FoamEntry::Scalar(x) => {
821 indent(out, level);
822 let _ = writeln!(out, "{k:<16}{};", fmt_scalar(*x));
823 }
824 FoamEntry::Word(w) => {
825 indent(out, level);
826 let _ = writeln!(out, "{k:<16}{w};");
827 }
828 FoamEntry::Str(s) => {
829 indent(out, level);
830 let _ = writeln!(out, "{k:<16}\"{s}\";");
831 }
832 FoamEntry::Tokens(vs) => {
833 indent(out, level);
834 let _ = write!(out, "{k:<16}");
835 for (i, v) in vs.iter().enumerate() {
836 if i > 0 {
837 out.push(' ');
838 }
839 write_value(out, v);
840 }
841 out.push_str(";\n");
842 }
843 FoamEntry::List(items) => {
844 indent(out, level);
845 let _ = write!(out, "{k:<16}");
846 let mut v = String::new();
847 write_value(&mut v, &FoamValue::List(items.clone()));
848 out.push_str(&v);
849 out.push_str(";\n");
850 }
851 FoamEntry::Dimensioned(d) => {
852 indent(out, level);
853 let _ = write!(out, "{k:<16}");
854 write_dimensioned(out, d);
855 out.push_str(";\n");
856 }
857 }
858 }
859}
860
861#[cfg(test)]
866mod tests {
867 use super::*;
868
869 #[test]
870 fn tokenize_glued_flag() {
871 let toks = tokenize("div(phi,U) (0 0 1)");
872 assert_eq!(toks[0].text, "div");
874 assert_eq!(toks[1].text, "(");
875 assert!(toks[1].glued, "`(` after div should be glued");
876 let list_open = toks.iter().rposition(|t| t.text == "(").unwrap();
878 assert!(!toks[list_open].glued);
879 }
880
881 #[test]
882 fn parse_function_keyword() {
883 let f = FoamFile::parse("divSchemes { div(phi,U) Gauss linear; }").unwrap();
884 let ds = f.dict.get_dict("divSchemes").unwrap();
885 match ds.get("div(phi,U)").unwrap() {
886 FoamEntry::Tokens(v) => {
887 assert_eq!(v[0], FoamValue::Word("Gauss".into()));
888 assert_eq!(v[1], FoamValue::Word("linear".into()));
889 }
890 other => panic!("expected Tokens, got {other:?}"),
891 }
892 }
893
894 #[test]
895 fn dimensioned_entry() {
896 let f = FoamFile::parse("nu [0 2 -1 0 0 0 0] 1e-05;").unwrap();
897 match f.dict.get("nu").unwrap() {
898 FoamEntry::Dimensioned(d) => {
899 assert_eq!(d.dims, [0.0, 2.0, -1.0, 0.0, 0.0, 0.0, 0.0]);
900 assert_eq!(d.value, vec![FoamValue::Scalar(1e-5)]);
901 }
902 other => panic!("expected Dimensioned, got {other:?}"),
903 }
904 }
905}