Avatar of artemkorsakov

artemkorsakov's solution

to Scale Generator in the Go Track

Published at Feb 22 2019 · 0 comments
Instructions
Test suite
Solution

Note:

This exercise has changed since this solution was written.

Given a tonic, or starting note, and a set of intervals, generate the musical scale starting with the tonic and following the specified interval pattern.

Scales in Western music are based on the chromatic (12-note) scale. This scale can be expressed as the following group of pitches:

A, A#, B, C, C#, D, D#, E, F, F#, G, G#

A given sharp note (indicated by a #) can also be expressed as the flat of the note above it (indicated by a b) so the chromatic scale can also be written like this:

A, Bb, B, C, Db, D, Eb, E, F, Gb, G, Ab

The major and minor scale and modes are subsets of this twelve-pitch collection. They have seven pitches, and are called diatonic scales. The collection of notes in these scales is written with either sharps or flats, depending on the tonic. Here is a list of which are which:

No Sharps or Flats: C major a minor

Use Sharps: G, D, A, E, B, F# major e, b, f#, c#, g#, d# minor

Use Flats: F, Bb, Eb, Ab, Db, Gb major d, g, c, f, bb, eb minor

The diatonic scales, and all other scales that derive from the chromatic scale, are built upon intervals. An interval is the space between two pitches.

The simplest interval is between two adjacent notes, and is called a "half step", or "minor second" (sometimes written as a lower-case "m"). The interval between two notes that have an interceding note is called a "whole step" or "major second" (written as an upper-case "M"). The diatonic scales are built using only these two intervals between adjacent notes.

Non-diatonic scales can contain other intervals. An "augmented first" interval, written "A", has two interceding notes (e.g., from A to C or Db to E). There are also smaller and larger intervals, but they will not figure into this exercise.

Running the tests

To run the tests run the command go test from within the exercise directory.

If the test suite contains benchmarks, you can run these with the --bench and --benchmem flags:

go test -v --bench . --benchmem

Keep in mind that each reviewer will run benchmarks on a different machine, with different specs, so the results from these benchmark tests may vary.

Further information

For more detailed information about the Go track, including how to get help if you're having trouble, please visit the exercism.io Go language page.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

cases_test.go

package scale

// Source: exercism/problem-specifications
// Commit: 1b1c0e4 scale generator: Add canonical data (#1156)
// Problem Specifications Version: 1.0.0

type scaleTest struct {
	description string
	tonic       string
	interval    string
	expected    []string
}

var scaleTestCases = []scaleTest{
	{
		description: "Chromatic scale with sharps",
		tonic:       "C",
		interval:    "",
		expected:    []string{"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"},
	},
	{
		description: "Chromatic scale with flats",
		tonic:       "F",
		interval:    "",
		expected:    []string{"F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E"},
	},
	{
		description: "Simple major scale",
		tonic:       "C",
		interval:    "MMmMMMm",
		expected:    []string{"C", "D", "E", "F", "G", "A", "B"},
	},
	{
		description: "Major scale with sharps",
		tonic:       "G",
		interval:    "MMmMMMm",
		expected:    []string{"G", "A", "B", "C", "D", "E", "F#"},
	},
	{
		description: "Major scale with flats",
		tonic:       "F",
		interval:    "MMmMMMm",
		expected:    []string{"F", "G", "A", "Bb", "C", "D", "E"},
	},
	{
		description: "Minor scale with sharps",
		tonic:       "f#",
		interval:    "MmMMmMM",
		expected:    []string{"F#", "G#", "A", "B", "C#", "D", "E"},
	},
	{
		description: "Minor scale with flats",
		tonic:       "bb",
		interval:    "MmMMmMM",
		expected:    []string{"Bb", "C", "Db", "Eb", "F", "Gb", "Ab"},
	},
	{
		description: "Dorian mode",
		tonic:       "d",
		interval:    "MmMMMmM",
		expected:    []string{"D", "E", "F", "G", "A", "B", "C"},
	},
	{
		description: "Mixolydian mode",
		tonic:       "Eb",
		interval:    "MMmMMmM",
		expected:    []string{"Eb", "F", "G", "Ab", "Bb", "C", "Db"},
	},
	{
		description: "Lydian mode",
		tonic:       "a",
		interval:    "MMMmMMm",
		expected:    []string{"A", "B", "C#", "D#", "E", "F#", "G#"},
	},
	{
		description: "Phrygian mode",
		tonic:       "e",
		interval:    "mMMMmMM",
		expected:    []string{"E", "F", "G", "A", "B", "C", "D"},
	},
	{
		description: "Locrian mode",
		tonic:       "g",
		interval:    "mMMmMMM",
		expected:    []string{"G", "Ab", "Bb", "C", "Db", "Eb", "F"},
	},
	{
		description: "Harmonic minor",
		tonic:       "d",
		interval:    "MmMMmAm",
		expected:    []string{"D", "E", "F", "G", "A", "Bb", "Db"},
	},
	{
		description: "Octatonic",
		tonic:       "C",
		interval:    "MmMmMmMm",
		expected:    []string{"C", "D", "D#", "F", "F#", "G#", "A", "B"},
	},
	{
		description: "Hexatonic",
		tonic:       "Db",
		interval:    "MMMMMM",
		expected:    []string{"Db", "Eb", "F", "G", "A", "B"},
	},
	{
		description: "Pentatonic",
		tonic:       "A",
		interval:    "MMAMA",
		expected:    []string{"A", "B", "C#", "E", "F#"},
	},
	{
		description: "Enigmatic",
		tonic:       "G",
		interval:    "mAMMMmm",
		expected:    []string{"G", "G#", "B", "C#", "D#", "F", "F#"},
	},
}

scale_generator_test.go

package scale

import (
	"fmt"
	"testing"
)

func TestScale(t *testing.T) {
	for _, test := range scaleTestCases {
		actual := Scale(test.tonic, test.interval)
		if fmt.Sprintf("%q", actual) != fmt.Sprintf("%q", test.expected) {
			t.Fatalf("FAIL: %s - Scale(%q, %q)\nExpected: %q\nActual: %q", test.description, test.tonic, test.interval, test.expected, actual)
		}
		t.Logf("PASS: %s", test.description)
	}
}

func BenchmarkScale(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for _, test := range scaleTestCases {
			Scale(test.tonic, test.interval)
		}
	}
}
package scale

import (
	"errors"
	"strings"
)

var sharps = []string{"A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"}
var flats = []string{"A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"}
var isSharps = []string{"C", "G", "D", "A", "a", "E", "B", "F#", "e", "b", "f#", "c#", "g#", "d#"}
var isFlats = []string{"F", "Bb", "Eb", "Ab", "Db", "Gb", "d", "g", "c", "f", "bb", "eb"}

// Scale generates the musical scale starting with the tonic and following the specified interval pattern.
func Scale(tonic string, interval string) []string {
	scale := GetSharpsOrFlats(tonic)
	return GetScale(scale, tonic, interval)
}

// GetScale generates the musical scale starting with the tonic.
func GetScale(scale []string, tonic string, interval string) []string {
	result := make([]string, 0)
	index := IndexIgnoreCase(scale, tonic)
	if len(interval) == 0 {
		for i := 0; i < len(scale); i++ {
			result = append(result, scale[(index+i)%len(scale)])
		}
		return result
	}
	result = append(result, scale[index])
	for _, c := range interval {
		i, err := GetInterval(c)
		index = (index + i) % len(scale)
		if scale[index] == result[0] {
			return result
		}
		if err == nil {
			result = append(result, scale[index])
		}
	}
	return result
}

// GetSharpsOrFlats returns sharps if tonic is used for sharps or flats.
func GetSharpsOrFlats(tonic string) []string {
	if Index(isSharps, tonic) != -1 {
		return sharps
	}
	if Index(isFlats, tonic) != -1 {
		return flats
	}
	return nil
}

// Index returns the index of the first instance of substr in array, or -1 if substr is not present in array.
func Index(array []string, substr string) int {
	for i, str := range array {
		if str == substr {
			return i
		}
	}
	return -1
}

// IndexIgnoreCase returns the index of the first instance ignore case of substr in array, or -1 if substr is not present in array.
func IndexIgnoreCase(array []string, substr string) int {
	for i, str := range array {
		if strings.ToLower(str) == strings.ToLower(substr) {
			return i
		}
	}
	return -1
}

// GetInterval returns the interval.
func GetInterval(c rune) (int, error) {
	if c == 'm' {
		return 1, nil
	}
	if c == 'M' {
		return 2, nil
	}
	if c == 'A' {
		return 3, nil
	}
	return 0, errors.New("unknown rune")
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?