diff --git a/config.json b/config.json index 3f9e2095..ba176a94 100644 --- a/config.json +++ b/config.json @@ -848,6 +848,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "scale-generator", + "name": "Scale Generator", + "uuid": "c2eaec94-826e-44e2-b92d-827a713dd373", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "change", "name": "Change", diff --git a/exercises/practice/scale-generator/.docs/instructions.md b/exercises/practice/scale-generator/.docs/instructions.md new file mode 100644 index 00000000..ebb7debc --- /dev/null +++ b/exercises/practice/scale-generator/.docs/instructions.md @@ -0,0 +1,68 @@ +# Instructions + +## Chromatic Scales + +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 ♭) so the chromatic scale can also be written like this: + +> A, B♭, B, C, D♭, D, E♭, E, F, G♭, G, A♭ + +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 (starting note). +Here is a table indicating whether the flat expression or sharp expression of the scale would be used for a given tonic: + +| Key Signature | Major | Minor | +| ------------- | --------------------- | -------------------- | +| Natural | C | a | +| Sharp | G, D, A, E, B, F♯ | e, b, f♯, c♯, g♯, d♯ | +| Flat | F, B♭, E♭, A♭, D♭, G♭ | d, g, c, f, b♭, e♭ | + +Note that by common music theory convention the natural notes "C" and "a" follow the sharps scale when ascending and the flats scale when descending. +For the scope of this exercise the scale is only ascending. + +### Task + +Given a tonic, generate the 12 note chromatic scale starting with the tonic. + +- Shift the base scale appropriately so that all 12 notes are returned starting with the given tonic. +- For the given tonic, determine if the scale is to be returned with flats or sharps. +- Return all notes in uppercase letters (except for the `b` for flats) irrespective of the casing of the given tonic. + +## Diatonic Scales + +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 second" interval, written "A", has two interceding notes (e.g., from A to C or D♭ to E) or a "whole step" plus a "half step". +There are also smaller and larger intervals, but they will not figure into this exercise. + +### Task + +Given a tonic and a set of intervals, generate the musical scale starting with the tonic and following the specified interval pattern. + +This is similar to generating chromatic scales except that instead of returning 12 notes, you will return N+1 notes for N intervals. +The first note is always the given tonic. +Then, for each interval in the pattern, the next note is determined by starting from the previous note and skipping the number of notes indicated by the interval. + +For example, starting with G and using the seven intervals MMmMMMm, there would be the following eight notes: + +| Note | Reason | +| ---- | ------------------------------------------------- | +| G | Tonic | +| A | M indicates a whole step from G, skipping G♯ | +| B | M indicates a whole step from A, skipping A♯ | +| C | m indicates a half step from B, skipping nothing | +| D | M indicates a whole step from C, skipping C♯ | +| E | M indicates a whole step from D, skipping D♯ | +| F♯ | M indicates a whole step from E, skipping F | +| G | m indicates a half step from F♯, skipping nothing | diff --git a/exercises/practice/scale-generator/.meta/ScaleGenerator.example.ps1 b/exercises/practice/scale-generator/.meta/ScaleGenerator.example.ps1 new file mode 100644 index 00000000..a383e09c --- /dev/null +++ b/exercises/practice/scale-generator/.meta/ScaleGenerator.example.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS + Implement a program to generate different scales of music based on a tonic. + +.DESCRIPTION + Please read the instructions for the rules and further details of music related terms. + + This exercise require you to create a class Scale, which take in a starting tonic. + The class required two methods to be implemented: + - Chromatic : which will return a 12-note scale depend on whether the starting tonic is flat or sharp. + - Interval(string) : method take in a string represent the intervals and return a diatonic scales. + +.EXAMPLE + $scale = [Scale]::new("C") + $scale.Chromatic() + Returns: @("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B") + + $scale.Interval("MMmMMMm") + Returns: @("C", "D", "E", "F", "G", "A", "B", "C") +#> + +Class Scale { + [string] hidden $tonic + [string[]] hidden $pitches + [int] hidden $NOTES + + Scale([string] $tonic) { + $this.tonic = $tonic + $this.pitches = $this.GetPitches($tonic) + $this.NOTES = 12 + } + + [string[]] Chromatic() { + $tonicTitle = (Get-Culture).TextInfo.ToTitleCase($this.tonic) + $tonicIndex = $this.pitches.IndexOf($tonicTitle) + if ($tonicIndex) { + return $this.pitches[$tonicIndex..$this.pitches.Length] + $this.pitches[0..($tonicIndex - 1)] + } + return $this.pitches + } + + [string[]] Interval([string] $intervals) { + $chromatic = $this.Chromatic() + $invervalsValues = $this.GetInterval($intervals) + + $result = @() + for ($i = 0; $i -lt $invervalsValues.Count; $i++) { + $noteIndex = ($i + $invervalsValues[$i]) % $this.NOTES + $result += $chromatic[$noteIndex] + } + return $result + } + + [int] hidden IsSharp([string] $tonic) { + <# + Determine if a tonic starting a flar or sharp scale + #> + return @('G', 'D', 'A', 'E', 'B', 'F#', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'C', 'a') -ccontains $tonic + } + + [string[]] hidden GetPitches([string] $tonic) { + <# + Returns the flat or sharp pitches based on the tonic + #> + if ($this.IsSharp($tonic)) { + return @('A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#') + } + return @('A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab') + } + + [int[]] hidden GetInterval([string] $intervals) { + <# + Return the numerical values of the intervals for the Diatonic Scales + #> + $count = 0 + $values = switch -CaseSensitive ($intervals.ToCharArray()) { + 'M' {$count++; $count} + 'A' {$count+=2; $count} + Default {$count} + } + return @(0) + $values + } +} \ No newline at end of file diff --git a/exercises/practice/scale-generator/.meta/config.json b/exercises/practice/scale-generator/.meta/config.json new file mode 100644 index 00000000..365fa48b --- /dev/null +++ b/exercises/practice/scale-generator/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "glaxxie" + ], + "files": { + "solution": [ + "ScaleGenerator.ps1" + ], + "test": [ + "ScaleGenerator.tests.ps1" + ], + "example": [ + ".meta/ScaleGenerator.example.ps1" + ] + }, + "blurb": "Generate musical scales, given a starting note and a set of intervals." +} diff --git a/exercises/practice/scale-generator/.meta/tests.toml b/exercises/practice/scale-generator/.meta/tests.toml new file mode 100644 index 00000000..1cab430d --- /dev/null +++ b/exercises/practice/scale-generator/.meta/tests.toml @@ -0,0 +1,136 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[10ea7b14-8a49-40be-ac55-7c62b55f9b47] +description = "Chromatic scales -> Chromatic scale with sharps" + +[af8381de-9a72-4efd-823a-48374dbfe76f] +description = "Chromatic scales -> Chromatic scale with flats" + +[7195998a-7be7-40c9-8877-a1d7949e061b] +description = "Scales with specified intervals -> Simple major scale" +reimplements = "6f5b1410-1dd7-4c6c-b410-6b7e986f6f1e" + +[fe853b97-1878-4090-b218-4029246abb91] +description = "Scales with specified intervals -> Major scale with sharps" +reimplements = "13a92f89-a83e-40b5-b9d4-01136931ba02" + +[d60cb414-cc02-4fcb-ad7a-fc7ef0a9eead] +description = "Scales with specified intervals -> Major scale with flats" +reimplements = "aa3320f6-a761-49a1-bcf6-978e0c81080a" + +[77dab9b3-1bbc-4f9a-afd8-06da693bcc67] +description = "Scales with specified intervals -> Minor scale with sharps" +reimplements = "63daeb2f-c3f9-4c45-92be-5bf97f61ff94" + +[5fa1728f-5b66-4b43-9b7c-84359b7069d4] +description = "Scales with specified intervals -> Minor scale with flats" +reimplements = "616594d0-9c48-4301-949e-af1d4fad16fd" + +[f3f1c353-8f7b-4a85-a5b5-ae06c2645823] +description = "Scales with specified intervals -> Dorian mode" +reimplements = "390bd12c-5ac7-4ec7-bdde-4e58d5c78b0a" + +[5fe14e5a-3ddc-4202-a158-2c1158beb5d0] +description = "Scales with specified intervals -> Mixolydian mode" +reimplements = "846d0862-0f3e-4f3b-8a2d-9cc74f017848" + +[e6307799-b7f6-43fc-a6d8-a4834d6e2bdb] +description = "Scales with specified intervals -> Lydian mode" +reimplements = "7d49a8bb-b5f7-46ad-a207-83bd5032291a" + +[7c4a95cd-ecf4-448d-99bc-dbbca51856e0] +description = "Scales with specified intervals -> Phrygian mode" +reimplements = "a4e4dac5-1891-4160-a19f-bb06d653d4d0" + +[f476f9c9-5a13-473d-bb6c-f884cf8fd9f2] +description = "Scales with specified intervals -> Locrian mode" +reimplements = "ef3650af-90f8-4ad9-9ef6-fdbeae07dcaa" + +[87fdbcca-d3dd-46d5-9c56-ec79e25b19f4] +description = "Scales with specified intervals -> Harmonic minor" +reimplements = "70517400-12b7-4530-b861-fa940ae69ee8" + +[b28ecc18-88db-4fd5-a973-cfe6361e2b24] +description = "Scales with specified intervals -> Octatonic" +reimplements = "37114c0b-c54d-45da-9f4b-3848201470b0" + +[a1c7d333-6fb3-4f3b-9178-8a0cbe043134] +description = "Scales with specified intervals -> Hexatonic" +reimplements = "496466e7-aa45-4bbd-a64d-f41030feed9c" + +[9acfd139-0781-4926-8273-66a478c3b287] +description = "Scales with specified intervals -> Pentatonic" +reimplements = "bee5d9ec-e226-47b6-b62b-847a9241f3cc" + +[31c933ca-2251-4a5b-92dd-9d5831bc84ad] +description = "Scales with specified intervals -> Enigmatic" +reimplements = "dbee06a6-7535-4ab7-98e8-d8a36c8402d1" + +[6f5b1410-1dd7-4c6c-b410-6b7e986f6f1e] +description = "Scales with specified intervals -> Simple major scale" +include = false + +[13a92f89-a83e-40b5-b9d4-01136931ba02] +description = "Scales with specified intervals -> Major scale with sharps" +include = false + +[aa3320f6-a761-49a1-bcf6-978e0c81080a] +description = "Scales with specified intervals -> Major scale with flats" +include = false + +[63daeb2f-c3f9-4c45-92be-5bf97f61ff94] +description = "Scales with specified intervals -> Minor scale with sharps" +include = false + +[616594d0-9c48-4301-949e-af1d4fad16fd] +description = "Scales with specified intervals -> Minor scale with flats" +include = false + +[390bd12c-5ac7-4ec7-bdde-4e58d5c78b0a] +description = "Scales with specified intervals -> Dorian mode" +include = false + +[846d0862-0f3e-4f3b-8a2d-9cc74f017848] +description = "Scales with specified intervals -> Mixolydian mode" +include = false + +[7d49a8bb-b5f7-46ad-a207-83bd5032291a] +description = "Scales with specified intervals -> Lydian mode" +include = false + +[a4e4dac5-1891-4160-a19f-bb06d653d4d0] +description = "Scales with specified intervals -> Phrygian mode" +include = false + +[ef3650af-90f8-4ad9-9ef6-fdbeae07dcaa] +description = "Scales with specified intervals -> Locrian mode" +include = false + +[70517400-12b7-4530-b861-fa940ae69ee8] +description = "Scales with specified intervals -> Harmonic minor" +include = false + +[37114c0b-c54d-45da-9f4b-3848201470b0] +description = "Scales with specified intervals -> Octatonic" +include = false + +[496466e7-aa45-4bbd-a64d-f41030feed9c] +description = "Scales with specified intervals -> Hexatonic" +include = false + +[bee5d9ec-e226-47b6-b62b-847a9241f3cc] +description = "Scales with specified intervals -> Pentatonic" +include = false + +[dbee06a6-7535-4ab7-98e8-d8a36c8402d1] +description = "Scales with specified intervals -> Enigmatic" +include = false diff --git a/exercises/practice/scale-generator/ScaleGenerator.ps1 b/exercises/practice/scale-generator/ScaleGenerator.ps1 new file mode 100644 index 00000000..a731190c --- /dev/null +++ b/exercises/practice/scale-generator/ScaleGenerator.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Implement a program to generate different scales of music based on a tonic. + +.DESCRIPTION + Please read the instructions for the rules and further details of music related terms. + + This exercise require you to create a class Scale, which take in a starting tonic. + The class required two methods to be implemented: + - Chromatic : which will return a 12-note scale depend on whether the starting tonic is flat or sharp. + - Interval(string) : method take in a string represent the intervals and return a diatonic scales. + +.EXAMPLE + $scale = [Scale]::new("C") + $scale.Chromatic() + Returns: @("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B") + + $scale.Interval("MMmMMMm") + Returns: @("C", "D", "E", "F", "G", "A", "B", "C") +#> + +Class Scale { + Scale([string] $tonic) { + Throw "Please implement this class" + } + + [string[]] Chromatic() { + Throw "Please implement this function" + } + + [string[]] Interval([string] $intervals) { + Throw "Please implement this function" + } +} \ No newline at end of file diff --git a/exercises/practice/scale-generator/ScaleGenerator.tests.ps1 b/exercises/practice/scale-generator/ScaleGenerator.tests.ps1 new file mode 100644 index 00000000..0562dab2 --- /dev/null +++ b/exercises/practice/scale-generator/ScaleGenerator.tests.ps1 @@ -0,0 +1,128 @@ +BeforeAll { + . "./ScaleGenerator.ps1" +} + +Describe "ScaleGenerator test cases" { + Context "Chromatic scales" { + It "Chromatic scale with sharps" { + $got = [Scale]::new("C").Chromatic() + $want = @("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B") + + $got | Should -BeExactly $want + } + + It "Chromatic scale with flats" { + $got = [Scale]::new("F").Chromatic() + $want = @("F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E") + + $got | Should -BeExactly $want + } + } + + Context "Scales with specified intervals" { + It "Simple major scale" { + $got = [Scale]::new("C").Interval("MMmMMMm") + $want = @("C", "D", "E", "F", "G", "A", "B", "C") + + $got | Should -BeExactly $want + } + + It "Major scale with sharps" { + $got = [Scale]::new("G").Interval("MMmMMMm") + $want = @("G", "A", "B", "C", "D", "E", "F#", "G") + + $got | Should -BeExactly $want + } + + It "Major scale with flats" { + $got = [Scale]::new("F").Interval("MMmMMMm") + $want = @("F", "G", "A", "Bb", "C", "D", "E", "F") + + $got | Should -BeExactly $want + } + + It "Minor scale with sharps" { + $got = [Scale]::new("f#").Interval("MmMMmMM") + $want = @("F#", "G#", "A", "B", "C#", "D", "E", "F#") + + $got | Should -BeExactly $want + } + + It "Minor scale with flats" { + $got = [Scale]::new("bb").Interval("MmMMmMM") + $want = @("Bb", "C", "Db", "Eb", "F", "Gb", "Ab", "Bb") + + $got | Should -BeExactly $want + } + + It "Dorian mode" { + $got = [Scale]::new("d").Interval("MmMMMmM") + $want = @("D", "E", "F", "G", "A", "B", "C", "D") + + $got | Should -BeExactly $want + } + + It "Mixolydian mode" { + $got = [Scale]::new("Eb").Interval("MMmMMmM") + $want = @("Eb", "F", "G", "Ab", "Bb", "C", "Db", "Eb") + + $got | Should -BeExactly $want + } + + It "Lydian mode" { + $got = [Scale]::new("a").Interval("MMMmMMm") + $want = @("A", "B", "C#", "D#", "E", "F#", "G#", "A") + + $got | Should -BeExactly $want + } + + It "Phrygian mode" { + $got = [Scale]::new("e").Interval("mMMMmMM") + $want = @("E", "F", "G", "A", "B", "C", "D", "E") + + $got | Should -BeExactly $want + } + + It "Locrian mode" { + $got = [Scale]::new("g").Interval("mMMmMMM") + $want = @("G", "Ab", "Bb", "C", "Db", "Eb", "F", "G") + + $got | Should -BeExactly $want + } + + It "Harmonic minor" { + $got = [Scale]::new("d").Interval("MmMMmAm") + $want = @("D", "E", "F", "G", "A", "Bb", "Db", "D") + + $got | Should -BeExactly $want + } + + It "Octatonic" { + $got = [Scale]::new("C").Interval("MmMmMmMm") + $want = @("C", "D", "D#", "F", "F#", "G#", "A", "B", "C") + + $got | Should -BeExactly $want + } + + It "Hexatonic" { + $got = [Scale]::new("Db").Interval("MMMMMM") + $want = @("Db", "Eb", "F", "G", "A", "B", "Db") + + $got | Should -BeExactly $want + } + + It "Pentatonic" { + $got = [Scale]::new("A").Interval("MMAMA") + $want = @("A", "B", "C#", "E", "F#", "A") + + $got | Should -BeExactly $want + } + + It "Enigmatic" { + $got = [Scale]::new("G").Interval("mAMMMmm") + $want = @("G", "G#", "B", "C#", "D#", "F", "F#", "G") + + $got | Should -BeExactly $want + } + } +}