k-nucleotide/src/main.nika

// The Benchmarks Game's `k-nucleotide`: reads DNA in FASTA form from standard
// input and reports how often each short fragment occurs in one section.
//
//     cat dna.fasta | nikaia run
//     A 33.333
//     T 30.000
//     …
//     3	GGT
//     3	GGTA
//     0	GGTATT
//     …
//
// The input is FASTA text: a header line starting with `>`, then lines of DNA.
// The section read is the one whose header starts with `THREE` (the
// benchmark's choice); `cat dna.fasta | nikaia run -- ONE` picks another.
//
// Shows: reading standard input, a `grammar` for a line-based format, map
// keys that are views into a string rather than copies, and `?.` with `??`.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints against `tests/` (ADR-269 D13).

use std::io
use std::cli
use std::collections

// ---------------------------------------------------------------------------
// 1. FASTA
// ---------------------------------------------------------------------------

grammar Fasta {

    // In FASTA a newline is the structure, not a blank to skip. Defining `WS`
    // as empty turns off the whitespace skipping of lowercase rules, which
    // would otherwise eat the line endings this grammar is made of.
    rule WS = "" -> { }

    entry rule file -> Vec[Section] =
        sections:section* -> { sections }

    // A header, then every line that does not start a new one. `not(">")`
    // consumes nothing: it only tests, and it is what ends the body.
    rule section -> Section =
        ">" header:LINE body:body_line* -> { Section { header, lines: body } }

    rule body_line -> String = not(">") l:LINE -> { l }

    // `until` stops before the terminator, so the line ending is consumed
    // explicitly. Input whose last line has no line ending is refused as not
    // FASTA rather than losing that line.
    rule LINE -> String = s:until(line_ending) line_ending -> { s }
}

pub struct Section {
    header: String,
    lines: Vec[String],
}

// ---------------------------------------------------------------------------
// 2. Counting
// ---------------------------------------------------------------------------

// A count is a struct with a method rather than a bare number:
// `entry(..).and_modify` hands the lambda the value already there, and the
// lambda calls a method on it. `sync` says a function cannot pause, and the
// compiler checks it.
pub struct Tally {
    n: i64,
}

impl Tally {
    pub fn(first: i64) -> Tally sync {
        return Tally { n: first }
    }

    fn bump(ref mut self) sync {
        self.n += 1
    }
}

// Every fragment of length `k`, and how often each occurs.
//
// The keys are views into `dna`: `ref dna[i..<i + k]` is a slice, not a copy,
// so a sequence of a million characters builds a table of positions and
// lengths rather than a million small strings.
fn count(dna: String, k: i64) -> collections::HashMap[String, Tally] {
    let mut counts: collections::HashMap[String, Tally] = collections::HashMap()

    if dna.len() < k {
        return counts
    }

    for i in 0..<dna.len() - k + 1 {
        // `i` is a counter, not a position the text handed out: the bytes
        // are cut, and made text again (Part I 2.6). A cut through a scalar
        // is no fragment of DNA.
        let fragment = dna.bytes[i..<i + k].text() catch {
            continue
        }
        counts
            .entry(fragment)
            .and_modify fn (tally) { tally.bump() }
            .or_insert_with fn { Tally(1) }
    }

    return counts
}

// The fragments of one length, as percentages, most frequent first.
//
// Two stable sorts give the order the benchmark asks for: by fragment, then by
// count descending. The second sort keeps the first's order among equal counts,
// so ties come out alphabetically.
fn report_frequencies(dna: String, k: i64) {
    let counts = count(dna, k)

    let mut rows = Vec()
    let mut total = 0
    for (fragment, tally) in counts {
        total += tally.n
        rows.push((fragment, tally.n))
    }

    rows.sort_by_key fn (row) { row.0 }
    rows.sort_by_key fn (row) { -row.1 }

    for row in rows {
        let percent = (row.1 as f64) * 100.0 / (total as f64)
        println(f"{row.0} {percent:.3}")
    }
    println("")
}

// One named fragment, and how often it occurs. `k` is the fragment's own
// length, so the table is built for exactly the width being asked about.
fn report_fragment(dna: String, fragment: String) {
    let counts = count(dna, fragment.len())
    // `get` hands back a `Tally?`, because the key may not be there: `?.`
    // reaches into it if it is, and `??` gives the value to use if it is not.
    let n = counts.get(fragment)?.n ?? 0
    println(f"{n}\t{fragment}")
}

// ---------------------------------------------------------------------------
// 3. The program
// ---------------------------------------------------------------------------

fn main() throws {
    // The whole of standard input, read into a string the program owns: a pipe
    // cannot be mapped like a file. There is no `await`, yet while the pipe is
    // empty other tasks run. With `user-parallelism = "yes"` in nikaia.toml
    // `[build]` the read may resume on another thread; the output is the same.
    let text = io::read_to_string() catch {
        eprintln(f"cannot read standard input: {error}")
        return
    }

    let sections = Fasta::file(text) catch {
        eprintln(f"standard input is not FASTA: {error}")
        return
    }

    // The section to count, by the start of its header. Lines are uppercased,
    // since FASTA may write DNA in either case.
    let wanted = cli::args().nth(1) ?? "THREE"

    let mut dna = String()
    for section in sections {
        if section.header.starts_with(ref wanted) {
            for line in section.lines {
                dna.push_str(ref line.to_uppercase())
            }
        }
    }

    if dna.len() == 0 {
        eprintln(f"no section whose header starts with {wanted}")
        return
    }

    report_frequencies(dna, 1)
    report_frequencies(dna, 2)

    report_fragment(dna, "GGT")
    report_fragment(dna, "GGTA")
    report_fragment(dna, "GGTATT")
    report_fragment(dna, "GGTATTTTAATT")
    report_fragment(dna, "GGTATTTTAATTTATAGT")
}

The file itself · all the examples

This site is open source. Improve this page.