Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nucleotide-count: add generator and update example #1014

Merged
merged 11 commits into from
Jan 16, 2018
84 changes: 84 additions & 0 deletions exercises/nucleotide-count/.meta/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"log"
"sort"
"strconv"
"strings"
"text/template"

"../../../gen"
)

func main() {
t, err := template.New("").Parse(tmpl)
if err != nil {
log.Fatal(err)
}
var j js
if err := gen.Gen("nucleotide-count", &j, t); err != nil {
log.Fatal(err)
}
}

// The JSON structure we expect to be able to unmarshal into
type js struct {
exercise string
version string
Cases []struct {
Description string
Cases []OneCase
}
}

// OneCase represents each test case
type OneCase struct {
Description string
Property string
Strand string
Expected map[string]interface{}
}

// ErrorExpected returns true if an error should be raised
func (c OneCase) ErrorExpected() bool {
_, exists := c.Expected["error"]
return exists
}

// SortedMapString collects key:values for a map in sorted order
func (c OneCase) SortedMapString() string {
strs := make([]string, 0, len(c.Expected))
for k, v := range c.Expected {
switch t := v.(type) {
case float64:
strs = append(strs, `"`+k+`": `+strconv.FormatFloat(t, 'f', -1, 64))
default:
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah,

default:

That line is unnecessary. Can remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok .. I made it a little more useful :)

in the meantime the problem specification json structure has changed, so I updated also the generator

}
sort.Strings(strs)
return strings.Join(strs, ",")
}

// template applied to above data structure generates the Go test cases
var tmpl = `package dna

{{.Header}}

{{range .J.Cases}}// {{.Description}}
var testCases = []struct {
description string
strand string
expected map[string]int
errorExpected bool
}{
{{range .Cases}}{
description: {{printf "%q" .Description}},
strand: {{printf "%#v" .Strand}},
{{if .ErrorExpected}}errorExpected: true,
{{else}}expected: map[string]int{ {{.SortedMapString}} },
{{- end}}
},
{{end}}{{end}}
}
`
39 changes: 39 additions & 0 deletions exercises/nucleotide-count/cases_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dna

// Source: exercism/problem-specifications
// Commit: bbdcaee nucleotide-count: fix wrong order introduced in #951
// Problem Specifications Version: 1.2.0

// count all nucleotides in a strand
var testCases = []struct {
description string
strand string
expected map[string]int
errorExpected bool
}{
{
description: "empty strand",
strand: "",
expected: map[string]int{"A": 0, "C": 0, "G": 0, "T": 0},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be easy enough to use this expected type ?

expected map[byte]int

and then generate this as

expected: map[byte]int{'A': 0, 'C': 0, 'G': 0, 'T': 0},

A nucleotide is one single letter after all. Perhaps too nit-picky, but it seems to match the exercise problem better.
What do you think ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me. Always a fan of more content in hints.md as well.

},
{
description: "can count one nucleotide in single-character input",
strand: "G",
expected: map[string]int{"A": 0, "C": 0, "G": 1, "T": 0},
},
{
description: "strand with repeated nucleotide",
strand: "GGGGGGG",
expected: map[string]int{"A": 0, "C": 0, "G": 7, "T": 0},
},
{
description: "strand with multiple nucleotides",
strand: "AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC",
expected: map[string]int{"A": 20, "C": 12, "G": 17, "T": 21},
},
{
description: "strand with invalid nucleotides",
strand: "AGXXACT",
errorExpected: true,
},
}
31 changes: 19 additions & 12 deletions exercises/nucleotide-count/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,42 @@ import (
"strings"
)

// Histogram is a mapping from nucleotide to its count in given DNA
type Histogram map[byte]int

// DNA is a list of nucleotides
type DNA string

const validNucleotides = "ACGT"
var validNucleotides = []string{"A", "C", "G", "T"}

func isValidNucleotide(nucleotide string) bool {
for _, n := range validNucleotides {
if nucleotide == n {
return true
}
}
return false
}

// Count counts number of occurrences of given nucleotide in given DNA
func (dna DNA) Count(nucleotide byte) (count int, err error) {
if !strings.Contains(validNucleotides, string(nucleotide)) {
func (dna DNA) Count(nucleotide string) (count int, err error) {
if !isValidNucleotide(nucleotide) {
return 0, errors.New("dna: invalid nucleotide " + string(nucleotide))
}

return strings.Count(string(dna), string(nucleotide)), nil
return strings.Count(string(dna), nucleotide), nil
}

// Counts generates a histogram of valid nucleotides in given DNA.
// Returns error if DNA contains invalid nucleotide.
func (dna DNA) Counts() (Histogram, error) {
func (dna DNA) Counts() (result map[string]int, e error) {
var total int
h := Histogram{}
result = make(map[string]int)

for i := range validNucleotides {
nucleotide := validNucleotides[i]
h[nucleotide], _ = dna.Count(nucleotide)
total += h[nucleotide]
result[nucleotide], _ = dna.Count(nucleotide)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps replace

result[nucleotide], _ = dna.Count(nucleotide)

with

if result[nucleotide], e = dna.Count(nucleotide); e != nil {
return nil, e
}

Then var total is unnecessary as well as the later if test using it; it also preserves the specific error from Count.

Copy link
Contributor Author

@ilmanzo ilmanzo Jan 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would fail the test with invalid nucleotides, like "AGXXACT" in the test suite... Because the algorithm only iterates on valid ones "ACGT". I'll remove pointless code :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see now.

total += result[nucleotide]
}
if total != len(dna) {
return nil, errors.New("dna: contains invalid nucleotide")
}
return h, nil
return result, nil
}
24 changes: 0 additions & 24 deletions exercises/nucleotide-count/nucleotide_count.go

This file was deleted.

109 changes: 16 additions & 93 deletions exercises/nucleotide-count/nucleotide_count_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,99 +5,22 @@ import (
"testing"
)

var tallyTests = []struct {
strand DNA
nucleotide byte
expected int
}{
{"", 'A', 0},
{"ACT", 'G', 0},
{"CCCCC", 'C', 5},
{"GGGGGTAACCCGG", 'T', 1},
}

func TestNucleotideCounts(t *testing.T) {
for _, tt := range tallyTests {
if count, err := tt.strand.Count(tt.nucleotide); err != nil {
t.Fatal(err)
} else if count != tt.expected {
t.Fatalf("Got \"%v\", expected \"%v\"", count, tt.expected)
}
}
}

func TestHasErrorForInvalidNucleotides(t *testing.T) {
dna := DNA("GATTACA")
if _, err := dna.Count('X'); err == nil {
t.Fatalf("X is an invalid nucleotide, but no error was raised")
}
}

// In most cases, this test is pointless.
// Very occasionally it matters.
// Just roll with it.
func TestCountingDoesntChangeCount(t *testing.T) {
dna := DNA("CGATTGGG")
dna.Count('T')
count1, err := dna.Count('T')
if err != nil {
t.Fatal(err)
}
count2, err := dna.Count('T')
if err != nil {
t.Fatal(err)
}
if count1 != count2 || count2 != 2 {
t.Fatalf("Got %v, expected %v", []int{count1, count2}, []int{2, 2})
}
}

type histogramTest struct {
strand DNA
expected Histogram
err bool
}

var histogramTests = []histogramTest{
{
"",
Histogram{'A': 0, 'C': 0, 'T': 0, 'G': 0},
false,
},
{
"GGGGGGGG",
Histogram{'A': 0, 'C': 0, 'T': 0, 'G': 8},
false,
},
{
"AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC",
Histogram{'A': 20, 'C': 12, 'T': 21, 'G': 17},
false,
},
{
"GGXXX",
nil,
true,
},
}

func TestSequenceHistograms(t *testing.T) {
for _, tt := range histogramTests {
counts, err := tt.strand.Counts()
if tt.err && err == nil {
t.Fatalf("DNA{ %q }: expected error but didn't get one.", tt.strand)
} else if !tt.err && err != nil {
t.Fatalf("DNA{ %q }: expected no error but got error %s", tt.strand, err.Error())
} else if !tt.err && !reflect.DeepEqual(counts, tt.expected) {
t.Fatalf("DNA{ %q }: Got %v, expected %v", tt.strand, counts, tt.expected)
}
}
}

func BenchmarkSequenceHistograms(b *testing.B) {
for _, tt := range histogramTests {
for i := 0; i < b.N; i++ {
tt.strand.Counts()
func TestCounts(t *testing.T) {
for _, tc := range testCases {
dna := DNA(tc.strand)
s, err := dna.Counts()
if tc.errorExpected {
if err == nil {
t.Fatalf("FAIL: %s\nCounts(%q)\nExpected error\nActual: %#v",
tc.description, tc.strand, s)
}
} else if err != nil {
t.Fatalf("FAIL: %s\nCounts(%q)\nExpected: %#v\nGot error: %q",
tc.description, tc.strand, tc.expected, err)
} else if !reflect.DeepEqual(s, tc.expected) {
t.Fatalf("FAIL: %s\nCounts(%q)\nExpected: %#v\nActual: %#v",
tc.description, tc.strand, tc.expected, s)
}
t.Logf("PASS: %s", tc.description)
}
}