Now I want you to perform code transformation from python to rust.

# Description

## Fields and structs

```rust
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct BseBasisMinimal {
    pub molssi_bse_schema: BseMolssiBseSchema,
    #[serde(serialize_with = "ordered_i32_map")]
    pub elements: HashMap<String, BseBasisElement>,
    pub function_types: Vec<String>,
    pub name: String,
    pub description: String,
}

#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct BseBasisElement {
    pub references: Vec<BseBasisReference>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub electron_shells: Option<Vec<BseElectronShell>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ecp_potentials: Option<Vec<BseEcpPotential>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ecp_electrons: Option<i32>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BseElectronShell {
    pub function_type: String,
    pub region: String,
    pub angular_momentum: Vec<i32>,
    pub exponents: Vec<String>,
    pub coefficients: Vec<Vec<String>>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BseEcpPotential {
    pub angular_momentum: Vec<i32>,
    pub coefficients: Vec<Vec<String>>,
    pub ecp_type: String,
    pub r_exponents: Vec<i32>,
    pub gaussian_exponents: Vec<String>,
}
```

## Reader helpers definitions

This is signature in Python

```python
# helpers.py, signatures

floating_re_str = r'[-+]?\d*\.\d*(?:[dDeE][-+]?\d+)?'
floating_re = re.compile(floating_re_str)
floating_only_re = re.compile('^' + floating_re_str + '$')
integer_re_str = r'[-+]?\d+'
integer_re = re.compile(integer_re_str)
integer_only_re = re.compile('^' + integer_re_str + '$')
# Basis set name: require at least one alphabetic ASCII character; allow alphabetic characters, digits, and the characters +, -, *, (, ), [ and ]
basis_name_re_str = r'\d*[a-zA-Z][a-zA-Z0-9\-\+\*\(\)\[\]]*'
basis_name_re = re.compile(basis_name_re_str)

def is_floating(s):
    '''Tests if a string is a floating point number'''

def is_integer(s):
    '''Tests if a string is an integer'''

def replace_d(s):
    '''Replaces fortran-style 'D' with 'E'''

def potential_am_list(max_am):
    '''Creates a canonical list of AM for use with ECP potentials'''

def chunk_list(lst, rows, cols):
    '''Turns a list into a matrix of the given dimensions'''

def remove_expected_line(lines, expected='', position=0):
    '''Tests the first element of the list to see if it is an expected string, and removes it'''

def parse_line_regex(rex, line, description=None, convert_int=False):

def parse_line_regex_dict(rex, line, description=None, convert_int=False):

def partition_lines(lines,
                    condition,
                    before=0,
                    min_after=None,
                    min_blocks=None,
                    max_blocks=None,
                    min_size=1,
                    include_match=True):
    '''Partition a list of lines based on some condition'''

def read_n_floats(lines, n_numbers, convert=False, split=r'\s+'):
    '''Reads in a number of space-separated floating-point numbers'''

def read_all_floats(lines, convert=False, split=r'\s+'):
    '''Reads in all floats on all lines'''

def read_n_integers(lines, n_ints, convert=False, split=r'\s+'):
    '''Reads in a number of space-separated integers'''

def parse_fixed_matrix(lines, rows, cols):
    '''Parses a simple matrix of numbers with a predefined number of rows/columns'''

def parse_matrix(lines, rows=None, cols=None, split=r'\s+'):
    '''Parses a simple matrix of numbers'''

def parse_primitive_matrix(lines, nprim=None, ngen=None, split=r'\s+'):
    '''Parses a matrix/table of exponents and coefficients'''

def parse_ecp_table(lines, order=['r_exp', 'g_exp', 'coeff'], split=r'\s+'):

def prune_lines(lines, skipchars='', prune_blank=True, strip_end_blanks=True):
    '''Remove comment and blank lines'''

def remove_block(lines, start_re, end_re):
    '''Removes a block of data from the lines of text'''
```

The rust counterparts are

```rust
// helpers.rs

lazy_static::lazy_static! {
    pub static ref FLOATING_RE: Regex = Regex::new(r"[-+]?\d*\.\d*(?:[dDeE][-+]?\d+)?").unwrap();
    pub static ref FLOATING_ONLY_RE: Regex = Regex::new(r"^[-+]?\d*\.\d*(?:[dDeE][-+]?\d+)?$").unwrap();
    pub static ref INTEGER_RE: Regex = Regex::new(r"[-+]?\d+").unwrap();
    pub static ref INTEGER_ONLY_RE: Regex = Regex::new(r"^[-+]?\d+$").unwrap();
    pub static ref BASIS_NAME_RE: Regex = Regex::new(r"\d*[a-zA-Z][a-zA-Z0-9\-\+\*\(\)\[\]]*").unwrap();
    pub static ref SPACES_RE: Regex = Regex::new(r"\s+").unwrap();
}

pub(crate) fn is_floating(s: &str) -> bool
pub(crate) fn is_integer(s: &str) -> bool
pub(crate) fn replace_d(s: &str) -> String
pub(crate) fn potential_am_list(max_am: i32) -> Vec<i32>
pub(crate) fn chunk_list<T: Clone>(lst: &[T], rows: usize, cols: usize) -> Result<Vec<Vec<T>>, BseError>
pub(crate) fn remove_expected_line(lines: &[String], expected: &str, position: isize) -> Result<Vec<String>, BseError>
pub fn parse_line_regex(rex: &Regex, line: &str, description: &str) -> Result<Vec<String>, BseError>
pub fn parse_line_regex_dict(rex: &Regex, line: &str, description: &str) -> Result<HashMap<String, String>, BseError>
pub fn partition_lines(
    lines: &[String],
    condition: impl Fn(&str) -> bool,
    before: usize,
    min_after: Option<usize>,
    min_blocks: Option<usize>,
    max_blocks: Option<usize>,
    min_size: usize,
    include_match: bool,
) -> Result<Vec<Vec<String>>, BseError>
pub fn read_n_floats(
    lines: &[String],
    n_numbers: usize,
    split_re: Option<&Regex>,
) -> Result<(Vec<String>, Vec<String>), BseError>
pub fn read_all_floats(lines: &[String], split_re: Option<&Regex>) -> Result<Vec<String>, BseError>
pub fn read_n_integers(
    lines: &[String],
    n_ints: usize,
    split_re: Option<&Regex>,
) -> Result<(Vec<String>, Vec<String>), BseError>
pub fn parse_fixed_matrix(
    lines: &[String],
    rows: usize,
    cols: usize,
    split_re: Option<&Regex>,
) -> Result<(Vec<Vec<String>>, Vec<String>), BseError>
pub fn parse_matrix(
    lines: &[String],
    rows: Option<usize>,
    cols: Option<usize>,
    split_re: Option<&Regex>,
) -> Result<Vec<Vec<String>>, BseError>
pub fn parse_primitive_matrix(
    lines: &[String],
    nprim: Option<usize>,
    ngen: Option<usize>,
    split_re: Option<&Regex>,
) -> Result<(Vec<String>, Vec<Vec<String>>), BseError>
pub struct ReaderECP {
    pub r_exp: Vec<i32>,
    pub g_exp: Vec<String>,
    pub coeff: Vec<Vec<String>>,
}
pub fn parse_ecp_table(lines: &[String], order: &[&str], split_re: Option<&Regex>) -> Result<ReaderECP, BseError>
pub fn prune_lines(lines: &[String], skipchars: &str, prune_blank: bool, strip_end_blanks: bool) -> Vec<String>
pub fn remove_block(
    lines: &[String],
    start_re: &Regex,
    end_re: &Regex,
) -> Result<(Vec<String>, Vec<String>), BseError>
```

## Additional notes

- Most functions/modules have already imported, including regex, itertools, etc.
- In the following example, though main logic is translated from python, and the result is verified to be correct; however, the code in rust may not be exactly translated from python line by line. Some very minor code refactorization occurs.

# Example of translation: NWChem

## Example of python to be translated

```python
'''
Reader for the NWChem format
'''

import re
from .. import lut, manip
from . import helpers

am_line_re = re.compile(r'^([A-Za-z]+)\s+([A-Za-z]+)$')
nelec_re = re.compile(r'^([a-z]+)\s+nelec\s+(\d+)$', flags=re.IGNORECASE)


def _parse_electron_lines(basis_lines, bs_data):
    ''' Parses lines representing all the electron shells for all elements

    Resulting information is stored in bs_data
    '''

    # Remove 'end' from the lines (if they exist)
    # They may exist when this is called from other readers
    basis_lines = [x for x in basis_lines if x.lower() != 'end']

    # Basis entry needs to start with 'basis'
    if not basis_lines[0].lower().startswith('basis'):
        raise RuntimeError("Basis entry must start with 'basis'")

    # Is the basis set spherical or cartesian?
    am_type = 'cartesian' if basis_lines[0].lower().find('spherical') == -1 else 'spherical'

    # Start at index 1 in order to strip of the first line ('BASIS AO PRINT' or something)
    shell_blocks = helpers.partition_lines(basis_lines[1:], lambda x: x[0].isalpha(), min_size=2)

    for sh_lines in shell_blocks:
        element_sym, shell_am = helpers.parse_line_regex(am_line_re, sh_lines[0], "Element sym, shell am")
        shell_am = lut.amchar_to_int(shell_am)

        element_Z = lut.element_Z_from_sym(element_sym, as_str=True)
        element_data = manip.create_element_data(bs_data, element_Z, 'electron_shells', key_exist_ok=True)

        func_type = lut.function_type_from_am(shell_am, 'gto', am_type)

        # How many columns of coefficients do we have?
        # Only if this is a fused shell do we know
        ngen = len(shell_am) if len(shell_am) > 1 else None

        exponents, coefficients = helpers.parse_primitive_matrix(sh_lines[1:], ngen=ngen)

        shell = {
            'function_type': func_type,
            'region': '',
            'angular_momentum': shell_am,
            'exponents': exponents,
            'coefficients': coefficients
        }

        element_data['electron_shells'].append(shell)


def _parse_ecp_lines(basis_lines, bs_data):
    ''' Parses lines representing all the ECP potentials for all elements

    Resulting information is stored in bs_data
    '''

    # Remove 'end' from the lines (if they exist)
    # They may exist when this is called from other readers
    basis_lines = [x for x in basis_lines if x.lower() != 'end']

    # Start at index 1 in order to strip of the first line ('ECP' or something)
    # This splits out based on starting with an alpha character. A block can be either
    #   a potential or the nelec line
    ecp_blocks = helpers.partition_lines(basis_lines[1:], lambda x: x[0].isalpha())

    for pot_lines in ecp_blocks:
        # Check if this is the nelec line
        if len(pot_lines) == 1:
            # Check for this and give a better error message
            if not nelec_re.match(pot_lines[0]):
                raise RuntimeError("Unknown block with single line. Perhaps it's an empty block? Line: " +
                                   pot_lines[0])

            element_sym, n_elec = helpers.parse_line_regex(nelec_re, pot_lines[0].lower(), "ECP: Element sym, nelec")

            element_Z = lut.element_Z_from_sym(element_sym, as_str=True)
            element_data = manip.create_element_data(bs_data, element_Z, 'ecp_electrons', create=int)
            element_data['ecp_electrons'] = n_elec
        else:
            element_sym, pot_am = helpers.parse_line_regex(am_line_re, pot_lines[0], "ECP: Element sym, pot AM")

            element_Z = lut.element_Z_from_sym(element_sym, as_str=True)
            element_data = manip.create_element_data(bs_data, element_Z, 'ecp_potentials', key_exist_ok=True)

            # See if this is the 'ul' angular momentum
            if pot_am.lower() == 'ul':
                pot_am = []  # Placeholder - leave for later
            else:
                pot_am = lut.amchar_to_int(pot_am)

            ecp_data = helpers.parse_ecp_table(pot_lines[1:])
            ecp_pot = {
                'angular_momentum': pot_am,
                'ecp_type': 'scalar_ecp',
                'r_exponents': ecp_data['r_exp'],
                'gaussian_exponents': ecp_data['g_exp'],
                'coefficients': ecp_data['coeff']
            }

            element_data['ecp_potentials'].append(ecp_pot)

    # Fix ecp angular momentum now that everything has been read
    # Specifically, we can set the 'ul' potential to be the max am + 1
    for el, v in bs_data.items():
        if 'ecp_potentials' not in v:
            continue

        all_ecp_am = []
        for x in v['ecp_potentials']:
            all_ecp_am.extend(x['angular_momentum'])

        max_ecp_am = max(all_ecp_am)

        for s in v['ecp_potentials']:
            if not s['angular_momentum']:
                s['angular_momentum'] = [max_ecp_am + 1]

    # Make sure the number of electrons replaced by the ECP was specified for all elements
    for el, v in bs_data.items():
        if 'ecp_potentials' in v and 'ecp_electrons' not in v:
            raise RuntimeError("Number of ECP electrons not specified for element {}".format(el))


def read_nwchem(basis_lines):
    '''Reads NWChem-formatted file data and converts it to a dictionary with the
       usual BSE fields

       Note that the nwchem format does not store all the fields we
       have, so some fields are left blank
    '''

    basis_lines = helpers.prune_lines(basis_lines, '#')

    bs_data = {}
    other_data = {}

    # split into basis and ecp
    basis_sections = helpers.partition_lines(basis_lines,
                                             lambda x: x.lower() == 'end',
                                             min_blocks=1,
                                             max_blocks=2,
                                             include_match=False)

    for s in basis_sections:
        if s[0].lower().startswith('basis'):
            _parse_electron_lines(s, bs_data)
        elif s[0].lower().startswith('ecp'):
            _parse_ecp_lines(s, bs_data)
        else:
            raise RuntimeError("Unknown section: " + s[0])

    return bs_data, other_data
```

## Example of code that has been translated to rust

```rust
//! Reader for the NWChem format

use crate::prelude::*;
use crate::readers::helpers;

lazy_static::lazy_static! {
    static ref AM_LINE_RE: Regex = Regex::new(r"^([A-Za-z]+)\s+([A-Za-z]+)$").unwrap();
    static ref NELEC_RE: Regex = RegexBuilder::new(r"^([a-z]+)\s+nelec\s+(\d+)$").case_insensitive(true).build().unwrap();
}

/// Parses lines representing all the electron shells for all elements.
fn parse_electron_lines(
    elements: &mut HashMap<String, BseBasisElement>,
    basis_lines: &[String],
) -> Result<(), BseError> {
    // assumes that basis_lines are already trimmed

    // Remove 'end' from the lines (if they exist)
    // They may exist when this is called from other readers
    let basis_lines = basis_lines.iter().filter(|line| !line.eq_ignore_ascii_case("end")).cloned().collect_vec();

    // Basis entry needs to start with 'basis'
    if !basis_lines.first().is_some_and(|line| line.to_lowercase().starts_with("basis")) {
        return bse_raise!(ValueError, "Basis entry must start with 'basis'");
    }

    // Is the basis set spherical or cartesian?
    let am_type = if basis_lines[0].to_lowercase().contains("spherical") { "spherical" } else { "cartesian" };

    // Start at index 1 in order to strip of the first line ('BASIS AO PRINT' or
    // something)
    let shell_blocks = helpers::partition_lines(
        &basis_lines[1..],
        |x| x.chars().next().is_some_and(|c| c.is_alphabetic()),
        0,
        None,
        None,
        None,
        2,
        true,
    )?;

    for shl_lines in shell_blocks.iter() {
        let parsed = helpers::parse_line_regex(&AM_LINE_RE, shl_lines.first().unwrap(), "Element sym, shell am")?;
        let element_Z = lut::element_Z_from_sym(&parsed[0])
            .map_or(bse_raise!(ValueError, "Unknown element symbol: {}", parsed[0]), Ok)?;
        let shell_am = lut::amchar_to_int(&parsed[1], HIK)
            .map_or(bse_raise!(ValueError, "Unknown angular momentum: {}", parsed[1]), Ok)?;

        let function_type = lut::function_type_from_am(&shell_am, "gto", am_type);

        // How many columns of coefficients do we have?
        // Only if this is a fused shell do we know.
        let ngen = if shell_am.len() > 1 { Some(shell_am.len()) } else { None };
        let (exponents, coefficients) = helpers::parse_primitive_matrix(&shl_lines[1..], ngen, None, None)?;

        let shell = BseElectronShell {
            function_type,
            region: "".to_string(),
            angular_momentum: shell_am,
            exponents,
            coefficients,
        };

        elements.entry(element_Z.to_string()).or_default().electron_shells.get_or_insert_with(Default::default).push(shell);
    }

    Ok(())
}

/// Parses lines representing all the ECP potentials for all elements.
fn parse_ecp_lines(elements: &mut HashMap<String, BseBasisElement>, basis_lines: &[String]) -> Result<(), BseError> {
    // Remove 'end' from the lines (if they exist)
    // They may exist when this is called from other readers
    let basis_lines = basis_lines.iter().filter(|line| !line.eq_ignore_ascii_case("end")).cloned().collect_vec();

    // Start at index 1 in order to strip of the first line ('ECP' or something).
    // This splits out based on starting with an alpha character. A block can be
    // either a potential or the nelec line.
    let ecp_blocks = helpers::partition_lines(
        &basis_lines[1..],
        |x| x.chars().next().is_some_and(|c| c.is_alphabetic()),
        0,
        None,
        None,
        None,
        1,
        true,
    )?;

    for pot_lines in ecp_blocks.iter().filter(|x| !x.is_empty()) {
        // Check if this is a nelec line
        if pot_lines.len() == 1 {
            let parsed = helpers::parse_line_regex(&NELEC_RE, &pot_lines[0], "ECP: Element sym, nelec")?;
            let element_Z = lut::element_Z_from_sym(&parsed[0])
                .map_or(bse_raise!(ValueError, "Unknown element symbol: {}", parsed[0]), Ok)?;
            let nelec = parsed[1].parse().map_or(bse_raise!(ValueError, "Invalid nelec value: {}", parsed[1]), Ok)?;

            elements.entry(element_Z.to_string()).or_default().ecp_electrons = Some(nelec);
        } else {
            let parsed = helpers::parse_line_regex(&AM_LINE_RE, &pot_lines[0], "ECP: Element sym, pot AM")?;
            let element_Z = lut::element_Z_from_sym(&parsed[0])
                .map_or(bse_raise!(ValueError, "Unknown element symbol: {}", parsed[0]), Ok)?;
            let pot_am = match parsed[1].to_lowercase().as_str() {
                "ul" => vec![],
                _ => lut::amchar_to_int(&parsed[1], HIK)
                    .map_or(bse_raise!(ValueError, "Unknown angular momentum: {}", parsed[1]), Ok)?,
            };
            let ecp_data = helpers::parse_ecp_table(&pot_lines[1..], &["r_exp", "g_exp", "coeff"], None)?;
            let ecp_pot = BseEcpPotential {
                angular_momentum: pot_am,
                coefficients: ecp_data.coeff,
                ecp_type: "scalar_ecp".to_string(),
                r_exponents: ecp_data.r_exp,
                gaussian_exponents: ecp_data.g_exp,
            };
            elements.entry(element_Z.to_string()).or_default().ecp_potentials.get_or_insert_with(Default::default).push(ecp_pot);
        };
    }

    for (k, v) in elements.iter_mut().filter(|(_, v)| v.ecp_potentials.is_some()) {
        // Fix ecp angular momentum now that everything has been read
        // Specifically, we can set the 'ul' potential to be the max am + 1
        let ecp_potentials = v.ecp_potentials.as_mut().unwrap();
        let max_ecp_am = ecp_potentials.iter().flat_map(|p| &p.angular_momentum).max().cloned().unwrap_or(0);
        ecp_potentials.iter_mut().for_each(|p| {
            if p.angular_momentum.is_empty() {
                p.angular_momentum = vec![max_ecp_am + 1];
            }
        });

        // Make sure the number of electrons replaced by the ECP was specified for all
        // elements
        if v.ecp_electrons.is_none() {
            bse_raise!(ValueError, "Number of ECP electrons not specified for element {k}")?;
        }
    }

    Ok(())
}

pub fn read_nwchem(basis_str: &str) -> Result<BseBasisMinimal, BseError> {
    let lines: Vec<String> =
        basis_str.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty() && !s.starts_with('#')).collect();

    let mut basis_dict = BseBasisMinimal {
        molssi_bse_schema: BseMolssiBseSchema { schema_type: "minimal".to_string(), schema_version: "0.1".to_string() },
        elements: HashMap::new(),
        function_types: Vec::new(),
        name: "unknown_basis".to_string(),
        description: "no_description".to_string(),
    };

    let basis_sections =
        helpers::partition_lines(&lines, |x| x.to_lowercase() == "end", 0, None, Some(1), Some(2), 1, false)?;

    for s in basis_sections.iter().filter(|s| !s.is_empty()) {
        if s[0].to_lowercase().starts_with("basis") {
            parse_electron_lines(&mut basis_dict.elements, s)?
        } else if s[0].to_lowercase().starts_with("ecp") {
            parse_ecp_lines(&mut basis_dict.elements, s)?
        } else {
            return bse_raise!(ValueError, "Unknown section in NWChem basis: {}", s[0]);
        }
    }

    let function_types = compose::whole_basis_types(&basis_dict.elements);
    basis_dict.function_types = function_types;

    Ok(basis_dict)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_nwchem() {
        let args = BseGetBasisArgsBuilder::default().elements("H, O".to_string()).build().unwrap();
        let basis_str = get_formatted_basis("cc-pVDZ", "nwchem", args);
        let basis = read_nwchem(&basis_str).unwrap();
        println!("{basis:#?}");
    }
}
```

# Example of translation: Gaussian94

## Example of python to be translated

```python
'''
Reader for the Gaussian'94 format
'''

import re
from .. import lut, manip
from . import helpers

element_re = re.compile(r'^-?([A-Za-z]{1,3})(?:\s+0)?$')
ecp_shell_start_re = re.compile(r'^([A-Za-z]{1,3})\s+(\d+)\s+(\d+)$')
ecp_am_nelec_re = re.compile(r'^\S+\s+(\d+)\s+(\d+)$')

# This beast of a regex captures all the scaling factors as a single group:
#    '(?:{floating_re_str})+ ' is a non-capturing group of a number of floating point numbers.
#    Then, we capture all of them.
am_line_re = re.compile(r'^([A-Za-z]+)\s+(\d+)((?:\s+{})+)$'.format(helpers.floating_re_str))
explicit_am_line_re = re.compile(r'^\s*L=(\d+)\s+(\d+)((?:\s+{})+)$'.format(helpers.floating_re_str))


def _parse_electron_lines(basis_lines, bs_data):
    '''Parses lines representing all the electron shells for a single element

    Resulting information is stored in bs_data
    '''

    # Last line should be "****"
    last = basis_lines.pop()
    if last != '****':
        raise RuntimeError("Electron shell is missing terminating ****")

    # First line is "{element} 0"
    element_sym = basis_lines[0].split()[0]

    # In the format, if the element symbol starts with a dash, then Gaussian does not crash
    # with an error in case this element does not exist in the molecule input
    # (this is what you need for system basis set libraries).
    element_sym = element_sym.lstrip('-')

    element_Z = lut.element_Z_from_sym(element_sym, as_str=True)
    element_data = manip.create_element_data(bs_data, element_Z, 'electron_shells')

    # After that come shells. We determine the start of a shell
    # by if the line starts with an angular momentum (a non-numeric character
    shell_blocks = helpers.partition_lines(basis_lines[1:], lambda x: x[0].isalpha())

    for sh_lines in shell_blocks:
        # Shells start with AM nprim scaling
        if am_line_re.match(sh_lines[0]):
            shell_am, nprim, scaling_factors = helpers.parse_line_regex(am_line_re, sh_lines[0],
                                                                        "Shell AM, nprim, scaling")
            shell_am = lut.amchar_to_int(shell_am, hij=True)
        elif explicit_am_line_re.match(sh_lines[0]):
            shell_am, nprim, scaling_factors = helpers.parse_line_regex(explicit_am_line_re, sh_lines[0],
                                                                        "Shell AM, nprim, scaling")
            shell_am = [shell_am]
        else:
            raise RuntimeError("Failed to parse shell block starting on line: {}".format(sh_lines[0]))

        # Determine shell type
        func_type = lut.function_type_from_am(shell_am, 'gto', 'spherical')

        # Handle gaussian scaling factors
        # The square of the scaling factor is applied to exponents.
        # Typically they are 1.0, but not always
        scaling_factors = helpers.replace_d(scaling_factors)
        scaling_factors = [float(x) for x in scaling_factors.split()]

        # Remove any scaling factors that are 0.0
        scaling_factors = [x for x in scaling_factors if x != 0.0]

        # We should always have at least one scaling factor
        if len(scaling_factors) == 0:
            raise RuntimeError("No scaling factors given for element {}: Line: {}".format(element_sym, sh_lines[0]))

        # There can be multiple scaling factors, but we don't handle that. It seems to be very rare
        if len(scaling_factors) > 1:
            raise NotImplementedError("Number of scaling factors > 1")

        scaling_factor = float(scaling_factors[0])**2
        has_scaling = scaling_factor != 1.0

        # How many columns of coefficients do we have?
        # Gaussian doesn't support general contractions, so only >1 if
        # you have a fused shell
        ngen = len(shell_am)

        # Now read the exponents and coefficients
        exponents, coefficients = helpers.parse_primitive_matrix(sh_lines[1:], nprim, ngen)

        # If there is a scaling factor, apply it
        # But we keep track of some significant-figure type stuff (as best we can)
        if has_scaling:
            new_exponents = []
            for ex in exponents:
                ex = float(ex) * scaling_factor
                ex = '{:.16E}'.format(ex)

                # Trim useless zeroes
                ex_splt = ex.split('E')
                ex = ex_splt[0].rstrip('0')
                if ex[-1] == '.':  # Stripped all the zeroes...
                    ex += '0'
                ex += 'E' + ex_splt[1]
                new_exponents.append(ex)

            exponents = new_exponents

        shell = {
            'function_type': func_type,
            'region': '',
            'angular_momentum': shell_am,
            'exponents': exponents,
            'coefficients': coefficients
        }

        element_data['electron_shells'].append(shell)


def _parse_ecp_lines(basis_lines, bs_data):
    '''Parses lines representing all the ECP potentials for a single element

    Resulting information is stored in bs_data
    '''

    # First line is "{element} 0", with the zero being optional
    element_sym = basis_lines[0].split()[0]
    element_Z = lut.element_Z_from_sym(element_sym, as_str=True)
    element_data = manip.create_element_data(bs_data, element_Z, 'ecp_potentials')

    # Second line is information about the ECP
    max_am, ecp_electrons = helpers.parse_line_regex(ecp_am_nelec_re, basis_lines[1], 'ECP max_am, nelec')

    element_data['ecp_electrons'] = ecp_electrons

    # Partition all the potentials
    # Look for lines containing only an integer, but include the line
    # before it (is a comment line)
    ecp_blocks = helpers.partition_lines(basis_lines[2:], helpers.is_integer, before=1)

    for pot_lines in ecp_blocks:
        # first line is comment
        # second line is number of lines

        # Check that the number of lines is consistent
        if not helpers.is_integer(pot_lines[1]):
            raise RuntimeError("Number of lines for potential is not an integer: " + pot_lines[1])

        nlines = int(pot_lines[1])
        if nlines <= 0:
            raise RuntimeError("Number of lines for potential is <= 0")

        if len(pot_lines) != (nlines + 2):
            raise RuntimeError("Number of lines is incorrect. Expected {}, got {}".format(nlines, len(pot_lines) - 2))

        ecp_data = helpers.parse_ecp_table(pot_lines[2:])

        ecp_pot = {
            'angular_momentum': None,
            'ecp_type': 'scalar_ecp',
            'r_exponents': ecp_data['r_exp'],
            'gaussian_exponents': ecp_data['g_exp'],
            'coefficients': ecp_data['coeff']
        }

        element_data['ecp_potentials'].append(ecp_pot)

    # Determine the AM of the potentials
    # Highest AM first, then the rest in order
    all_pot_am = helpers.potential_am_list(max_am)

    # Were there as many potentials as we thought there should be?
    if len(all_pot_am) != len(element_data['ecp_potentials']):
        raise RuntimeError("Found incorrect number of potentials for {}: Expected {}, got {}".format(
            element_sym, len(all_pot_am), len(element_data['ecp_potentials'])))

    for idx, pot in enumerate(element_data['ecp_potentials']):
        pot['angular_momentum'] = [all_pot_am[idx]]


def read_g94(basis_lines):
    '''Reads G94-formatted file data and converts it to a dictionary with the
       usual BSE fields

       Note that the gaussian format does not store all the fields we
       have, so some fields are left blank
    '''

    # Removes comments
    basis_lines = helpers.prune_lines(basis_lines, '!')

    bs_data = {}
    other_data = {}

    # Empty file?
    if not basis_lines:
        return bs_data

    # split into element sections (may be electronic or ecp)
    element_sections = helpers.partition_lines(basis_lines, element_re.match, min_size=3)

    for es in element_sections:
        # Try to guess if this is an ecp
        # Each element block starts with the element symbol
        # If the number of lines > 3, and the 4th line is just an integer, then it is an ECP
        if len(es) > 3 and helpers.is_integer(es[3]):
            _parse_ecp_lines(es, bs_data)
        else:
            _parse_electron_lines(es, bs_data)

    return bs_data, other_data
```

## Example of code that has been translated to rust

```rust
//! Reader for the Gaussian94 format

use crate::prelude::*;
use crate::readers::helpers;

lazy_static::lazy_static! {
    static ref ELEMENT_RE: Regex = Regex::new(r"^-?([A-Za-z]{1,3})(?:\s+0)?$").unwrap();
    static ref ECP_SHELL_START_RE: Regex = Regex::new(r"^([A-Za-z]{1,3})\s+(\d+)\s+(\d+)$").unwrap();
    static ref ECP_AM_NELEC_RE: Regex = Regex::new(r"^\S+\s+(\d+)\s+(\d+)$").unwrap();
    static ref AM_LINE_RE: Regex =
        Regex::new(&format!(r"^([A-Za-z]+)\s+(\d+)((?:\s+{})+)$", helpers::FLOATING_RE.as_str())).unwrap();
    static ref EXPLICIT_AM_LINE_RE: Regex =
        Regex::new(&format!(r"^\s*L=(\d+)\s+(\d+)((?:\s+{})+)$", helpers::FLOATING_RE.as_str())).unwrap();
}

/// Parses lines representing all the electron shells for a single element.
fn parse_electron_lines(
    elements: &mut HashMap<String, BseBasisElement>,
    basis_lines: &[String],
) -> Result<(), BseError> {
    // Last line should be "****"
    let last = basis_lines.last().map_or(bse_raise!(ValueError, "Empty basis lines"), Ok)?;
    if last != "****" {
        bse_raise!(ValueError, "Electron shell is missing terminating ****")?
    }

    // First line is "{element} 0"
    let element_sym = basis_lines[0]
        .split_whitespace()
        .next()
        .map_or(bse_raise!(ValueError, "Invalid element line: {}", basis_lines[0]), Ok)?;

    // In the format, if the element symbol starts with a dash, then Gaussian does
    // not crash with an error in case this element does not exist in the
    // molecule input (this is what you need for system basis set libraries).
    let element_sym = element_sym.trim_start_matches('-');
    let element_Z = lut::element_Z_from_sym(element_sym)
        .map_or(bse_raise!(ValueError, "Unknown element symbol: {}", element_sym), Ok)?;

    // After that come shells. We determine the start of a shell
    // by if the line starts with an angular momentum (a non-numeric character)
    let shell_blocks = helpers::partition_lines(
        &basis_lines[1..basis_lines.len() - 1], // Skip first and last lines
        |x| x.chars().next().is_some_and(|c| c.is_alphabetic()),
        0,
        None,
        None,
        None,
        1,
        true,
    )?;

    for sh_lines in shell_blocks {
        let (shell_am, nprim, scaling_factors) = if AM_LINE_RE.is_match(&sh_lines[0]) {
            let parsed = helpers::parse_line_regex(&AM_LINE_RE, &sh_lines[0], "Shell AM, nprim, scaling")?;
            let shell_am = lut::amchar_to_int(&parsed[0], true)
                .map_or(bse_raise!(ValueError, "Unknown angular momentum: {}", parsed[0]), Ok)?;
            (shell_am, parsed[1].clone(), parsed[2].clone())
        } else if EXPLICIT_AM_LINE_RE.is_match(&sh_lines[0]) {
            let parsed = helpers::parse_line_regex(&EXPLICIT_AM_LINE_RE, &sh_lines[0], "Shell AM, nprim, scaling")?;
            let shell_am =
                vec![parsed[0].parse().map_or(bse_raise!(ValueError, "Invalid angular momentum: {}", parsed[0]), Ok)?];
            (shell_am, parsed[1].clone(), parsed[2].clone())
        } else {
            return bse_raise!(ValueError, "Failed to parse shell block starting on line: {}", sh_lines[0]);
        };

        // Determine shell type
        let func_type = lut::function_type_from_am(&shell_am, "gto", "spherical");

        // Handle gaussian scaling factors
        // The square of the scaling factor is applied to exponents.
        // Typically they are 1.0, but not always
        let scaling_factors = helpers::replace_d(&scaling_factors);
        let mut scaling_factors: Vec<f64> = scaling_factors
            .split_whitespace()
            .map(|s| s.parse().map_err(|_| BseError::ValueError(format!("Invalid scaling factor: {s}"))))
            .collect::<Result<_, _>>()?;

        // Remove any scaling factors that are 0.0
        scaling_factors.retain(|&x| x != 0.0);

        // We should always have at least one scaling factor
        if scaling_factors.is_empty() {
            bse_raise!(ValueError, "No scaling factors given for element {element_sym}: Line: {}", sh_lines[0])?;
        }

        // There can be multiple scaling factors, but we don't handle that. It seems to
        // be very rare
        if scaling_factors.len() > 1 {
            bse_raise!(NotImplementedError, "Number of scaling factors > 1")?;
        }

        let scaling_factor = scaling_factors[0].powi(2);
        let has_scaling = (scaling_factor - 1.0).abs() > f64::EPSILON;

        // How many columns of coefficients do we have?
        // Gaussian doesn't support general contractions, so only >1 if
        // you have a fused shell
        let ngen = shell_am.len();
        let nprim = nprim.parse().map_or(bse_raise!(ValueError, "Invalid nprim value: {nprim}"), Ok)?;

        // Now read the exponents and coefficients
        let (mut exponents, coefficients) =
            helpers::parse_primitive_matrix(&sh_lines[1..], Some(nprim), Some(ngen), None)?;

        // If there is a scaling factor, apply it
        // But we keep track of some significant-figure type stuff (as best we can)
        if has_scaling {
            exponents = exponents
                .into_iter()
                .map(|ex| {
                    let ex_val: f64 = ex.parse().map_or(bse_raise!(ValueError, "Invalid exponent: {ex}"), Ok)?;
                    let scaled = ex_val * scaling_factor;
                    let formatted = format!("{scaled:.16E}");
                    let mut parts = formatted.split('E');
                    let mantissa = parts.next().unwrap();
                    let exponent = parts.next().unwrap();

                    let trimmed_mantissa = mantissa.trim_end_matches('0');
                    let mantissa = if trimmed_mantissa.ends_with('.') {
                        format!("{trimmed_mantissa}0")
                    } else {
                        trimmed_mantissa.to_string()
                    };

                    Ok(format!("{mantissa}E{exponent}"))
                })
                .collect::<Result<Vec<String>, BseError>>()?;
        }

        let shell = BseElectronShell {
            function_type: func_type,
            region: "".to_string(),
            angular_momentum: shell_am,
            exponents,
            coefficients,
        };

        elements.entry(element_Z.to_string()).or_default().electron_shells.get_or_insert_with(Default::default).push(shell);
    }

    Ok(())
}

/// Parses lines representing all the ECP potentials for a single element.
fn parse_ecp_lines(elements: &mut HashMap<String, BseBasisElement>, basis_lines: &[String]) -> Result<(), BseError> {
    // First line is "{element} 0", with the zero being optional
    let element_sym = basis_lines[0]
        .split_whitespace()
        .next()
        .map_or(bse_raise!(ValueError, "Invalid element line: {}", basis_lines[0]), Ok)?;
    let element_Z = lut::element_Z_from_sym(element_sym)
        .map_or(bse_raise!(ValueError, "Unknown element symbol: {}", element_sym), Ok)?;

    // Second line is information about the ECP
    let parsed = helpers::parse_line_regex(&ECP_AM_NELEC_RE, &basis_lines[1], "ECP max_am, nelec")?;
    let max_am = parsed[0].parse().map_or(bse_raise!(ValueError, "Invalid max_am value: {}", parsed[0]), Ok)?;
    let ecp_electrons = parsed[1].parse().map_or(bse_raise!(ValueError, "Invalid nelec value: {}", parsed[1]), Ok)?;

    // Partition all the potentials
    // Look for lines containing only an integer, but include the line
    // before it (is a comment line)
    let ecp_blocks = helpers::partition_lines(&basis_lines[2..], helpers::is_integer, 1, None, None, None, 1, true)?;

    for pot_lines in ecp_blocks {
        // first line is comment
        // second line is number of lines

        // Check that the number of lines is consistent
        let nlines: i32 = pot_lines[1]
            .parse()
            .map_or(bse_raise!(ValueError, "Number of lines for potential is not an integer: {}", pot_lines[1]), Ok)?;

        if nlines <= 0 {
            bse_raise!(ValueError, "Number of lines for potential is <= 0")?;
        }

        if pot_lines.len() as i32 != (nlines + 2) {
            bse_raise!(ValueError, "Number of lines is incorrect. Expected {nlines}, got {}", pot_lines.len() - 2)?;
        }

        let ecp_data = helpers::parse_ecp_table(&pot_lines[2..], &["r_exp", "g_exp", "coeff"], None)?;

        let ecp_pot = BseEcpPotential {
            angular_momentum: vec![],
            coefficients: ecp_data.coeff,
            ecp_type: "scalar_ecp".to_string(),
            r_exponents: ecp_data.r_exp,
            gaussian_exponents: ecp_data.g_exp,
        };

        elements.entry(element_Z.to_string()).or_default().ecp_potentials.get_or_insert_with(Default::default).push(ecp_pot);
    }

    // Determine the AM of the potentials
    // Highest AM first, then the rest in order
    let all_pot_am = helpers::potential_am_list(max_am);

    // Were there as many potentials as we thought there should be?
    let element_data = elements.get_mut(&element_Z.to_string()).unwrap();
    let ecp_potentials = element_data.ecp_potentials.as_mut().unwrap();

    if all_pot_am.len() != ecp_potentials.len() {
        bse_raise!(
            ValueError,
            "Found incorrect number of potentials for {element_sym}: Expected {}, got {}",
            all_pot_am.len(),
            ecp_potentials.len()
        )?;
    }

    for (idx, pot) in ecp_potentials.iter_mut().enumerate() {
        pot.angular_momentum = vec![all_pot_am[idx]];
    }

    // Set the number of electrons
    elements.entry(element_Z.to_string()).or_default().ecp_electrons = Some(ecp_electrons);

    Ok(())
}

pub fn read_g94(basis_str: &str) -> Result<BseBasisMinimal, BseError> {
    // Removes comments
    let basis_lines =
        helpers::prune_lines(&basis_str.lines().map(|s| s.trim().to_string()).collect_vec(), "!", true, true);

    // Empty file?
    if basis_lines.is_empty() {
        return Ok(BseBasisMinimal::default());
    }

    let mut basis_dict = BseBasisMinimal {
        molssi_bse_schema: BseMolssiBseSchema { schema_type: "minimal".to_string(), schema_version: "0.1".to_string() },
        elements: HashMap::new(),
        function_types: Vec::new(),
        name: "unknown_basis".to_string(),
        description: "no_description".to_string(),
    };

    // split into element sections (may be electronic or ecp)
    let element_sections =
        helpers::partition_lines(&basis_lines, |x| ELEMENT_RE.is_match(x), 0, None, None, None, 3, true)?;

    for es in element_sections {
        // Try to guess if this is an ecp
        // Each element block starts with the element symbol
        // If the number of lines > 3, and the 4th line is just an integer, then it is
        // an ECP
        if es.len() > 3 && helpers::is_integer(&es[3]) {
            parse_ecp_lines(&mut basis_dict.elements, &es)?;
        } else {
            parse_electron_lines(&mut basis_dict.elements, &es)?;
        }
    }

    let function_types = compose::whole_basis_types(&basis_dict.elements);
    basis_dict.function_types = function_types;

    Ok(basis_dict)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_g94() {
        let args = BseGetBasisArgsBuilder::default().elements("H, O".to_string()).build().unwrap();
        let basis_str = get_formatted_basis("cc-pVDZ", "gaussian94", args);
        let basis = read_g94(&basis_str).unwrap();
        println!("{basis:#?}");
    }
}
```

# Your current task

Please translate the following code:

```python
import re
from .. import lut, manip
from . import helpers
from .nwchem import _parse_ecp_lines

all_element_names = [x.lower() for x in lut.all_element_names()]
all_element_names.append('wolffram')  # For tungsten

# Lines beginning a shell can be:
#   H {nprim} {ngen}
# or
#   {nprim} {ngen} 0
# This regex handles both cases
shell_begin_re = re.compile(r'^(?:[hH]\s+)?(\d+)\s+(\d+)(?: +0)?$')

# Typical format for starting an element with a comment
# Not sure this is absolutely required though
element_begin_re = re.compile(r'^!\s+([a-z]+)\s+\(.*\)\s*->\s*\[.*\]$')


#############################################################################
# There seems to be several dalton formats. One splits out by element, with
# each element block starting with "a {element_Z}". Then each shell
# starts with "{nprim} {ngen} 0"
# The second is also split by elements, but the element name
# is in a comment...
#############################################################################
def _line_begins_element(line):
    if not line:
        return False

    line = line.lower()

    if line.startswith('a '):
        return True

    if element_begin_re.match(line):
        s = line[1:].split()
        if s[0] in all_element_names:
            return True
        else:
            raise RuntimeError("Line looks to start an element, but element name is unknown to me. Line: " + line)

    return False


def _parse_electron_lines(basis_lines, bs_data):
    '''Parses lines representing all the electron shells for all elements

    Resulting information is stored in bs_data
    '''
    # fix common spelling mistakes
    basis_lines = [re.sub('PHOSPHOROUS', 'PHOSPHORUS', line) for line in basis_lines]
    # A little bit of a hack here
    # If we find the start of an element, remove all the following comment lines
    new_basis_lines = []
    i = 0
    while i < len(basis_lines):
        if _line_begins_element(basis_lines[i]):
            new_basis_lines.append(basis_lines[i])
            i += 1
            while basis_lines[i].startswith('!'):
                i += 1
        else:
            new_basis_lines.append(basis_lines[i])
            i += 1

    basis_lines = helpers.prune_lines(new_basis_lines, '$')

    # Now split out all the element blocks
    element_blocks = helpers.partition_lines(basis_lines, _line_begins_element, min_size=3)

    # For each block, split out all the shells
    for el_lines in element_blocks:
        # Figure out which type of block this is (does it start with 'a ' or a comment
        header = el_lines[0].lower()
        if header.startswith('a '):
            element_Z = helpers.parse_line_regex(r'a +(\d+) *$', header, "a {element_z}", convert_int=False)
        elif header.startswith('!'):
            element_name = helpers.parse_line_regex(element_begin_re, header, '! {element_name}')
            element_Z = lut.element_Z_from_name(element_name, as_str=True)
        else:
            raise RuntimeError("Unable to parse block in dalton: header line is \"{}\"".format(header))

        element_data = manip.create_element_data(bs_data, element_Z, 'electron_shells')
        el_lines.pop(0)

        # Remove all the rest of the comment lines
        el_lines = helpers.prune_lines(el_lines, '!')

        # Now partition again into blocks of shells for this element
        shell_blocks = helpers.partition_lines(el_lines, lambda x: shell_begin_re.match(x))

        # Shells are written in increasing angular momentum
        shell_am = 0

        for sh_lines in shell_blocks:
            nprim, ngen = helpers.parse_line_regex(shell_begin_re, sh_lines[0], 'nprim, ngen')
            bas_lines = sh_lines[1:]
            # fix for split over newline
            if nprim > 0 and bas_lines:
                num_line_splits = len(sh_lines[1:]) // nprim
                if num_line_splits * nprim == len(sh_lines[1:]):
                    bas_lines = [
                        ' '.join([sh_lines[1 + num_line_splits * i + offset] for offset in range(num_line_splits)])
                        for i in range(nprim)
                    ]
            exponents, coefficients = helpers.parse_primitive_matrix(bas_lines, nprim=nprim, ngen=ngen)

            func_type = lut.function_type_from_am([shell_am], 'gto', 'spherical')

            shell = {
                'function_type': func_type,
                'region': '',
                'angular_momentum': [shell_am],
                'exponents': exponents,
                'coefficients': coefficients
            }

            element_data['electron_shells'].append(shell)
            shell_am += 1


def read_dalton(basis_lines):
    '''Reads Dalton-formatted file data and converts it to a dictionary with the
       usual BSE fields
    '''

    ###########################################################################
    # We need to leave in comments until later, since they can be significant
    # (one format allows "! {ELEMENT}" to start an element block)
    ###########################################################################

    # But we still prune blank lines
    basis_lines = helpers.prune_lines(basis_lines)

    bs_data = {}
    other_data = {}

    # Skip forward until either:
    # 1. Line begins with 'a'
    # 2. Line begins with 'ecp'
    # 2. Lines begins with '!', with an element name following
    while basis_lines and not _line_begins_element(basis_lines[0]) and basis_lines[0].lower() != 'ecp':
        basis_lines.pop(0)

    # Empty file?
    if not basis_lines:
        return bs_data

    # Partition into ECP and electron blocks
    # I don't think Dalton supports ECPs, but the original BSE
    # Used the NWChem output format for the ECP part
    basis_sections = helpers.partition_lines(basis_lines, lambda x: x.lower() == 'ecp', min_blocks=1, max_blocks=2)

    for s in basis_sections:
        if s[0].lower() == 'ecp':
            _parse_ecp_lines(s, bs_data)
        else:
            _parse_electron_lines(s, bs_data)

    return bs_data, other_data
```
