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 crate::error::{IoError, Result};

use super::common::{
    canonical_noble_gas, is_s_orbital, is_zero_base_token, parse_element, parse_f64, parse_i32,
    parse_orbital,
};
use super::types::{ConfigInput, ConfigOccupation, ConfigRecord, ConfigState};
use super::validate::validate_config_input;

pub fn parse_config_inp(text: &str) -> Result<ConfigInput> {
    let records = config_lines(text)
        .map(|line| {
            let tokens = line.text.split_whitespace().collect::<Vec<_>>();
            parse_record(line.line, &tokens)
        })
        .collect::<Result<Vec<_>>>()?;
    let input = ConfigInput { records };
    validate_config_input(&input)?;
    Ok(input)
}

fn parse_record(line: usize, tokens: &[&str]) -> Result<ConfigRecord> {
    if tokens.len() < 3 {
        return Err(IoError::ConfigInpRowWidth {
            line,
            actual: tokens.len(),
            expected: 3,
        });
    }

    let potential_index = parse_i32(line, "potential index", tokens[0])?;
    let element = parse_element(line, tokens[1])?;
    let (noble_gas, mut index) = if is_zero_base_token(tokens[2]) {
        (None, 3)
    } else if let Some(noble_gas) = canonical_noble_gas(tokens[2]) {
        (Some(noble_gas), 3)
    } else {
        (None, 2)
    };
    let mut states = Vec::new();

    while index < tokens.len() {
        let orbital = parse_orbital(line, tokens[index])?;
        index += 1;
        let occupation_count = if is_s_orbital(&orbital) { 1 } else { 2 };
        let mut occupations = Vec::with_capacity(occupation_count);

        for _ in 0..occupation_count {
            let Some(token) = tokens.get(index) else {
                return Err(IoError::ConfigInpMissing {
                    field: "occupation",
                    line,
                });
            };
            let occupation = parse_f64(line, "occupation", token)?;
            index += 1;
            let spin = if tokens
                .get(index)
                .is_some_and(|token| token.eq_ignore_ascii_case("s"))
            {
                index += 1;
                let Some(token) = tokens.get(index) else {
                    return Err(IoError::ConfigInpMissing {
                        field: "spin",
                        line,
                    });
                };
                let spin = parse_f64(line, "spin", token)?;
                index += 1;
                Some(spin)
            } else {
                None
            };
            occupations.push(ConfigOccupation { occupation, spin });
        }

        states.push(ConfigState {
            orbital,
            occupations,
        });
    }

    Ok(ConfigRecord {
        potential_index,
        element,
        noble_gas,
        states,
    })
}

fn config_lines(text: &str) -> impl Iterator<Item = ConfigLine<'_>> {
    text.lines().enumerate().filter_map(|(index, raw)| {
        let line = strip_inline_comment(raw).trim();
        if line.is_empty() || is_comment_line(line) {
            None
        } else {
            Some(ConfigLine {
                line: index + 1,
                text: line,
            })
        }
    })
}

fn strip_inline_comment(line: &str) -> &str {
    let comment_index = line
        .char_indices()
        .find_map(|(index, ch)| matches!(ch, '#' | '!' | '%').then_some(index));
    comment_index.map_or(line, |index| &line[..index])
}

fn is_comment_line(line: &str) -> bool {
    line.chars()
        .next()
        .is_some_and(|ch| matches!(ch, '#' | '!' | '*' | ';' | 'C' | 'c'))
}

struct ConfigLine<'a> {
    line: usize,
    text: &'a str,
}