diff --git a/config.json b/config.json index 652b841b0d..c14921289a 100644 --- a/config.json +++ b/config.json @@ -990,6 +990,19 @@ "games" ] }, + { + "slug": "scale-generator", + "uuid": "b9c586e8-998b-4f5d-ab98-a08be29a9f19", + "core": false, + "unlocked_by": "pangram", + "difficulty": 3, + "topics": [ + "loops", + "pattern_recognition", + "strings", + "arrays" + ] + }, { "slug": "connect", "uuid": "2fa2c262-77ae-409b-bfd8-1d643faae772", diff --git a/exercises/scale-generator/.eslintrc b/exercises/scale-generator/.eslintrc new file mode 100644 index 0000000000..2e5a5079a0 --- /dev/null +++ b/exercises/scale-generator/.eslintrc @@ -0,0 +1,26 @@ +{ + "root": true, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 7, + "sourceType": "module" + }, + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings" + ], + "rules": { + "linebreak-style": "off", + + "import/extensions": "off", + "import/no-default-export": "off", + "import/no-unresolved": "off", + "import/prefer-default-export": "off" + } +} diff --git a/exercises/scale-generator/README.md b/exercises/scale-generator/README.md new file mode 100644 index 0000000000..40c15a52da --- /dev/null +++ b/exercises/scale-generator/README.md @@ -0,0 +1,82 @@ +# Scale Generator + +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. + +## Setup + +Go through the setup instructions for Javascript to install the necessary +dependencies: + +[https://exercism.io/tracks/javascript/installation](https://exercism.io/tracks/javascript/installation) + +## Requirements + +Install assignment dependencies: + +```bash +$ npm install +``` + +## Making the test suite pass + +Execute the tests with: + +```bash +$ npm test +``` + +In the test suites all tests but the first have been skipped. + +Once you get a test passing, you can enable the next one by changing `xtest` to +`test`. + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have +completed the exercise. diff --git a/exercises/scale-generator/babel.config.js b/exercises/scale-generator/babel.config.js new file mode 100644 index 0000000000..9da4622b24 --- /dev/null +++ b/exercises/scale-generator/babel.config.js @@ -0,0 +1,14 @@ +module.exports = { + presets: [ + [ + '@babel/env', + { + targets: { + node: 'current', + }, + useBuiltIns: false, + }, + + ], + ], +}; diff --git a/exercises/scale-generator/example.js b/exercises/scale-generator/example.js new file mode 100644 index 0000000000..202fd01c5a --- /dev/null +++ b/exercises/scale-generator/example.js @@ -0,0 +1,33 @@ +export class Scale { + constructor(tonic) { + this.INTERVAL_STEPS = ['m', 'M', 'A'] + this.SHARPS_SCALE = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'] + this.FLATS_SCALE = ['A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab'] + this.USE_FLATS = ['F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'd', 'g', 'c', 'f', 'bb', 'eb'] + + this.tonic = tonic.slice(0, 1).toUpperCase() + tonic.slice(1); + // note use of original tonic argument + this.chromaticScale = this.USE_FLATS.includes(tonic) ? this.FLATS_SCALE : this.SHARPS_SCALE + } + + chromatic() { + return this.reorderChromaticScale() + } + + interval(intervals) { + const scale = this.reorderChromaticScale() + const result = [] + let currentIndex = 0 + + for (const step of intervals) { + result.push(scale[currentIndex]) + currentIndex = currentIndex + (this.INTERVAL_STEPS.indexOf(step) + 1) + } + return result + } + + reorderChromaticScale() { + const tonicIndex = this.chromaticScale.indexOf(this.tonic) + return this.chromaticScale.slice(tonicIndex).concat(this.chromaticScale.slice(0, tonicIndex)) + } +} diff --git a/exercises/scale-generator/package.json b/exercises/scale-generator/package.json new file mode 100644 index 0000000000..893bf6fdd0 --- /dev/null +++ b/exercises/scale-generator/package.json @@ -0,0 +1,35 @@ +{ + "name": "exercism-javascript", + "description": "Exercism exercises in Javascript.", + "author": "Katrina Owen", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript" + }, + "devDependencies": { + "@babel/cli": "^7.5.5", + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "@types/jest": "^24.0.16", + "@types/node": "^12.6.8", + "babel-eslint": "^10.0.2", + "babel-jest": "^24.8.0", + "eslint": "^6.1.0", + "eslint-plugin-import": "^2.18.2", + "jest": "^24.8.0" + }, + "jest": { + "modulePathIgnorePatterns": [ + "package.json" + ] + }, + "scripts": { + "test": "jest --no-cache ./*", + "watch": "jest --no-cache --watch ./*", + "lint": "eslint .", + "lint-test": "eslint . && jest --no-cache ./* " + }, + "license": "MIT", + "dependencies": {} +} diff --git a/exercises/scale-generator/scale-generator.js b/exercises/scale-generator/scale-generator.js new file mode 100644 index 0000000000..c2306b3283 --- /dev/null +++ b/exercises/scale-generator/scale-generator.js @@ -0,0 +1,18 @@ +// +// This is only a SKELETON file for the 'Scale Generator' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +export class Scale { + constructor(tonic) { + throw new Error("Remove this statement and implement this function"); + } + + chromatic() { + throw new Error("Remove this statement and implement this function"); + } + + interval(intervals) { + throw new Error("Remove this statement and implement this function"); + } +} diff --git a/exercises/scale-generator/scale-generator.spec.js b/exercises/scale-generator/scale-generator.spec.js new file mode 100644 index 0000000000..9102907e0e --- /dev/null +++ b/exercises/scale-generator/scale-generator.spec.js @@ -0,0 +1,92 @@ +import { Scale } from './scale-generator' + +describe('ScaleGenerator', () => { + describe('Chromatic scales', () => { + test('Chromatic scale with sharps', () => { + const expected = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] + expect(new Scale('C').chromatic()).toEqual(expected) + }) + + xtest('Chromatic scale with flats', () => { + const expected = ['F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E'] + expect(new Scale('F').chromatic()).toEqual(expected) + }) + }) + + describe('Scales with specified intervals', () => { + xtest('Simple major scale', () => { + const expected = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] + expect(new Scale('C').interval('MMmMMMm')).toEqual(expected) + }) + + xtest('Major scale with sharps', () => { + const expected = ['G', 'A', 'B', 'C', 'D', 'E', 'F#'] + expect(new Scale('G').interval('MMmMMMm')).toEqual(expected) + }) + + xtest('Major scale with flats', () => { + const expected = ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'] + expect(new Scale('F').interval('MMmMMMm')).toEqual(expected) + }) + + xtest('Minor scale with sharps', () => { + const expected = ['F#', 'G#', 'A', 'B', 'C#', 'D', 'E'] + expect(new Scale('f#').interval('MmMMmMM')).toEqual(expected) + }) + + xtest('Minor scale with flats', () => { + const expected = ['Bb', 'C', 'Db', 'Eb', 'F', 'Gb', 'Ab'] + expect(new Scale('bb').interval('MmMMmMM')).toEqual(expected) + }) + + xtest('Dorian mode', () => { + const expected = ['D', 'E', 'F', 'G', 'A', 'B', 'C'] + expect(new Scale('d').interval('MmMMMmM')).toEqual(expected) + }) + + xtest('Mixolydian mode', () => { + const expected = ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'Db'] + expect(new Scale('Eb').interval('MMmMMmM')).toEqual(expected) + }) + + xtest('Lydian mode', () => { + const expected = ['A', 'B', 'C#', 'D#', 'E', 'F#', 'G#'] + expect(new Scale('a').interval('MMMmMMm')).toEqual(expected) + }) + + xtest('Phrygian mode', () => { + const expected = ['E', 'F', 'G', 'A', 'B', 'C', 'D'] + expect(new Scale('e').interval('mMMMmMM')).toEqual(expected) + }) + + xtest('Locrian mode', () => { + const expected = ['G', 'Ab', 'Bb', 'C', 'Db', 'Eb', 'F'] + expect(new Scale('g').interval('mMMmMMM')).toEqual(expected) + }) + + xtest('Harmonic minor', () => { + const expected = ['D', 'E', 'F', 'G', 'A', 'Bb', 'Db'] + expect(new Scale('d').interval('MmMMmAm')).toEqual(expected) + }) + + xtest('Octatonic', () => { + const expected = ['C', 'D', 'D#', 'F', 'F#', 'G#', 'A', 'B'] + expect(new Scale('C').interval('MmMmMmMm')).toEqual(expected) + }) + + xtest('Hexatonic', () => { + const expected = ['Db', 'Eb', 'F', 'G', 'A', 'B'] + expect(new Scale('Db').interval('MMMMMM')).toEqual(expected) + }) + + xtest('Pentatonic', () => { + const expected = ['A', 'B', 'C#', 'E', 'F#'] + expect(new Scale('A').interval('MMAMA')).toEqual(expected) + }) + + xtest('Enigmatic', () => { + const expected = ['G', 'G#', 'B', 'C#', 'D#', 'F', 'F#'] + expect(new Scale('G').interval('mAMMMmm')).toEqual(expected) + }) + }) +})