bio_files 0.5.3

Save and load common biology file formats
Documentation
use std::fmt::Write as _;

use bio_files::{BondType, Sdf};

const TWO_MOLS: &str = "\
water


  3  2  0  0  0  0            999 V2000
    0.0000    0.0000    0.0000 O   0  0
    0.9500    0.0000    0.0000 H   0  0
   -0.2400    0.9200    0.0000 H   0  0
  1  2  1  0
  1  3  1  0
M  END
> <NAME>
Water

$$$$
dioxygen


  2  1  0  0  0  0            999 V2000
    0.0000    0.0000    0.0000 O   0  0
    1.2100    0.0000    0.0000 O   0  0
  1  2  2  0
M  END
> <NAME>
Dioxygen

$$$$
";

#[test]
fn parses_every_record() {
    let mols = Sdf::new_multi(TWO_MOLS).unwrap();
    assert_eq!(mols.len(), 2);

    assert_eq!(mols[0].ident, "water");
    assert_eq!(mols[0].atoms.len(), 3);
    assert_eq!(mols[0].bonds.len(), 2);
    assert_eq!(mols[0].metadata.get("NAME").unwrap(), "Water");

    assert_eq!(mols[1].ident, "dioxygen");
    assert_eq!(mols[1].atoms.len(), 2);
    assert_eq!(mols[1].bonds.len(), 1);
    assert_eq!(mols[1].metadata.get("NAME").unwrap(), "Dioxygen");
}

#[test]
fn final_record_without_terminator() {
    let text = TWO_MOLS.trim_end().trim_end_matches("$$$$");
    let mols = Sdf::new_multi(text).unwrap();
    assert_eq!(mols.len(), 2);
    assert_eq!(mols[1].ident, "dioxygen");
}

#[test]
fn single_mol_file_still_works() {
    let one = TWO_MOLS.split("$$$$").next().unwrap();
    let mols = Sdf::new_multi(one).unwrap();
    assert_eq!(mols.len(), 1);
    assert_eq!(mols[0].ident, "water");
}

#[test]
fn bad_record_is_skipped_not_fatal() {
    let text = format!("{TWO_MOLS}garbage\nnot an sdf\n$$$$\n");
    let mols = Sdf::new_multi(&text).unwrap();
    assert_eq!(mols.len(), 2);
}

#[test]
fn round_trips_through_save_multi() {
    let mols = Sdf::new_multi(TWO_MOLS).unwrap();
    let path = std::env::temp_dir().join("bio_files_sdf_multi_tmp.sdf");
    Sdf::save_multi(&mols, &path, Default::default()).unwrap();

    let reloaded = Sdf::load_multi(&path).unwrap();
    assert_eq!(reloaded.len(), 2);
    assert_eq!(reloaded[0].ident, "water");
    assert_eq!(reloaded[0].atoms.len(), 3);
    assert_eq!(reloaded[1].ident, "dioxygen");
    assert_eq!(reloaded[1].bonds.len(), 1);

    let _ = std::fs::remove_file(&path);
}

#[test]
fn parses_short_header_touching_coordinates_query_bonds_and_valid_elements() {
    let text = "Mrv1652309142106383D

  3  2  0  0  0  0            999 V2000
 1234.5678-1234.5678    0.0000 Gd  0  0  0  0  0  0  0  0  0  0  0  0
    1.0000    0.0000    0.0000 Nb  0  0  0  0  0  0  0  0  0  0  0  0
    2.0000    0.0000    0.0000 Sm  0  0  0  0  0  0  0  0  0  0  0  0
  1  2  6  0  0  0  0
  2  3  1  0  0  0  0
M  END
";

    let mol = Sdf::new(text).unwrap();
    assert_eq!(mol.atoms.len(), 3);
    assert_eq!(mol.atoms[0].posit.x, 1234.5678);
    assert_eq!(mol.atoms[0].posit.y, -1234.5678);
    assert!(mol.atoms.iter().all(|atom| atom.element.to_letter() == "X"));
    assert_eq!(mol.bonds[0].bond_type, BondType::Unknown);
}

#[test]
fn parses_touching_fixed_width_count_fields() {
    let mut text = String::from("joined counts\n\n\n 96101  0  0  0  0            999 V2000\n");
    for _ in 0..96 {
        writeln!(
            text,
            "    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0"
        )
        .unwrap();
    }
    for _ in 0..101 {
        writeln!(text, "  1  2  1  0  0  0  0").unwrap();
    }
    writeln!(text, "M  END").unwrap();

    let mol = Sdf::new(&text).unwrap();
    assert_eq!(mol.atoms.len(), 96);
    assert_eq!(mol.bonds.len(), 101);
}

#[test]
fn parses_touching_fixed_width_bond_indices() {
    let mut text =
        String::from("joined bond indices\n\n\n114  1  0  0  0  0            999 V2000\n");
    for _ in 0..114 {
        writeln!(
            text,
            "    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0"
        )
        .unwrap();
    }
    writeln!(text, " 96114  1  0  0  0  0").unwrap();
    writeln!(text, "M  END").unwrap();

    let mol = Sdf::new(&text).unwrap();
    assert_eq!(mol.bonds[0].atom_0_sn, 96);
    assert_eq!(mol.bonds[0].atom_1_sn, 114);
}