primer3 0.1.0

Safe Rust bindings to the primer3 primer design library
Documentation
//! Utility functions for working with DNA sequences.

use std::ffi::CString;

/// Returns `true` if the DNA sequence is self-complementary (palindromic).
///
/// A self-complementary sequence reads the same on both strands in the
/// 5'→3' direction. For example, `ATAT` is self-complementary because its
/// reverse complement is also `ATAT`.
///
/// Returns `false` for empty sequences or sequences containing null bytes.
///
/// # Example
///
/// ```
/// assert!(primer3::is_symmetric("ATAT"));
/// assert!(primer3::is_symmetric("ACGT"));
/// assert!(!primer3::is_symmetric("AAAA"));
/// ```
pub fn is_symmetric(seq: &str) -> bool {
    if seq.is_empty() {
        return false;
    }
    let Ok(c_seq) = CString::new(seq.to_ascii_uppercase()) else {
        return false;
    };
    unsafe { primer3_sys::symmetry(c_seq.as_ptr()) == 1 }
}

/// Converts divalent cation concentration to an equivalent monovalent
/// concentration using the primer3 formula.
///
/// This is useful for approximating the combined ionic strength when both
/// monovalent (Na⁺, K⁺) and divalent (Mg²⁺) cations are present. The
/// dNTP concentration is subtracted because dNTPs chelate divalent cations.
///
/// # Arguments
///
/// * `divalent` - Divalent cation concentration in mM (e.g., `MgCl₂`).
/// * `dntp` - dNTP concentration in mM.
///
/// # Returns
///
/// The equivalent monovalent cation concentration in mM. Returns 0.0 if
/// the free divalent concentration (after dNTP chelation) is non-positive.
///
/// # Example
///
/// ```
/// let equiv = primer3::divalent_to_monovalent(1.5, 0.6);
/// assert!(equiv > 0.0);
/// ```
pub fn divalent_to_monovalent(divalent: f64, dntp: f64) -> f64 {
    unsafe { primer3_sys::divalent_to_monovalent(divalent, dntp) }
}

/// Complements a single DNA base (as a byte).
///
/// Handles both uppercase and lowercase IUPAC bases. Non-DNA bytes
/// are passed through unchanged.
fn complement_byte(b: u8) -> u8 {
    match b {
        b'A' => b'T',
        b'T' => b'A',
        b'G' => b'C',
        b'C' => b'G',
        b'a' => b't',
        b't' => b'a',
        b'g' => b'c',
        b'c' => b'g',
        b'R' => b'Y',
        b'Y' => b'R',
        b'S' => b'S',
        b'W' => b'W',
        b'K' => b'M',
        b'M' => b'K',
        b'B' => b'V',
        b'V' => b'B',
        b'D' => b'H',
        b'H' => b'D',
        b'N' => b'N',
        b'r' => b'y',
        b'y' => b'r',
        b's' => b's',
        b'w' => b'w',
        b'k' => b'm',
        b'm' => b'k',
        b'b' => b'v',
        b'v' => b'b',
        b'd' => b'h',
        b'h' => b'd',
        b'n' => b'n',
        other => other,
    }
}

/// Returns the reverse complement of a DNA sequence.
///
/// Handles both uppercase and lowercase IUPAC bases. Non-DNA characters
/// are passed through unchanged.
///
/// # Example
///
/// ```
/// assert_eq!(primer3::reverse_complement("ATCG"), "CGAT");
/// assert_eq!(primer3::reverse_complement("aAtTgGcC"), "GgCcAaTt");
/// ```
///
/// # Panics
///
/// Panics if the input contains non-ASCII bytes (debug assertion).
pub fn reverse_complement(seq: &str) -> String {
    debug_assert!(seq.is_ascii(), "reverse_complement requires ASCII input");
    let bytes: Vec<u8> = seq.bytes().rev().map(complement_byte).collect();
    // complement_byte maps ASCII bytes to ASCII bytes, so the result is valid UTF-8
    String::from_utf8(bytes).expect("complement_byte preserves ASCII validity")
}

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

    #[test]
    fn test_complement_byte_standard_bases() {
        assert_eq!(complement_byte(b'A'), b'T');
        assert_eq!(complement_byte(b'T'), b'A');
        assert_eq!(complement_byte(b'G'), b'C');
        assert_eq!(complement_byte(b'C'), b'G');
    }

    #[test]
    fn test_complement_byte_lowercase() {
        assert_eq!(complement_byte(b'a'), b't');
        assert_eq!(complement_byte(b't'), b'a');
        assert_eq!(complement_byte(b'g'), b'c');
        assert_eq!(complement_byte(b'c'), b'g');
    }

    #[test]
    fn test_complement_byte_iupac() {
        assert_eq!(complement_byte(b'R'), b'Y');
        assert_eq!(complement_byte(b'Y'), b'R');
        assert_eq!(complement_byte(b'S'), b'S');
        assert_eq!(complement_byte(b'W'), b'W');
        assert_eq!(complement_byte(b'K'), b'M');
        assert_eq!(complement_byte(b'M'), b'K');
        assert_eq!(complement_byte(b'N'), b'N');
    }

    #[test]
    fn test_complement_byte_passthrough() {
        assert_eq!(complement_byte(b'X'), b'X');
        assert_eq!(complement_byte(b'0'), b'0');
    }

    #[test]
    fn test_reverse_complement_empty() {
        assert_eq!(reverse_complement(""), "");
    }

    #[test]
    fn test_reverse_complement_single() {
        assert_eq!(reverse_complement("A"), "T");
        assert_eq!(reverse_complement("C"), "G");
    }

    #[test]
    fn test_reverse_complement_standard() {
        assert_eq!(reverse_complement("ATCG"), "CGAT");
        assert_eq!(reverse_complement("AAAA"), "TTTT");
        assert_eq!(reverse_complement("GCGC"), "GCGC");
    }

    #[test]
    fn test_reverse_complement_mixed_case() {
        assert_eq!(reverse_complement("aAtTgGcC"), "GgCcAaTt");
    }

    #[test]
    fn test_reverse_complement_palindrome() {
        assert_eq!(reverse_complement("ATAT"), "ATAT");
    }
}