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.text.parse::<f64>().map_err(|_| {
551 self.err(format!(
552 "dimension exponent {i} is not a number: `{}`",
553 t.text
554 ))
555 })?;
556 }
557 self.expect("]")?;
558 let mut value = Vec::new();
560 while !matches!(self.peek_text(), Some(";") | None) {
561 value.push(self.parse_leaf()?);
562 }
563 Ok(Dimensioned { dims, value })
564 }
565
566 fn parse_value_sequence(&mut self) -> Result<FoamEntry, IoError> {
569 let mut values = Vec::new();
570 while !matches!(self.peek_text(), Some(";") | None) {
571 values.push(self.parse_leaf()?);
572 }
573 Ok(match values.len() {
574 0 => return Err(self.err("empty entry value")),
575 1 => match values.into_iter().next().unwrap() {
576 FoamValue::Scalar(x) => FoamEntry::Scalar(x),
577 FoamValue::Word(w) => FoamEntry::Word(w),
578 FoamValue::Str(s) => FoamEntry::Str(s),
579 FoamValue::List(l) => FoamEntry::List(l),
580 },
581 _ => FoamEntry::Tokens(values),
582 })
583 }
584
585 fn parse_leaf(&mut self) -> Result<FoamValue, IoError> {
588 let t = self
589 .peek()
590 .ok_or_else(|| self.err("expected a value, found end of input"))?;
591 if t.text == "(" {
592 return self.parse_list();
593 }
594 let t = self.advance().unwrap();
596 if t.quoted {
597 return Ok(FoamValue::Str(t.text.clone()));
598 }
599 let text = t.text.clone();
600 if self.peek().is_some_and(|p| p.text == "(" && p.glued) {
602 let mut w = text;
603 while matches!(self.peek(), Some(p) if p.text == "(" && p.glued) {
604 w.push_str(&self.fold_paren_text());
605 }
606 return Ok(FoamValue::Word(w));
607 }
608 match text.parse::<f64>() {
609 Ok(x) => Ok(FoamValue::Scalar(x)),
610 Err(_) => Ok(FoamValue::Word(text)),
611 }
612 }
613
614 fn parse_list(&mut self) -> Result<FoamValue, IoError> {
617 self.expect("(")?;
618 let mut items = Vec::new();
619 loop {
620 match self.peek_text() {
621 Some(")") => {
622 self.advance();
623 break;
624 }
625 None => return Err(self.err("unexpected end of input inside `( … )`")),
626 _ => items.push(self.parse_leaf()?),
627 }
628 }
629 Ok(FoamValue::List(items))
630 }
631}
632
633fn parse_header(parser: &mut Parser) -> Result<Option<FoamHeader>, IoError> {
636 if parser.peek_text() != Some("FoamFile") {
637 return Ok(None);
638 }
639 parser.advance(); parser.expect("{")?;
641 let mut header = FoamHeader::new();
642 loop {
643 match parser.peek_text() {
644 Some("}") => {
645 parser.advance();
646 break;
647 }
648 None => return Err(parser.err("unexpected end of input inside FoamFile header")),
649 _ => {
650 let kw = parser.parse_keyword()?;
651 let vt = parser
653 .advance()
654 .ok_or_else(|| parser.err("expected a header value"))?;
655 let raw = if vt.quoted {
656 format!("\"{}\"", vt.text)
657 } else {
658 vt.text.clone()
659 };
660 parser.expect(";")?;
661 header.set(&kw, &raw);
662 }
663 }
664 }
665 Ok(Some(header))
666}
667
668#[derive(Debug, Clone, PartialEq)]
675pub struct FoamFile {
676 pub header: Option<FoamHeader>,
678 pub dict: FoamDict,
680}
681
682impl FoamFile {
683 pub fn parse(text: &str) -> Result<Self, IoError> {
685 Self::parse_named(text, "dictionary")
686 }
687
688 pub fn parse_named(text: &str, context: impl Into<String>) -> Result<Self, IoError> {
690 let tokens = tokenize(text);
691 let mut parser = Parser::new(tokens, context);
692 let header = parse_header(&mut parser)?;
693 let dict = parser.parse_dict_body(true)?;
694 Ok(Self { header, dict })
695 }
696
697 pub fn read(path: impl AsRef<Path>) -> Result<Self, IoError> {
699 let path = path.as_ref();
700 let text = std::fs::read_to_string(path)
701 .map_err(|e| IoError::io(path.display().to_string(), e))?;
702 Self::parse_named(&text, path.display().to_string())
703 }
704
705 pub fn to_foam_string(&self) -> String {
707 let mut out = String::new();
708 out.push_str(BANNER);
709 if let Some(h) = &self.header {
710 write_header(&mut out, h);
711 }
712 out.push_str(SEPARATOR);
713 out.push('\n');
714 write_dict_body(&mut out, &self.dict, 0);
715 out.push('\n');
716 out.push_str(FOOTER);
717 out
718 }
719
720 pub fn write(&self, path: impl AsRef<Path>) -> Result<(), IoError> {
722 let path = path.as_ref();
723 std::fs::write(path, self.to_foam_string())
724 .map_err(|e| IoError::io(path.display().to_string(), e))
725 }
726}
727
728pub(crate) const BANNER: &str = "\
732/*--------------------------------*- C++ -*----------------------------------*\\
733| ========= | |
734| \\\\ / F ield | OUTRAM PARK: independent OpenFOAM-format I/O |
735| \\\\ / O peration | (not the official OpenFOAM software) |
736| \\\\ / A nd | |
737| \\\\/ M anipulation | |
738\\*---------------------------------------------------------------------------*/
739";
740
741pub(crate) const SEPARATOR: &str =
742 "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //";
743
744pub(crate) const FOOTER: &str =
745 "// ************************************************************************* //\n";
746
747pub(crate) fn write_header(out: &mut String, header: &FoamHeader) {
749 out.push_str("FoamFile\n{\n");
750 for (k, v) in &header.entries {
751 let _ = writeln!(out, " {k:<12}{v};");
752 }
753 out.push_str("}\n");
754}
755
756pub(crate) fn fmt_scalar(x: f64) -> String {
758 if x.is_finite() && x == x.trunc() && x.abs() < 1e15 {
759 format!("{}", x as i64)
760 } else {
761 format!("{x}")
762 }
763}
764
765fn write_value(out: &mut String, v: &FoamValue) {
766 match v {
767 FoamValue::Scalar(x) => out.push_str(&fmt_scalar(*x)),
768 FoamValue::Word(w) => out.push_str(w),
769 FoamValue::Str(s) => {
770 out.push('"');
771 out.push_str(s);
772 out.push('"');
773 }
774 FoamValue::List(items) => {
775 out.push('(');
776 for (i, it) in items.iter().enumerate() {
777 if i > 0 {
778 out.push(' ');
779 }
780 write_value(out, it);
781 }
782 out.push(')');
783 }
784 }
785}
786
787fn write_dimensioned(out: &mut String, d: &Dimensioned) {
788 out.push('[');
789 for (i, e) in d.dims.iter().enumerate() {
790 if i > 0 {
791 out.push(' ');
792 }
793 out.push_str(&fmt_scalar(*e));
794 }
795 out.push(']');
796 for v in &d.value {
797 out.push(' ');
798 write_value(out, v);
799 }
800}
801
802fn indent(out: &mut String, level: usize) {
803 for _ in 0..level {
804 out.push_str(" ");
805 }
806}
807
808pub(crate) fn write_dict_body(out: &mut String, dict: &FoamDict, level: usize) {
810 for (k, entry) in dict.iter() {
811 match entry {
812 FoamEntry::SubDict(sub) => {
813 indent(out, level);
814 out.push_str(k);
815 out.push('\n');
816 indent(out, level);
817 out.push_str("{\n");
818 write_dict_body(out, sub, level + 1);
819 indent(out, level);
820 out.push_str("}\n");
821 }
822 FoamEntry::Scalar(x) => {
823 indent(out, level);
824 let _ = writeln!(out, "{k:<16}{};", fmt_scalar(*x));
825 }
826 FoamEntry::Word(w) => {
827 indent(out, level);
828 let _ = writeln!(out, "{k:<16}{w};");
829 }
830 FoamEntry::Str(s) => {
831 indent(out, level);
832 let _ = writeln!(out, "{k:<16}\"{s}\";");
833 }
834 FoamEntry::Tokens(vs) => {
835 indent(out, level);
836 let _ = write!(out, "{k:<16}");
837 for (i, v) in vs.iter().enumerate() {
838 if i > 0 {
839 out.push(' ');
840 }
841 write_value(out, v);
842 }
843 out.push_str(";\n");
844 }
845 FoamEntry::List(items) => {
846 indent(out, level);
847 let _ = write!(out, "{k:<16}");
848 let mut v = String::new();
849 write_value(&mut v, &FoamValue::List(items.clone()));
850 out.push_str(&v);
851 out.push_str(";\n");
852 }
853 FoamEntry::Dimensioned(d) => {
854 indent(out, level);
855 let _ = write!(out, "{k:<16}");
856 write_dimensioned(out, d);
857 out.push_str(";\n");
858 }
859 }
860 }
861}
862
863#[cfg(test)]
868mod tests {
869 use super::*;
870
871 #[test]
872 fn tokenize_glued_flag() {
873 let toks = tokenize("div(phi,U) (0 0 1)");
874 assert_eq!(toks[0].text, "div");
876 assert_eq!(toks[1].text, "(");
877 assert!(toks[1].glued, "`(` after div should be glued");
878 let list_open = toks.iter().rposition(|t| t.text == "(").unwrap();
880 assert!(!toks[list_open].glued);
881 }
882
883 #[test]
884 fn parse_function_keyword() {
885 let f = FoamFile::parse("divSchemes { div(phi,U) Gauss linear; }").unwrap();
886 let ds = f.dict.get_dict("divSchemes").unwrap();
887 match ds.get("div(phi,U)").unwrap() {
888 FoamEntry::Tokens(v) => {
889 assert_eq!(v[0], FoamValue::Word("Gauss".into()));
890 assert_eq!(v[1], FoamValue::Word("linear".into()));
891 }
892 other => panic!("expected Tokens, got {other:?}"),
893 }
894 }
895
896 #[test]
897 fn dimensioned_entry() {
898 let f = FoamFile::parse("nu [0 2 -1 0 0 0 0] 1e-05;").unwrap();
899 match f.dict.get("nu").unwrap() {
900 FoamEntry::Dimensioned(d) => {
901 assert_eq!(d.dims, [0.0, 2.0, -1.0, 0.0, 0.0, 0.0, 0.0]);
902 assert_eq!(d.value, vec![FoamValue::Scalar(1e-5)]);
903 }
904 other => panic!("expected Dimensioned, got {other:?}"),
905 }
906 }
907}