elli-gf2 0.1.0

Machine-first GF(2) elimination library with exact multi-backend reduction and auto-selection.
Documentation
use std::fs;
use std::path::Path;

use crate::SparseCol;

#[derive(Debug, Clone)]
pub struct SparseMatrixFile {
    pub rows: usize,
    pub cols: Vec<SparseCol>,
}

fn parse_usize(s: &str, what: &str) -> Result<usize, String> {
    s.trim()
        .parse::<usize>()
        .map_err(|_| format!("failed to parse {} as usize: {}", what, s))
}

/// File format:
///
/// Line 1: rows=<N>
/// Remaining non-empty, non-comment lines: one column per line
/// Each column line is a whitespace-separated list of row indices
///
/// Example:
/// rows=8
/// 0 2 4
/// 1 4
/// 2 3 7
/// # empty column below
///
/// 0 7
///
/// Empty lines after the header count as empty columns.
/// Comment lines begin with '#'.
pub fn parse_sparse_columns_str(input: &str) -> Result<SparseMatrixFile, String> {
    let mut lines = input.lines();

    let header = lines
        .next()
        .ok_or_else(|| "missing header line; expected rows=<N>".to_string())?
        .trim();

    let rows = if let Some(rest) = header.strip_prefix("rows=") {
        parse_usize(rest, "rows")?
    } else {
        return Err(format!(
            "invalid header '{}'; expected first line to look like rows=<N>",
            header
        ));
    };

    let mut cols: Vec<SparseCol> = Vec::new();

    for (idx, raw_line) in lines.enumerate() {
        let line = raw_line.trim();

        if line.starts_with('#') {
            continue;
        }

        if line.is_empty() {
            cols.push(Vec::new());
            continue;
        }

        let mut col: Vec<u32> = Vec::new();
        for tok in line.split_whitespace() {
            let r = tok
                .parse::<usize>()
                .map_err(|_| format!("line {}: failed to parse row index '{}'", idx + 2, tok))?;
            if r >= rows {
                return Err(format!(
                    "line {}: row index {} out of bounds for rows={}",
                    idx + 2,
                    r,
                    rows
                ));
            }
            col.push(r as u32);
        }

        col.sort_unstable();
        col.dedup();
        cols.push(col);
    }

    Ok(SparseMatrixFile { rows, cols })
}

pub fn load_sparse_columns_file<P: AsRef<Path>>(path: P) -> Result<SparseMatrixFile, String> {
    let path_ref = path.as_ref();
    let text = fs::read_to_string(path_ref)
        .map_err(|e| format!("failed to read {}: {}", path_ref.display(), e))?;
    parse_sparse_columns_str(&text)
}