use std::fmt::Write as _;
use std::path::Path;
use super::IoError;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Token {
pub text: String,
pub glued: bool,
pub quoted: bool,
}
pub(crate) fn tokenize(text: &str) -> Vec<Token> {
let bytes = text.as_bytes();
let n = bytes.len();
let mut i = 0;
let mut tokens: Vec<Token> = Vec::new();
let mut pending_ws = true;
let mut word = String::new();
let mut word_glued = true;
let is_delim = |c: u8| matches!(c, b'(' | b')' | b'{' | b'}' | b';' | b'[' | b']');
macro_rules! flush_word {
() => {
if !word.is_empty() {
tokens.push(Token {
text: std::mem::take(&mut word),
glued: word_glued,
quoted: false,
});
}
};
}
while i < n {
let c = bytes[i];
if i + 1 < n && c == b'/' && bytes[i + 1] == b'/' {
flush_word!();
while i < n && bytes[i] != b'\n' {
i += 1;
}
pending_ws = true;
} else if i + 1 < n && c == b'/' && bytes[i + 1] == b'*' {
flush_word!();
i += 2;
while i + 1 < n && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i += 2;
pending_ws = true;
} else if c == b'"' {
flush_word!();
i += 1;
let mut s = String::new();
while i < n && bytes[i] != b'"' {
s.push(bytes[i] as char);
i += 1;
}
i += 1; tokens.push(Token {
text: s,
glued: !pending_ws,
quoted: true,
});
pending_ws = false;
} else if is_delim(c) {
flush_word!();
tokens.push(Token {
text: (c as char).to_string(),
glued: !pending_ws,
quoted: false,
});
pending_ws = false;
i += 1;
} else if (c as char).is_whitespace() {
flush_word!();
pending_ws = true;
i += 1;
} else {
if word.is_empty() {
word_glued = !pending_ws;
}
word.push(c as char);
pending_ws = false;
i += 1;
}
}
flush_word!();
tokens
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dimensioned {
pub dims: [f64; 7],
pub value: Vec<FoamValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FoamValue {
Scalar(f64),
Word(String),
Str(String),
List(Vec<FoamValue>),
}
impl FoamValue {
pub fn as_vector3(&self) -> Option<crate::primitives::Vector3> {
if let FoamValue::List(v) = self {
if let [FoamValue::Scalar(x), FoamValue::Scalar(y), FoamValue::Scalar(z)] = v.as_slice()
{
return Some(crate::primitives::Vector3::new(*x, *y, *z));
}
}
None
}
pub fn as_scalar(&self) -> Option<f64> {
match self {
FoamValue::Scalar(x) => Some(*x),
_ => None,
}
}
pub fn as_word(&self) -> Option<&str> {
match self {
FoamValue::Word(w) => Some(w.as_str()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FoamEntry {
Scalar(f64),
Word(String),
Str(String),
Tokens(Vec<FoamValue>),
List(Vec<FoamValue>),
Dimensioned(Dimensioned),
SubDict(FoamDict),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FoamDict {
entries: Vec<(String, FoamEntry)>,
}
impl FoamDict {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, keyword: impl Into<String>, entry: FoamEntry) -> &mut Self {
let keyword = keyword.into();
if let Some(slot) = self.entries.iter_mut().find(|(k, _)| *k == keyword) {
slot.1 = entry;
} else {
self.entries.push((keyword, entry));
}
self
}
pub fn get(&self, keyword: &str) -> Option<&FoamEntry> {
self.entries
.iter()
.find(|(k, _)| k == keyword)
.map(|(_, v)| v)
}
pub fn get_dict(&self, keyword: &str) -> Option<&FoamDict> {
match self.get(keyword) {
Some(FoamEntry::SubDict(d)) => Some(d),
_ => None,
}
}
pub fn get_scalar(&self, keyword: &str) -> Option<f64> {
match self.get(keyword) {
Some(FoamEntry::Scalar(x)) => Some(*x),
_ => None,
}
}
pub fn get_word(&self, keyword: &str) -> Option<&str> {
match self.get(keyword) {
Some(FoamEntry::Word(w)) => Some(w.as_str()),
_ => None,
}
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &FoamEntry)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FoamHeader {
entries: Vec<(String, String)>,
}
impl FoamHeader {
pub fn new() -> Self {
Self::default()
}
pub fn standard(class: &str, object: &str) -> Self {
let mut h = Self::new();
h.set("version", "2.0");
h.set("format", "ascii");
h.set("class", class);
h.set("object", object);
h
}
pub fn standard_with_location(class: &str, location: &str, object: &str) -> Self {
let mut h = Self::new();
h.set("version", "2.0");
h.set("format", "ascii");
h.set("class", class);
h.set("location", &format!("\"{location}\""));
h.set("object", object);
h
}
pub fn set(&mut self, keyword: &str, value: &str) -> &mut Self {
if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == keyword) {
slot.1 = value.to_string();
} else {
self.entries.push((keyword.to_string(), value.to_string()));
}
self
}
pub fn get(&self, keyword: &str) -> Option<&str> {
self.entries
.iter()
.find(|(k, _)| k == keyword)
.map(|(_, v)| v.as_str())
}
pub fn class(&self) -> Option<&str> {
self.get("class")
}
pub fn object(&self) -> Option<&str> {
self.get("object")
}
}
struct Parser {
tokens: Vec<Token>,
pos: usize,
context: String,
}
impl Parser {
fn new(tokens: Vec<Token>, context: impl Into<String>) -> Self {
Self {
tokens,
pos: 0,
context: context.into(),
}
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos)
}
fn peek_text(&self) -> Option<&str> {
self.tokens.get(self.pos).map(|t| t.text.as_str())
}
fn advance(&mut self) -> Option<Token> {
let t = self.tokens.get(self.pos).cloned();
if t.is_some() {
self.pos += 1;
}
t
}
fn err(&self, message: impl Into<String>) -> IoError {
IoError::parse(self.context.clone(), message.into())
}
fn expect(&mut self, tok: &str) -> Result<(), IoError> {
match self.advance() {
Some(t) if t.text == tok => Ok(()),
Some(t) => Err(self.err(format!("expected `{tok}`, found `{}`", t.text))),
None => Err(self.err(format!("expected `{tok}`, found end of input"))),
}
}
fn parse_keyword(&mut self) -> Result<String, IoError> {
let first = self
.advance()
.ok_or_else(|| self.err("expected a keyword, found end of input"))?;
if matches!(first.text.as_str(), "(" | ")" | "{" | "}" | ";" | "[" | "]") {
return Err(self.err(format!("expected a keyword, found `{}`", first.text)));
}
let mut kw = first.text.clone();
while matches!(self.peek(), Some(t) if t.text == "(" && t.glued) {
kw.push_str(&self.fold_paren_text());
}
Ok(kw)
}
fn fold_paren_text(&mut self) -> String {
let mut out = String::new();
self.advance();
out.push('(');
let mut depth = 1;
while depth > 0 {
let Some(t) = self.advance() else { break };
match t.text.as_str() {
"(" => {
depth += 1;
out.push('(');
}
")" => {
depth -= 1;
out.push(')');
}
other => out.push_str(other),
}
}
out
}
fn parse_dict_body(&mut self, top_level: bool) -> Result<FoamDict, IoError> {
let mut dict = FoamDict::new();
loop {
match self.peek_text() {
None => {
if top_level {
break;
}
return Err(self.err("unexpected end of input inside `{ … }`"));
}
Some("}") => {
if top_level {
return Err(self.err("unexpected `}` at top level"));
}
self.advance();
break;
}
Some(";") => {
self.advance(); }
_ => {
let (kw, entry) = self.parse_entry()?;
dict.insert(kw, entry);
}
}
}
Ok(dict)
}
fn parse_entry(&mut self) -> Result<(String, FoamEntry), IoError> {
let keyword = self.parse_keyword()?;
match self.peek_text() {
Some("{") => {
self.advance();
let sub = self.parse_dict_body(false)?;
if self.peek_text() == Some(";") {
self.advance();
}
Ok((keyword, FoamEntry::SubDict(sub)))
}
Some("[") => {
let dim = self.parse_dimensioned()?;
self.expect(";")?;
Ok((keyword, FoamEntry::Dimensioned(dim)))
}
_ => {
let entry = self.parse_value_sequence()?;
self.expect(";")?;
Ok((keyword, entry))
}
}
}
fn parse_dimensioned(&mut self) -> Result<Dimensioned, IoError> {
self.expect("[")?;
let mut dims = [0.0f64; 7];
for (i, slot) in dims.iter_mut().enumerate() {
let t = self
.advance()
.ok_or_else(|| self.err("unexpected end of input inside `[ … ]`"))?;
*slot = t.text.parse::<f64>().map_err(|_| {
self.err(format!(
"dimension exponent {i} is not a number: `{}`",
t.text
))
})?;
}
self.expect("]")?;
let mut value = Vec::new();
while !matches!(self.peek_text(), Some(";") | None) {
value.push(self.parse_leaf()?);
}
Ok(Dimensioned { dims, value })
}
fn parse_value_sequence(&mut self) -> Result<FoamEntry, IoError> {
let mut values = Vec::new();
while !matches!(self.peek_text(), Some(";") | None) {
values.push(self.parse_leaf()?);
}
Ok(match values.len() {
0 => return Err(self.err("empty entry value")),
1 => match values.into_iter().next().unwrap() {
FoamValue::Scalar(x) => FoamEntry::Scalar(x),
FoamValue::Word(w) => FoamEntry::Word(w),
FoamValue::Str(s) => FoamEntry::Str(s),
FoamValue::List(l) => FoamEntry::List(l),
},
_ => FoamEntry::Tokens(values),
})
}
fn parse_leaf(&mut self) -> Result<FoamValue, IoError> {
let t = self
.peek()
.ok_or_else(|| self.err("expected a value, found end of input"))?;
if t.text == "(" {
return self.parse_list();
}
let t = self.advance().unwrap();
if t.quoted {
return Ok(FoamValue::Str(t.text.clone()));
}
let text = t.text.clone();
if self.peek().is_some_and(|p| p.text == "(" && p.glued) {
let mut w = text;
while matches!(self.peek(), Some(p) if p.text == "(" && p.glued) {
w.push_str(&self.fold_paren_text());
}
return Ok(FoamValue::Word(w));
}
match text.parse::<f64>() {
Ok(x) => Ok(FoamValue::Scalar(x)),
Err(_) => Ok(FoamValue::Word(text)),
}
}
fn parse_list(&mut self) -> Result<FoamValue, IoError> {
self.expect("(")?;
let mut items = Vec::new();
loop {
match self.peek_text() {
Some(")") => {
self.advance();
break;
}
None => return Err(self.err("unexpected end of input inside `( … )`")),
_ => items.push(self.parse_leaf()?),
}
}
Ok(FoamValue::List(items))
}
}
fn parse_header(parser: &mut Parser) -> Result<Option<FoamHeader>, IoError> {
if parser.peek_text() != Some("FoamFile") {
return Ok(None);
}
parser.advance(); parser.expect("{")?;
let mut header = FoamHeader::new();
loop {
match parser.peek_text() {
Some("}") => {
parser.advance();
break;
}
None => return Err(parser.err("unexpected end of input inside FoamFile header")),
_ => {
let kw = parser.parse_keyword()?;
let vt = parser
.advance()
.ok_or_else(|| parser.err("expected a header value"))?;
let raw = if vt.quoted {
format!("\"{}\"", vt.text)
} else {
vt.text.clone()
};
parser.expect(";")?;
header.set(&kw, &raw);
}
}
}
Ok(Some(header))
}
#[derive(Debug, Clone, PartialEq)]
pub struct FoamFile {
pub header: Option<FoamHeader>,
pub dict: FoamDict,
}
impl FoamFile {
pub fn parse(text: &str) -> Result<Self, IoError> {
Self::parse_named(text, "dictionary")
}
pub fn parse_named(text: &str, context: impl Into<String>) -> Result<Self, IoError> {
let tokens = tokenize(text);
let mut parser = Parser::new(tokens, context);
let header = parse_header(&mut parser)?;
let dict = parser.parse_dict_body(true)?;
Ok(Self { header, dict })
}
pub fn read(path: impl AsRef<Path>) -> Result<Self, IoError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| IoError::io(path.display().to_string(), e))?;
Self::parse_named(&text, path.display().to_string())
}
pub fn to_foam_string(&self) -> String {
let mut out = String::new();
out.push_str(BANNER);
if let Some(h) = &self.header {
write_header(&mut out, h);
}
out.push_str(SEPARATOR);
out.push('\n');
write_dict_body(&mut out, &self.dict, 0);
out.push('\n');
out.push_str(FOOTER);
out
}
pub fn write(&self, path: impl AsRef<Path>) -> Result<(), IoError> {
let path = path.as_ref();
std::fs::write(path, self.to_foam_string())
.map_err(|e| IoError::io(path.display().to_string(), e))
}
}
pub(crate) const BANNER: &str = "\
/*--------------------------------*- C++ -*----------------------------------*\\
| ========= | |
| \\\\ / F ield | OUTRAM PARK: independent OpenFOAM-format I/O |
| \\\\ / O peration | (not the official OpenFOAM software) |
| \\\\ / A nd | |
| \\\\/ M anipulation | |
\\*---------------------------------------------------------------------------*/
";
pub(crate) const SEPARATOR: &str =
"// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //";
pub(crate) const FOOTER: &str =
"// ************************************************************************* //\n";
pub(crate) fn write_header(out: &mut String, header: &FoamHeader) {
out.push_str("FoamFile\n{\n");
for (k, v) in &header.entries {
let _ = writeln!(out, " {k:<12}{v};");
}
out.push_str("}\n");
}
pub(crate) fn fmt_scalar(x: f64) -> String {
if x.is_finite() && x == x.trunc() && x.abs() < 1e15 {
format!("{}", x as i64)
} else {
format!("{x}")
}
}
fn write_value(out: &mut String, v: &FoamValue) {
match v {
FoamValue::Scalar(x) => out.push_str(&fmt_scalar(*x)),
FoamValue::Word(w) => out.push_str(w),
FoamValue::Str(s) => {
out.push('"');
out.push_str(s);
out.push('"');
}
FoamValue::List(items) => {
out.push('(');
for (i, it) in items.iter().enumerate() {
if i > 0 {
out.push(' ');
}
write_value(out, it);
}
out.push(')');
}
}
}
fn write_dimensioned(out: &mut String, d: &Dimensioned) {
out.push('[');
for (i, e) in d.dims.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(&fmt_scalar(*e));
}
out.push(']');
for v in &d.value {
out.push(' ');
write_value(out, v);
}
}
fn indent(out: &mut String, level: usize) {
for _ in 0..level {
out.push_str(" ");
}
}
pub(crate) fn write_dict_body(out: &mut String, dict: &FoamDict, level: usize) {
for (k, entry) in dict.iter() {
match entry {
FoamEntry::SubDict(sub) => {
indent(out, level);
out.push_str(k);
out.push('\n');
indent(out, level);
out.push_str("{\n");
write_dict_body(out, sub, level + 1);
indent(out, level);
out.push_str("}\n");
}
FoamEntry::Scalar(x) => {
indent(out, level);
let _ = writeln!(out, "{k:<16}{};", fmt_scalar(*x));
}
FoamEntry::Word(w) => {
indent(out, level);
let _ = writeln!(out, "{k:<16}{w};");
}
FoamEntry::Str(s) => {
indent(out, level);
let _ = writeln!(out, "{k:<16}\"{s}\";");
}
FoamEntry::Tokens(vs) => {
indent(out, level);
let _ = write!(out, "{k:<16}");
for (i, v) in vs.iter().enumerate() {
if i > 0 {
out.push(' ');
}
write_value(out, v);
}
out.push_str(";\n");
}
FoamEntry::List(items) => {
indent(out, level);
let _ = write!(out, "{k:<16}");
let mut v = String::new();
write_value(&mut v, &FoamValue::List(items.clone()));
out.push_str(&v);
out.push_str(";\n");
}
FoamEntry::Dimensioned(d) => {
indent(out, level);
let _ = write!(out, "{k:<16}");
write_dimensioned(out, d);
out.push_str(";\n");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokenize_glued_flag() {
let toks = tokenize("div(phi,U) (0 0 1)");
assert_eq!(toks[0].text, "div");
assert_eq!(toks[1].text, "(");
assert!(toks[1].glued, "`(` after div should be glued");
let list_open = toks.iter().rposition(|t| t.text == "(").unwrap();
assert!(!toks[list_open].glued);
}
#[test]
fn parse_function_keyword() {
let f = FoamFile::parse("divSchemes { div(phi,U) Gauss linear; }").unwrap();
let ds = f.dict.get_dict("divSchemes").unwrap();
match ds.get("div(phi,U)").unwrap() {
FoamEntry::Tokens(v) => {
assert_eq!(v[0], FoamValue::Word("Gauss".into()));
assert_eq!(v[1], FoamValue::Word("linear".into()));
}
other => panic!("expected Tokens, got {other:?}"),
}
}
#[test]
fn dimensioned_entry() {
let f = FoamFile::parse("nu [0 2 -1 0 0 0 0] 1e-05;").unwrap();
match f.dict.get("nu").unwrap() {
FoamEntry::Dimensioned(d) => {
assert_eq!(d.dims, [0.0, 2.0, -1.0, 0.0, 0.0, 0.0, 0.0]);
assert_eq!(d.value, vec![FoamValue::Scalar(1e-5)]);
}
other => panic!("expected Dimensioned, got {other:?}"),
}
}
}