ahda 0.1.1

Pseudoalignment compression and conversion between formats.
Documentation
// ahda: Pseudoalignment compression and conversion between formats.
//
// Copyright 2025 Tommi Mäklin [tommi@maklin.fi].
//
// Copyrights in this project are retained by contributors. No copyright assignment
// is required to contribute to this project.
//
// Except as otherwise noted (below and/or in individual files), this
// project is licensed under the Apache License, Version 2.0
// <LICENSE-APACHE> or <http://www.apache.org/licenses/LICENSE-2.0> or
// the MIT license, <LICENSE-MIT> or <http://opensource.org/licenses/MIT>,
// at your option.
//

//! Fulgor `pseudoalign` parser.
//!
//! ## Expected format
//! This example was generated with fulgor v4.2.0 from the `fulgor pseudoalign` subcommand.
//!
//! ```text
//! 11301   0
//! 4995    0
//! 7542    1       1
//! 15782   0
//! 652016  2       0       1
//! 100817  1       1
//! ```
//!
//! or, with tabs and line breaks visible:
//! ```text
//! 11301\t0$
//! 4995\t0$
//! 7542\t1\t1$
//! 15782\t0$
//! 652016\t2\t0\t1$
//! 100817\t1\t1$
//! ```
//!
//! ### Pros of the fulgor format
//! - The first column contains the index of the query in the input .fastx file.
//! - Starting from the third column, the subsequent columns contain indexes of the aligned target sequences.
//! - Queries with no alignments are shown.
//! - Number of queries can be inferred from the file.
//!
//! ### Cons of the fulgor format
//! - Number of target sequences cannot be inferred with certainty.
//! - Name of the target sequence is not given.
//! - Name of the query sequence is not given.
//!
//! ### Other considerations for the fulgor format
//! - The second column contains the total number of aligned target sequences.
//!

use std::io::Read;

use crate::PseudoAln;
use crate::errors::CorruptedInputErr;

type E = Box<dyn std::error::Error>;

/// Parse a line from Fulgor
///
/// Reads a pseudoalignment line stored in the *Fulgor* format.
///
/// Returns the [pseudoalignment](PseudoAln) on the line.
///
pub fn read_fulgor<R: Read>(
    conn: &mut R,
) -> Result<PseudoAln, E> {
    let separator: char = '\t';
    let mut contents: String = String::new();
    conn.read_to_string(&mut contents)?;

    let mut records = contents.split(separator);

    let read_name_bytes = records.next().ok_or(CorruptedInputErr{})?;
    let _ = records.next().ok_or(CorruptedInputErr)?;

    let query_name = read_name_bytes.as_bytes().to_vec();
    let mut ones: Vec<u32> = Vec::new();

    for record in records {
        let id = record.parse::<u32>()?;
        ones.push(id);
    }

    let res = PseudoAln{ones_names: None,  query_id: None, ones: Some(ones), query_name: Some(query_name)};
    Ok(res)
}

/// Parse a line from Fulgor >= v4.x.y
///
/// Reads a pseudoalignment line stored in the *Fulgor* format (v4 onwards).
///
/// Returns the [pseudoalignment](PseudoAln) on the line.
///
pub fn read_fulgor_v4<R: Read>(
    conn: &mut R,
) -> Result<PseudoAln, E> {
    let separator: char = '\t';
    let mut contents: String = String::new();
    conn.read_to_string(&mut contents)?;

    let mut records = contents.split(separator);

    let read_id_bytes = records.next().ok_or(CorruptedInputErr{})?;
    let _ = records.next().ok_or(CorruptedInputErr)?;

    let query_id = read_id_bytes.parse::<u32>()?;
    let mut ones: Vec<u32> = Vec::new();

    for record in records {
        let id = record.parse::<u32>()?;
        ones.push(id);
    }

    let res = PseudoAln{ones_names: None,  query_id: Some(query_id), ones: Some(ones), query_name: None};
    Ok(res)
}

// Tests
#[cfg(test)]
mod tests {

    #[test]
    fn read_fulgor_multiple() {
        use crate::PseudoAln;
        use super::read_fulgor;
        use std::io::BufRead;
        use std::io::BufReader;
        use std::io::Cursor;

        let mut data: Vec<u8> = b"ERR4035126.4996\t0\n".to_vec();
        data.append(&mut b"ERR4035126.1262953\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262954\t1\t1\n".to_vec());
        data.append(&mut b"ERR4035126.1262955\t1\t1\n".to_vec());
        data.append(&mut b"ERR4035126.1262956\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262957\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262958\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262959\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.651965\t2\t0\t1\n".to_vec());
        data.append(&mut b"ERR4035126.11302\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262960\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262961\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.1262962\t1\t0\n".to_vec());
        data.append(&mut b"ERR4035126.651965\t2\t0\t1\n".to_vec());

        let expected = vec![
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![]), query_name: Some("ERR4035126.4996".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262953".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![1]), query_name: Some("ERR4035126.1262954".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![1]), query_name: Some("ERR4035126.1262955".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262956".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262957".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262958".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262959".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0, 1]), query_name: Some("ERR4035126.651965".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![]), query_name: Some("ERR4035126.11302".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262960".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262961".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0]), query_name: Some("ERR4035126.1262962".as_bytes().to_vec()) },
            PseudoAln{ones_names: None,  query_id: None, ones: Some(vec![0, 1]), query_name: Some("ERR4035126.651965".as_bytes().to_vec()) },
        ];

        let cursor = Cursor::new(data);
        let reader = BufReader::new(cursor);
        let got: Vec<PseudoAln> = reader.lines().map(|line| {
            read_fulgor(&mut line.unwrap().as_bytes()).unwrap()
        }).collect();

        assert_eq!(got, expected);
    }

    #[test]
    fn read_fulgor_v4_multiple() {
        use crate::PseudoAln;
        use super::read_fulgor_v4;
        use std::io::BufRead;
        use std::io::BufReader;
        use std::io::Cursor;

        let mut data: Vec<u8> = b"4996\t0\n".to_vec();
        data.append(&mut b"1262953\t1\t0\n".to_vec());
        data.append(&mut b"1262954\t1\t1\n".to_vec());
        data.append(&mut b"1262955\t1\t1\n".to_vec());
        data.append(&mut b"1262956\t1\t0\n".to_vec());
        data.append(&mut b"1262957\t1\t0\n".to_vec());
        data.append(&mut b"1262958\t1\t0\n".to_vec());
        data.append(&mut b"1262959\t1\t0\n".to_vec());
        data.append(&mut b"651965\t2\t0\t1\n".to_vec());
        data.append(&mut b"11302\t0\n".to_vec());
        data.append(&mut b"1262960\t1\t0\n".to_vec());
        data.append(&mut b"1262961\t1\t0\n".to_vec());
        data.append(&mut b"1262962\t1\t0\n".to_vec());
        data.append(&mut b"651965\t2\t0\t1\n".to_vec());

        let expected = vec![
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![]), query_id: Some(4996) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262953) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![1]), query_id: Some(1262954) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![1]), query_id: Some(1262955) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262956) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262957) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262958) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262959) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0, 1]), query_id: Some(651965) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![]), query_id: Some(11302) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262960) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262961) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0]), query_id: Some(1262962) },
            PseudoAln{ones_names: None,  query_name: None, ones: Some(vec![0, 1]), query_id: Some(651965) },
        ];

        let cursor = Cursor::new(data);
        let reader = BufReader::new(cursor);
        let got: Vec<PseudoAln> = reader.lines().map(|line| {
            read_fulgor_v4(&mut line.unwrap().as_bytes()).unwrap()
        }).collect();

        assert_eq!(got, expected);
    }
}