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 Histogram
errorExpected bool
}{
{{range .Cases}}{
description: {{printf "%q" .Description}},
strand: {{printf "%#v" .Strand}},
{{if .ErrorExpected}}errorExpected: true,
{{else}}expected: Histogram{ {{.SortedMapString}} },
{{- end}}
},
{{end}}{{end}}
}
`
20 changes: 20 additions & 0 deletions exercises/nucleotide-count/.meta/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Implementation

You should define a custom type 'DNA' with a function 'Counts' that outputs two values:

- a frequency count for the given DNA strand
- an error (if there are invalid nucleotides)

Which is a good type for a DNA strand ?

Which is the best Go types to represent the output values ?

Take a look at the test cases to get a hint about what could be the possible inputs.


## note about the tests
You may be wondering about the `cases_test.go` file. We explain it in the
[leap exercise][leap-exercise-readme].

[leap-exercise-readme]: https://github.com/exercism/go/blob/master/exercises/leap/README.md

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 Histogram
errorExpected bool
}{
{
description: "empty strand",
strand: "",
expected: Histogram{'A': 0, 'C': 0, 'G': 0, 'T': 0},
},
{
description: "can count one nucleotide in single-character input",
strand: "G",
expected: Histogram{'A': 0, 'C': 0, 'G': 1, 'T': 0},
},
{
description: "strand with repeated nucleotide",
strand: "GGGGGGG",
expected: Histogram{'A': 0, 'C': 0, 'G': 7, 'T': 0},
},
{
description: "strand with multiple nucleotides",
strand: "AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC",
expected: Histogram{'A': 20, 'C': 12, 'G': 17, 'T': 21},
},
{
description: "strand with invalid nucleotides",
strand: "AGXXACT",
errorExpected: true,
},
}
22 changes: 6 additions & 16 deletions exercises/nucleotide-count/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,25 @@ 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
type Histogram map[byte]int

const validNucleotides = "ACGT"

// 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)) {
return 0, errors.New("dna: invalid nucleotide " + string(nucleotide))
}

return strings.Count(string(dna), string(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) {
var total int
h := Histogram{}
result := make(Histogram)

for i := range validNucleotides {
nucleotide := validNucleotides[i]
h[nucleotide], _ = dna.Count(nucleotide)
total += h[nucleotide]
result[nucleotide] = strings.Count(string(dna), string(nucleotide))
total += result[nucleotide]
}
if total != len(dna) {
return nil, errors.New("dna: contains invalid nucleotide")
}
return h, nil
return result, nil
}
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)
}
}