refeff-io 0.2.0

FEFF file-format readers and writers (feff.inp, .dat/.bin handoffs, PAD encoding) for the refeff FEFF10 port
Documentation
use num_complex::Complex64;

use crate::error::{IoError, Result};
use crate::format::write_fortran_zero_scaled_exp;

pub(super) const DMDW_EGRID_INFO_PATH: &str = "dmdw_Egrid.info";
pub(super) const DMDW_A2F_INFO_PATH: &str = "dmdw_a2f.info";
pub(super) const DMDW_SPECTRAL_INFO_PATH: &str = "dmdw_spectral.info";
pub(super) const DMDW_SELF_ENERGY_DAT_PATH: &str = "dmdw_*SE_a2F.dat";
pub(super) const DMDW_AKW_DAT_PATH: &str = "dmdw_Akw.dat";
pub(super) const DMDW_A2F_INFO_ROW_WIDTH: usize = 2;
pub(super) const DMDW_SELF_ENERGY_ROW_WIDTH: usize = 2;
pub(super) const DMDW_AKW_ROW_WIDTH: usize = 5;

pub(super) fn numeric_values(path: &'static str, line: usize, text: &str) -> Result<Vec<f64>> {
    let mut values = Vec::new();
    for token in text.split_whitespace() {
        if let Some(value) = parse_numeric_token(path, line, token) {
            values.push(value?);
        }
    }
    Ok(values)
}

pub(super) fn parse_single_value(
    path: &'static str,
    line: usize,
    field: &'static str,
    text: &str,
) -> Result<f64> {
    let values = numeric_values(path, line, text)?;
    if values.len() == 1 {
        Ok(values[0])
    } else {
        parse_error(
            path,
            line,
            format!(
                "{field} line has {} numeric value(s), expected 1",
                values.len()
            ),
        )
    }
}

pub(super) fn parse_complex_value(
    path: &'static str,
    line: usize,
    field: &'static str,
    text: &str,
) -> Result<Complex64> {
    let value_text = text
        .rsplit_once('=')
        .map(|(_, value)| value.trim())
        .unwrap_or_else(|| text.trim());
    let normalized = value_text
        .trim_start_matches('(')
        .trim_end_matches(')')
        .replace(',', " ");
    let values = normalized
        .split_whitespace()
        .map(|token| parse_f64(path, line, field, token))
        .collect::<Result<Vec<_>>>()?;

    match values.as_slice() {
        [real] => Ok(Complex64::new(*real, 0.0)),
        [real, imaginary] => Ok(Complex64::new(*real, *imaginary)),
        _ => parse_error(
            path,
            line,
            format!(
                "{field} line has {} numeric value(s), expected 1 or 2",
                values.len()
            ),
        ),
    }
}

pub(super) fn parse_i32_field(
    path: &'static str,
    line: usize,
    field: &'static str,
    text: &str,
) -> Result<i32> {
    let value = parse_single_value(path, line, field, text)?;
    if value.fract() != 0.0 {
        return parse_error(path, line, format!("{field} must be an integer"));
    }
    if value < f64::from(i32::MIN) || value > f64::from(i32::MAX) {
        return parse_error(path, line, format!("{field} is out of range"));
    }
    Ok(value as i32)
}

pub(super) fn parse_two_column_row(
    path: &'static str,
    line: usize,
    text: &str,
    expected_width: usize,
    row_name: &'static str,
) -> Result<(f64, f64)> {
    let tokens = text.split_whitespace().collect::<Vec<_>>();
    if tokens.len() != expected_width {
        return parse_error(
            path,
            line,
            format!(
                "{row_name} row has {} token(s), expected {expected_width}",
                tokens.len()
            ),
        );
    }
    Ok((
        parse_f64(path, line, "first column", tokens[0])?,
        parse_f64(path, line, "second column", tokens[1])?,
    ))
}

pub(super) fn write_fortran_complex_tuple(out: &mut String, value: Complex64) -> std::fmt::Result {
    out.push('(');
    write_fortran_zero_scaled_exp(out, value.re, 20, 10)?;
    out.push(',');
    write_fortran_zero_scaled_exp(out, value.im, 20, 10)?;
    out.push(')');
    Ok(())
}

pub(super) fn is_numeric_token(token: &str) -> bool {
    parse_numeric_token(DMDW_SELF_ENERGY_DAT_PATH, 0, token).is_some_and(|value| value.is_ok())
}

fn parse_numeric_token(path: &'static str, line: usize, token: &str) -> Option<Result<f64>> {
    let normalized = token.replace(['D', 'd'], "E");
    normalized
        .parse::<f64>()
        .map(|value| {
            if value.is_finite() {
                Ok(value)
            } else {
                parse_error(path, line, "numeric value must be finite")
            }
        })
        .ok()
}

pub(super) fn parse_f64(
    path: &'static str,
    line: usize,
    field: &'static str,
    token: &str,
) -> Result<f64> {
    let value = token
        .replace(['D', 'd'], "E")
        .parse::<f64>()
        .map_err(|_| parse_error_value(path, line, format!("invalid {field} value {token:?}")))?;
    if value.is_finite() {
        Ok(value)
    } else {
        Err(parse_error_value(
            path,
            line,
            format!("{field} value must be finite"),
        ))
    }
}

pub(super) fn invalid_data<T>(
    path: &'static str,
    field: &'static str,
    message: impl Into<String>,
) -> Result<T> {
    Err(IoError::Parse {
        path: path.into(),
        line: 0,
        message: format!("{field}: {}", message.into()),
    })
}

pub(super) fn parse_error<T>(
    path: &'static str,
    line: usize,
    message: impl Into<String>,
) -> Result<T> {
    Err(parse_error_value(path, line, message))
}

pub(super) fn parse_error_value(
    path: &'static str,
    line: usize,
    message: impl Into<String>,
) -> IoError {
    IoError::Parse {
        path: path.into(),
        line,
        message: message.into(),
    }
}