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.
//

//! Themisto `pseudoalign` parser.
//!
//! ## Expected format
//! This example was generated with themisto v3.2.2 from the `themisto pseudoalign` subcommand.
//!
//! ```text
//! 117378
//! 119884
//! 111108 0
//! 128864
//! 651964 0 1
//! 670792 1
//! ```
//!
//! or, with tabs and line breaks visible (note the columns are separated by single *spaces*):
//! ```text
//! 117378$
//! 119884$
//! 111108 0$
//! 128864$
//! 651964 0 1$
//! 670792 1$
//! ```
//!
//! ### Pros of the themisto format
//! - The first column contains the index of the query in the input .fastx file.
//! - 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 themisto 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 themisto format
//! - Older (major) versions of the tool may append spaces at the end of the line.
//!

use std::io::Read;

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

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

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

    let mut records = contents.split(separator);

    let id_bytes = records.next().ok_or(CorruptedInputErr)?;
    let read_id = 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(read_id), ones: Some(ones), ..Default::default()};
    Ok(res)
}

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

    #[test]
    fn read_themisto_line_multiple_aligned() {
        use std::io::Cursor;
        use crate::PseudoAln;
        use super::read_themisto;

        let data: Vec<u8> = b"128 0 7 11 3".to_vec();
        let expected = PseudoAln{ones_names: None,  query_id: Some(128), ones: Some(vec![0, 7, 11, 3]), ..Default::default()};

        let mut input: Cursor<Vec<u8>> = Cursor::new(data);
        let got = read_themisto(&mut input).unwrap();

        assert_eq!(got, expected);
    }

    #[test]
    fn read_themisto_line_empty() {
        use std::io::Cursor;
        use crate::PseudoAln;
        use super::read_themisto;

        let data: Vec<u8> = b"185216".to_vec();
        let expected = PseudoAln{ones_names: None,  query_id: Some(185216), ones: Some(Vec::new()), ..Default::default()};

        let mut input: Cursor<Vec<u8>> = Cursor::new(data);
        let got = read_themisto(&mut input).unwrap();

        assert_eq!(got, expected);
    }

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

        let data: Vec<u8> = b"185216\n188352\n202678 1\n202728\n651964 0 1\n651966 0 1\n1166624 0\n1166625 0\n1166626 1".to_vec();
        let expected = vec![
            PseudoAln{ones_names: None,  query_id: Some(185216), ones: Some(vec![]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(188352), ones: Some(vec![]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(202678), ones: Some(vec![1]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(202728), ones: Some(vec![]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(651964), ones: Some(vec![0, 1]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(651966), ones: Some(vec![0, 1]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(1166624), ones: Some(vec![0]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(1166625), ones: Some(vec![0]), ..Default::default()},
            PseudoAln{ones_names: None,  query_id: Some(1166626), ones: Some(vec![1]), ..Default::default()},
        ];

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

        assert_eq!(got, expected);
    }
}