Skip to content

Commit

Permalink
nucleotide-count: add generator and update example (#1014)
Browse files Browse the repository at this point in the history
* implemented generator and new example for pig latin

* go fmt

* small refactoring on @ferhatelmas suggestions

* each test gets a description printed and PASS or FAIL

* implemented test generator for nucleotide-count exercise (issue #605)

* added hints.md and changed .expected type in generator

* restored original nucleotide_count.go stub and adjusted expected type for tests

* small refactoring in example.go

* problem specification changed format; check for unexpected type in generator
  • Loading branch information
ilmanzo authored and ferhatelmas committed Jan 16, 2018
1 parent 35f0d77 commit c1d260a
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 109 deletions.
87 changes: 87 additions & 0 deletions exercises/nucleotide-count/.meta/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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
Input struct {
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:
log.Fatalf("unexpected type %T for %v", t, v)
}

}
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" .Input.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: 879a096 nucleotide-count: Apply new "input" policy
// Problem Specifications Version: 1.3.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)
}
}

0 comments on commit c1d260a

Please sign in to comment.