Every problem

All 15 problems from Rosalind — Bioinformatics Armory. Press Run on any block to execute it in your browser. Every problem is on this page, so it is large and takes a moment to settle — the sections are lighter.

INI — Introduction to the Bioinformatics Armory

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: INI — Introduction to the Bioinformatics Armory
# https://rosalind.info/problems/ini/
#
# Given: A DNA string s of length at most 1000 bp.
# Return: Four integers separated by spaces counting A, C, G, T occurrences.

let s = dna"AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC"

let counts = base_counts(s)
let result = str(counts.A) + " " + str(counts.C) + " " + str(counts.G) + " " + str(counts.T)

println("Result:   " + result)
println("Expected: 20 12 17 21")
println("Match:    " + str(result == "20 12 17 21"))

fn test_ini_base_counts() {
    assert result == "20 12 17 21", "INI: expected '20 12 17 21', got '" + result + "'"
}

GBK — GenBank Introduction

solvedbrowser + CLI · online Problem statement Open in the workbench Download .bl

Calls NCBI, so it needs a network connection and its answer can change over time.

Entrez record counts grow as GenBank is updated, so no fixed answer can be asserted.

# Rosalind: GBK — GenBank Introduction
# https://rosalind.info/problems/gbk/
#
# Given: A genus name, two dates in YYYY/M/D format.
# Return: Number of Nucleotide GenBank entries for that genus published between the dates.

let genus = "Anthoxanthum"
let date_from = "2003/07/25"
let date_to = "2005/12/27"

# Build NCBI Entrez query with organism and date range
let query = genus + "[Organism] AND (\"" + date_from + "\"[PDAT] : \"" + date_to + "\"[PDAT])"

try {
    let result = ncbi_search("nucleotide", query)
    println("Result:   " + str(len(result)))
    println("Expected: 7")
    println("Note:     Count may differ as GenBank is updated over time")
} catch e {
    println("NCBI query failed: " + str(e))
    println("Expected: 7 (from Rosalind sample)")
}

FRMT — Data Formats

solvedbrowser + CLI · online Problem statement Open in the workbench Download .bl

Calls NCBI, so it needs a network connection and its answer can change over time.

# Rosalind: FRMT — Data Formats
# https://rosalind.info/problems/frmt/
#
# Given: A collection of n (n <= 10) GenBank entry IDs.
# Return: The shortest of the strings in FASTA format.

let ids = ["FJ817486", "JX069768", "JX469983"]

try {
    # Fetch each sequence as FASTA string, extract sequence length
    let results = ids |> map(|id| {
        let fasta_str = ncbi_sequence(id)
        # Parse: first line is header (>...), rest is sequence
        let lines = split(fasta_str, "\n")
        let header = lines[0]
        # drop(_, 1) removes the ">" header. tail() would not: it returns the
        # *last* n lines and defaults to 5, so the length below counted only
        # the tail of each record and the comparison was decided by luck.
        let seq = drop(lines, 1) |> join("")
        {id: id, header: header, seq_len: len(seq)}
    })

    # Find shortest
    let shortest = results |> reduce(|a, b| {
        if a.seq_len <= b.seq_len then a else b
    })

    println("Shortest: " + shortest.id + " (" + str(shortest.seq_len) + " bp)")
    println("Header:   " + shortest.header)
    println("Expected: JX469983.1 is the shortest")
} catch e {
    println("NCBI fetch failed: " + str(e))
    println("Expected: JX469983.1 is the shortest sequence")
}

MEME — New Motif Discovery

partialbrowser + CLI Problem statement Open in the workbench Download .bl

Partial: no probabilistic motif discovery; finds exact shared substrings instead of a MEME position-weight motif.

# Rosalind: MEME — New Motif Discovery
# https://rosalind.info/problems/meme/
#
# Given: FASTA protein sequences sharing a common motif (length >= 20).
# Return: Regular expression for the best-scoring motif.
#
# Note: This problem requires the MEME Suite external tool.
# BioLang can find exact shared substrings but not probabilistic motifs
# with position-specific variation. We demonstrate substring search instead.

let seqs = [
    "MSNLHTHHLRLQAKLHEQHPGLHVSPFEVDFDMKAIDLLGKYNNKRGFYKTLRAVRMFMILPTAIAFFDSWTLNMDLWWCWIPVHKNPHSFLKTWSPAAGHRGWQFDHNFFKDMGHHYLDQKRALQHIRHYQHVCEDWMYRCRSIWEHTPYVSHNDLCLWMAPRPCEQMISRVSSMWTLDGFPFHFRMHYPQNHESRHGQKQPLSYNFHICDDRHFGMHFPHPQNNHQEHLSHHDCMTQVYAH",
    "MCYRMTAWSSGKQFNKGADIFRMSFDLWWCWIPVHKNPHSFLKTWSPAAGHRGWQFDHNFFKQPQHVIWNHCQPFQHQMHRNFATMDYNAHKWMLRSLAGKFLDLGYRQMSRVLQHVINATPHESYNFHAKQRLSYIPVNEKIQPQETSWQVEEPF",
    "MSHKADMRSSRKKCSIGIDLWWCWIPVHKKPHSFLKTWSPAAGHRGWQFDHNFFKALGEKVRQTEKQEYFLEKFPHHEQFMISEPQKQESRCWRAVMKPEDAYNEIQTLGKQHCHFWQRHMIFVQKGVKAVQNWLSFRYTQCPYRGSQR"
]

# Find shared substrings of length >= 20 using a simple sliding window
let min_motif = 20
let first_seq = seqs[0]
let shared = []

let i = 0
while i <= len(first_seq) - min_motif {
    let candidate = substr(first_seq, i, min_motif)
    let found_in_all = true
    for seq in seqs {
        if !(seq |> contains(candidate)) then
            found_in_all = false
    }
    if found_in_all then
        shared = shared + [candidate]
    i = i + 1
}

if len(shared) > 0 then {
    println("Shared substring(s) of length " + str(min_motif) + ":")
    # Show unique shared substrings
    let seen = []
    shared |> each(|s| {
        if !(seen |> contains(s)) then {
            println("  " + s)
            seen = seen + [s]
        }
    })
}

println("\nExpected (from MEME): DLWWCWIPVHK[NK]PHSFLKTWSPAAGHRGWQFDHNFF")
println("Note: MEME finds probabilistic motifs with position-specific variants;")
println("      BioLang finds exact shared substrings as an approximation.")

fn test_meme_shared_substrings() {
    assert len(shared) == 6, "MEME: expected 6 shared 20-mers, got " + str(len(shared))
    assert shared[0] == "PHSFLKTWSPAAGHRGWQFD", "MEME: unexpected first shared substring " + shared[0]
}

NEED — Pairwise Global Alignment

solvedbrowser + CLI · online Problem statement Open in the workbench Download .bl

Calls NCBI, so it needs a network connection and its answer can change over time.

Reproduces the EMBOSS Needle score of 257 using align() with affine gaps (match 5, mismatch -4, gap_open -9, gap_extend -1). Asserted, but excluded from the hermetic CI gate because it fetches from NCBI.

# Rosalind: NEED — Pairwise Global Alignment
# https://rosalind.info/problems/need/
#
# Given: Two GenBank IDs.
# Return: Maximum global alignment score (DNAfull matrix, gap_open=10, gap_extend=1).

let ids = ["JX205496.1", "JX469991.1"]

# Fetch each record and drop the ">" header line. drop(_, 1) rather than
# tail(): tail() returns the *last* n lines and defaults to 5.
let sequences = ids |> map(|id| drop(split(ncbi_sequence(id), "\n"), 1) |> join(""))

println("Sequences:")
range(0, len(ids)) |> each(|i| println("  " + ids[i] + ": " + str(len(sequences[i])) + " bp"))

# DNAfull scores an ACGT match +5 and a mismatch -4.
#
# EMBOSS charges `gapopen` for the first position of a gap and `gapextend` for
# each further one. This aligner charges `gap_open + gap_extend` to start a gap
# and `gap_extend` to continue it, so gapopen=10 / gapextend=1 becomes
# gap_open = -9 and gap_extend = -1.
let result = align(sequences[0], sequences[1], "global", 5, -4, -1, -9)

println("\nResult:   " + str(result.score))
println("Expected: 257")
println("Match:    " + str(result.score == 257))

println("\nAlignment detail:")
println("  Identity: " + str(result.identity))
println("  Gaps:     " + str(result.gaps))

fn test_need_global_alignment_score() {
    assert result.score == 257, "NEED: expected 257, got " + str(result.score)
}

TFSQ — FASTQ format introduction

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: TFSQ — FASTQ Format Introduction
# https://rosalind.info/problems/tfsq/
#
# Given: FASTQ entries.
# Return: Corresponding FASTA records (strip quality, change @ to >).

# Sample FASTQ data
let fastq_id = "SEQ_ID"
let fastq_seq = "GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT"

# Convert to FASTA format
let fasta_text = ">" + fastq_id + "\n" + fastq_seq

println("Result:")
println(fasta_text)
println("")
println("Expected:")
println(">SEQ_ID")
println("GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT")

let ok = fasta_text == ">SEQ_ID\nGATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT"
println("\nMatch: " + str(ok))

fn test_tfsq_fastq_to_fasta() {
    assert ok, "TFSQ: FASTA conversion did not match the expected record"
}

PHRE — Read Quality Distribution

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: PHRE — Read Quality Distribution
# https://rosalind.info/problems/phre/
#
# Given: A quality threshold and FASTQ entries.
# Return: Number of reads whose average quality is below the threshold.

let threshold = 28

# Sample reads with Phred33 quality strings
let quality_strings = [
    "6.3536354;.151<211/0?::6/-2051)-*\"40/.,+%)",
    "AH@FGGGJ<GB<<9:GD=D@GG9=?A@DC=;:?>839/4856",
    "@DJEJEA?JHJ@8?F?IA3=;8@C95=;=?;>D/:;74792."
]

# Parse Phred33: each character's ASCII value minus 33 gives quality score
let below_count = 0
for qual_str in quality_strings {
    let chars = split(qual_str, "")
    let scores = chars |> filter(|c| c != "") |> map(|c| ascii(c) - 33)
    let avg = (scores |> reduce(|a, b| a + b)) / len(scores)
    if avg < threshold then
        below_count = below_count + 1
}

println("Result:   " + str(below_count))
println("Expected: 1")
println("Match:    " + str(below_count == 1))

fn test_phre_reads_below_threshold() {
    assert below_count == 1, "PHRE: expected 1, got " + str(below_count)
}

PTRA — Protein Translation

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: PTRA — Protein Translation
# https://rosalind.info/problems/ptra/
#
# Given: A DNA string s and a protein string p.
# Return: The genetic code variant table number used for translation.
#
# BioLang uses standard genetic code (table 1). We verify by translating
# and comparing the result.

let dna_seq = dna"ATGGCCATGGCGCCCAGAACTGAGATCAATAGTACCCGTATTAACGGGTGA"
let expected_protein = protein"MAMAPRTEINSTRING"

# translate() stops at the first stop codon
let result = translate(dna_seq)

println("Translated: " + str(result))
println("Expected:   " + str(expected_protein))

# Standard genetic code (table 1) should produce the expected protein
let grid = if result == expected_protein then 1 else 0
println("Table:      " + str(grid))
println("Expected:   1")
println("Match:      " + str(grid == 1))

fn test_ptra_standard_genetic_code() {
    assert grid == 1, "PTRA: standard code (grid 1) should reproduce " + str(expected_protein)
}

FILT — Read Filtration by Quality

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: FILT — Read Filtration by Quality
# https://rosalind.info/problems/filt/
#
# Given: Quality threshold q, percentage p, and FASTQ entries.
# Return: Number of reads where at least p% of bases have quality >= q.

let q = 20
let p = 90

let reads = [
    {id: "Rosalind_0049_1", seq: "GCAGAGACCAGTAGATGTGTTTGCGGACGGTCGGGCTCCATGTGACACAG",
     qual: "FD@@;C<AI?4BA:=>C<G=:AE=><A??>764A8B797@A:58:527+,"},
    {id: "Rosalind_0049_2", seq: "AATGGGGGGGGGAGACAAAATACGGCTAAGGCAGGGGTCCTTGATGTCAT",
     qual: "1<<65:793967<4:92568-34:.>1;2752)24')*15;1,.3*3+*!"},
    {id: "Rosalind_0049_3", seq: "ACCCCATACGGCGAGCGTCAGCATCTGATATCCTCTTTCAATCCTAGCTA",
     qual: "B:EI>JDB5=>DA?E6B@@CA?C;=;@@C:6D:3=@49;@87;::;;?8+"}
]

let passing = 0
for read in reads {
    let chars = split(read.qual, "") |> filter(|c| c != "")
    let scores = chars |> map(|c| ascii(c) - 33)
    let good = scores |> filter(|s| s >= q) |> count()
    let pct = (good * 100) / len(scores)
    if pct >= p then
        passing = passing + 1
}

println("Result:   " + str(passing))
println("Expected: 2")
println("Match:    " + str(passing == 2))

fn test_filt_reads_passing() {
    assert passing == 2, "FILT: expected 2, got " + str(passing)
}

RVCO — Complementing a Strand of DNA

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: RVCO — Complementing a Strand of DNA
# https://rosalind.info/problems/rvco/
#
# Given: A collection of n (n <= 10) DNA strings.
# Return: The number of strings that match their own reverse complement.

let sequences = [
    dna"ATAT",
    dna"GCATA"
]

let palindrome_count = 0
for seq in sequences {
    let rc = reverse_complement(seq)
    if str(seq) == str(rc) then
        palindrome_count = palindrome_count + 1
}

println("Result:   " + str(palindrome_count))
println("Expected: 1")
println("Match:    " + str(palindrome_count == 1))

# Verify: ATAT -> rev_comp = ATAT (palindrome), GCATA -> rev_comp = TATGC (not)
println("\nDetail:")
for seq in sequences {
    let rc = reverse_complement(seq)
    let is_palindrome = str(seq) == str(rc)
    println("  " + str(seq) + " -> rc=" + str(rc) + " palindrome=" + str(is_palindrome))
}

fn test_rvco_palindrome_count() {
    assert palindrome_count == 1, "RVCO: expected 1, got " + str(palindrome_count)
}

SUBO — Suboptimal Local Alignment

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Brute-force O(n^3) window scan; ~25 s. A motif-index approach would be the natural follow-up.

# Rosalind: SUBO — Suboptimal Local Alignment
# https://rosalind.info/problems/subo/
#
# Given: Two DNA strings with a shared inexact repeat (32-40 bp, <=3 changes).
# Return: Total occurrences of the repeat in each string.

let seq1 = "GACTCCTTTGTTTGCCTTAAATAGATACATATTTACTCTTGACTCTTTTGTTGGCCTTAAATAGATACATATTTGTGCGACTCCACGAGTGATTCGTA"
let seq2 = "ATGGACTCCTTTGTTTGCCTTAAATAGATACATATTCAACAAGTGTGCACTTAGCCTTGCCGACTCCTTTGTTTGCCTTAAATAGATACATATTTG"

# Strategy: find all 32-bp substrings of seq1 that appear in seq2 with <=3 mismatches
let motif_len = 33

# Helper: count mismatches between two strings of equal length
# (hamming_distance works on sequences; we use manual counting for strings)
let s1 = seq1
let s2 = seq2

# Find the best shared motif by checking each 33-bp window of s1 against all windows of s2
let best_motif = ""
let best_total = 0

let i = 0
while i <= len(s1) - motif_len {
    let candidate = substr(s1, i, motif_len)

    # Count approximate matches in s2
    let hits_s2 = 0
    let j = 0
    while j <= len(s2) - motif_len {
        let target = substr(s2, j, motif_len)
        let dist = hamming_distance(candidate, target)
        if dist <= 3 then
            hits_s2 = hits_s2 + 1
        j = j + 1
    }

    if hits_s2 > 0 then {
        # Also count in s1
        let hits_s1 = 0
        let k = 0
        while k <= len(s1) - motif_len {
            let target = substr(s1, k, motif_len)
            let dist = hamming_distance(candidate, target)
            if dist <= 3 then
                hits_s1 = hits_s1 + 1
            k = k + 1
        }
        let total = hits_s1 + hits_s2
        if total > best_total then {
            best_total = total
            best_motif = candidate
        }
    }
    i = i + 1
}

# Now count non-overlapping occurrences using the best motif
# Count all approximate matches (overlapping) — Rosalind counts overlapping instances
let count1 = 0
let i = 0
while i <= len(s1) - motif_len {
    let window = substr(s1, i, motif_len)
    if hamming_distance(best_motif, window) <= 3 then
        count1 = count1 + 1
    i = i + 1
}

let count2 = 0
let j = 0
while j <= len(s2) - motif_len {
    let window = substr(s2, j, motif_len)
    if hamming_distance(best_motif, window) <= 3 then
        count2 = count2 + 1
    j = j + 1
}

println("Motif:    " + best_motif)
println("Result:   " + str(count1) + " " + str(count2))
println("Expected: 2 2")
println("Match:    " + str(count1 == 2 && count2 == 2))

fn test_subo_repeat_occurrences() {
    assert count1 == 2 && count2 == 2, "SUBO: expected 2 2, got " + str(count1) + " " + str(count2)
}

BPHR — Base Quality Distribution

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BPHR — Base Quality Distribution
# https://rosalind.info/problems/bphr/
#
# Given: FASTQ file and quality threshold q.
# Return: Number of positions where mean base quality falls below q.

let threshold = 26

let quality_strings = [
    ">?F?@6<C<HF?<85486B;85:8488/2/",
    "@J@H@>B9:B;<D==:<;:,<::?463-,,",
    "=88;99637@5,4664-65)/?4-2+)$)$",
    "<@BGE@8C9=B9:B<>>>7?B>7:02+33."
]

# All reads should be the same length
let read_len = len(split(quality_strings[0], "") |> filter(|c| c != ""))

# Calculate mean quality at each position across all reads
let below_count = 0
let i = 0
while i < read_len {
    let total = 0
    for qual_str in quality_strings {
        let chars = split(qual_str, "") |> filter(|c| c != "")
        total = total + ascii(chars[i]) - 33
    }
    let mean = total / len(quality_strings)
    if mean < threshold then
        below_count = below_count + 1
    i = i + 1
}

println("Result:   " + str(below_count))
println("Expected: 17")
println("Match:    " + str(below_count == 17))

fn test_bphr_positions_below_threshold() {
    assert below_count == 17, "BPHR: expected 17, got " + str(below_count)
}

CLUS — Global Multiple Alignment

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: CLUS — Global Multiple Alignment
# https://rosalind.info/problems/clus/
#
# Given: Set of DNA strings in FASTA format.
# Return: ID of the string most different from the others.
#
# We compute pairwise edit distances and find the sequence with
# the highest average distance to all others.

let sequences = [
    {id: "Rosalind_18", seq: dna"GACATGTTTGTTTGCCTTAAACTCGTGGCGGCCTAGCCGTAAGTTAAG"},
    {id: "Rosalind_23", seq: dna"ACTCATGTTTGTTTGCCTTAAACTCTTGGCGGCTTAGCCGTAACTTAAG"},
    {id: "Rosalind_51", seq: dna"TCCTATGTTTGTTTGCCTCAAACTCTTGGCGGCCTAGCCGTAAGGTAAG"},
    {id: "Rosalind_7",  seq: dna"CACGTCTGTTCGCCTAAAACTTTGATTGCCGGCCTACGCTAGTTAGTTA"},
    {id: "Rosalind_28", seq: dna"GGGGTCATGGCTGTTTGCCTTAAACCCTTGGCGGCCTAGCCGTAATGTTT"}
]

# Compute pairwise distances and find the most distant sequence
let most_different_id = ""
let max_avg_dist = 0

for i_seq in sequences {
    let total_dist = 0
    let pair_count = 0
    for j_seq in sequences {
        if i_seq.id != j_seq.id then {
            let dist = edit_distance(str(i_seq.seq), str(j_seq.seq))
            total_dist = total_dist + dist
            pair_count = pair_count + 1
        }
    }
    let avg_dist = total_dist / pair_count
    if avg_dist > max_avg_dist then {
        max_avg_dist = avg_dist
        most_different_id = i_seq.id
    }
}

println("Result:   " + most_different_id)
println("Expected: Rosalind_7")
println("Match:    " + str(most_different_id == "Rosalind_7"))

fn test_clus_most_different_sequence() {
    assert most_different_id == "Rosalind_7", "CLUS: expected Rosalind_7, got " + most_different_id
}

ORFR — Finding Genes with ORFs

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: ORFR — Finding Genes with ORFs
# https://rosalind.info/problems/orfr/
#
# Given: A DNA string s of length at most 1 kbp.
# Return: The longest protein string from any ORF (all six reading frames).

let s = dna"AGCCATGTAGCTAACTCAGGTTACATGGGGATGACCCCGCGACTTGGATTAGAGTCTCTTTTGGAATAAGCCTGAATGATCCGAGTAGCATCTCAG"

# find_orfs searches 3 forward reading frames; we also need the reverse complement
let fwd_orfs = find_orfs(s, 1)
let rc = reverse_complement(s)
let rev_orfs = find_orfs(rc, 1)
let all_orfs = fwd_orfs + rev_orfs

# Find the longest protein
let longest = all_orfs |> reduce(|a, b| {
    if seq_len(a.protein) >= seq_len(b.protein) then a else b
})

let result = longest.protein
let expected = protein"MLLGSFRLIPKETLIQVAGSSPCNLS"

println("Result:   " + str(result))
println("Expected: " + str(expected))
println("Match:    " + str(result == expected))

println("\nAll ORFs found (" + str(len(all_orfs)) + "):")
all_orfs |> each(|o| println("  frame=" + str(o.frame) + " len=" + str(seq_len(o.protein)) + " " + str(o.protein)))

fn test_orfr_longest_protein() {
    assert result == expected, "ORFR: expected " + str(expected) + ", got " + str(result)
}

BFIL — Base Filtration by Quality

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BFIL — Base Filtration by Quality
# https://rosalind.info/problems/bfil/
#
# Given: FASTQ file and quality threshold q (Phred33).
# Return: FASTQ with leading and trailing low-quality bases trimmed.

let q = 20

let reads = [
    {id: "Rosalind_0049",
     seq:  "GCAGAGACCAGTAGATGTGTTTGCGGACGGTCGGGCTCCATGTGACACAG",
     qual: "FD@@;C<AI?4BA:=>C<G=:AE=><A??>764A8B797@A:58:527+,"},
    {id: "Rosalind_0049",
     seq:  "AATGGGGGGGGGAGACAAAATACGGCTAAGGCAGGGGTCCTTGATGTCAT",
     qual: "1<<65:793967<4:92568-34:.>1;2752)24')*15;1,.3*3+*!"},
    {id: "Rosalind_0049",
     seq:  "ACCCCATACGGCGAGCGTCAGCATCTGATATCCTCTTTCAATCCTAGCTA",
     qual: "B:EI>JDB5=>DA?E6B@@CA?C;=;@@C:6D:3=@49;@87;::;;?8+"}
]

let trimmed_reads = []

println("Result:")
for read in reads {
    let chars_seq = split(read.seq, "") |> filter(|c| c != "")
    let chars_qual = split(read.qual, "") |> filter(|c| c != "")
    let scores = chars_qual |> map(|c| ascii(c) - 33)

    # Find first position from left with quality >= q
    let start = 0
    while start < len(scores) && scores[start] < q {
        start = start + 1
    }
    # Find last position from right with quality >= q
    let end_pos = len(scores) - 1
    while end_pos >= 0 && scores[end_pos] < q {
        end_pos = end_pos - 1
    }

    let trimmed_seq = chars_seq |> slice(start, end_pos + 1) |> join("")
    let trimmed_qual = chars_qual |> slice(start, end_pos + 1) |> join("")
    trimmed_reads = push(trimmed_reads, trimmed_seq)

    println("@" + read.id)
    println(trimmed_seq)
    println("+")
    println(trimmed_qual)
}

println("\nExpected:")
println("@Rosalind_0049")
println("GCAGAGACCAGTAGATGTGTTTGCGGACGGTCGGGCTCCATGTGACAC")
println("+")
println("FD@@;C<AI?4BA:=>C<G=:AE=><A??>764A8B797@A:58:527")
println("@Rosalind_0049")
println("ATGGGGGGGGGAGACAAAATACGGCTAAGGCAGGGGTCCT")
println("+")
println("<<65:793967<4:92568-34:.>1;2752)24')*15;")
println("@Rosalind_0049")
println("ACCCCATACGGCGAGCGTCAGCATCTGATATCCTCTTTCAATCCTAGCT")
println("+")
println("B:EI>JDB5=>DA?E6B@@CA?C;=;@@C:6D:3=@49;@87;::;;?8")

fn test_bfil_trims_low_quality_ends() {
    let expected = [
        "GCAGAGACCAGTAGATGTGTTTGCGGACGGTCGGGCTCCATGTGACAC",
        "ATGGGGGGGGGAGACAAAATACGGCTAAGGCAGGGGTCCT",
        "ACCCCATACGGCGAGCGTCAGCATCTGATATCCTCTTTCAATCCTAGCT"
    ]
    assert len(trimmed_reads) == 3, "BFIL: expected 3 reads, got " + str(len(trimmed_reads))
    let i = 0
    while i < 3 {
        assert trimmed_reads[i] == expected[i], "BFIL: read " + str(i + 1) + " trimmed to " + trimmed_reads[i]
        i = i + 1
    }
}