Is this a Major Scale (or equivalent)?

16

0

Sandbox

The major scale (or Ionian scale) is one of the most commonly used musical scales, especially in Western music. It is one of the diatonic scales. Like many musical scales, it is made up of seven notes: the eighth duplicates the first at double its frequency so that it is called a higher octave of the same note.

The seven musical notes are:

C, D, E, F, G, A, B, C (repeated for example purposes)

A major scale is a diatonic scale. Take the previous succession of notes as a major scale (Actually, It is the scale C Major). The sequence of intervals between the notes of a major scale is:

whole, whole, half, whole, whole, whole, half

where "whole" stands for a whole tone (a red u-shaped curve in the figure), and "half" stands for a semitone (a red broken line in the figure).

enter image description here

In this case, from C to D exist a whole tone, from D to E exist a whole tone, from E to F exist half tone, etc...

We have 2 components that affects the tone distance between notes. These are the Sharp symbol (♯) and the flat symbol (♭).

The Sharp symbol (♯) adds half tone to the note. Example. From C to D we mentioned that exists a whole tone, if we use C♯ instead C then from C♯ to D exists half tone.

The Flat symbol (♭) do the opposite of the Sharp symbol, it subtract half tone from the note. Example: From D to E we mentioned that exists a whole tone, if we use Db instead D then from Db to E exists a tone and a half.

By default, from Note to Note exist a whole tone except for E to F and B to C in where just half tone exists.

Note in some cases using enharmonic pitches can create an equivalent to a Major Scale. An example of this is C#, D#, E#, F#, G#, A#, B#, C# where E# and B# are enharmonic but the scale follows the sequence of a Major Scale.


Challenge

Given a scale, output a truthy value if it is a Major Scale or equivalent, otherwise output a falsey value.

Rules

  • Standard I/O method allowed
  • Standard rules apply
  • You don't need to take in consideration the 8th note. Assume the input will only consist of 7 notes
  • Assume double flat (♭♭), double sharp (♯♯) or natural sign (♮) don't exist

Test cases

C, D, E, F, G, A, B                 => true
C#, D#, E#, F#, G#, A#, B#          => true
Db, Eb, F, Gb, Ab, Bb, C            => true
D, E, Gb, G, A, Cb, C#              => true
Eb, E#, G, G#, Bb, B#, D            => true
-----------------------------------------------
C, D#, E, F, G, A, B                => false
Db, Eb, F, Gb, Ab, B, C             => false
G#, E, F, A, B, D#, C               => false 
C#, C#, E#, F#, G#, A#, B#          => false
Eb, E#, Gb, G#, Bb, B#, D           => false

Luis felipe De jesus Munoz

Posted 2018-11-08T13:16:44.913

Reputation: 9 639

@Abigail Basically yes. They have the same tone although they are different notes. – Luis felipe De jesus Munoz – 2018-11-08T14:15:03.597

and conversely, Fb = E and B# = C. – Skidsdev – 2018-11-08T14:24:23.583

1and Cx (or C##) = D – SaggingRufus – 2018-11-08T18:32:17.003

@SaggingRufus but in this challenge we are not using double sharp – Luis felipe De jesus Munoz – 2018-11-08T18:46:46.177

I'm going to have to downvote this because I don't accept scales that don't have exactly one of each letter in order. No B#, D, Fb, E#, G, A Cb thank you very much... – Neil – 2018-11-08T18:53:09.743

@Neil A A#/Bb B B#/Cb C C#/Db D D#/Eb E E#/Fb F F#/Gb G G#/Ab those are the only possible notes in that order repeating. – SaggingRufus – 2018-11-08T19:00:19.383

@Neil Ok, you are free to do whatever you want – Luis felipe De jesus Munoz – 2018-11-08T19:09:57.300

1Btw, Pentatonic scales do not have one of each letter :v – Luis felipe De jesus Munoz – 2018-11-08T19:48:26.407

Fair enough, but they do have unique letters in order at least. – Neil – 2018-11-08T21:00:54.710

1

@Neil Chromatic scales do not have unique letters and I'm sure there is a type of scale that doesnt follow an ascending order

– Luis felipe De jesus Munoz – 2018-11-08T21:38:36.973

Sure, but those are hardly major scales any more. – Neil – 2018-11-08T23:52:57.970

1I'm going to have to upvote this because @Neil downvoted it thank you very much – David Conrad – 2018-11-09T18:03:11.330

Answers

11

Perl 6, 76 65 63 59 bytes

-4 bytes thanks to Phil H

{221222==[~] (.skip Z-$_)X%12}o*>>.&{13*.ord+>3+?/\#/-?/b/}

Try it online!

Explanation

*>>.&{ ... }  # Map notes to integers
  13*.ord     # 13 * ASCII code:  A=845 B=858 C=871 D=884 E=897 F=910 G=923
  +>3         # Right shift by 3: A=105 B=107 C=108 D=110 E=112 F=113 G=115
              # Subtracting 105 would yield A=0 B=2 C=3 D=5 E=7 F=8 G=10
              # but isn't necessary because we only need differences
  +?/\#/      # Add 1 for '#'
  -?/b/       # Subtract 1 for 'b'

{                           }o  # Compose with block
            (.skip Z-$_)        # Pairwise difference
                        X%12    # modulo 12
         [~]  # Join
 221222==     # Equals 221222

nwellnhof

Posted 2018-11-08T13:16:44.913

Reputation: 10 037

@PhilH Yes, of course. Thanks! – nwellnhof – 2018-11-08T16:53:54.583

That's a really clever way of mapping the notes to their relative values, +1 from me! – Sok – 2018-11-09T15:44:25.823

10

Node.js v10.9.0, 78 76 71 69 bytes

a=>!a.some(n=>(a-(a=~([x,y]=Buffer(n),x/.6)-~y%61)+48)%12-2+!i--,i=3)

Try it online!

How?

Each note \$n\$ is converted to a negative number in \$[-118,-71]\$ with:

[x, y] = Buffer(n) // split n into two ASCII codes x and y
~(x / .6)          // base value, using the ASCII code of the 1st character
- ~y % 61          // +36 if the 2nd character is a '#' (ASCII code 35)
                   // +38 if the 2nd character is a 'b' (ASCII code 98)
                   // +1  if the 2nd character is undefined

Which gives:

  n   | x  | x / 0.6 | ~(x / 0.6) | -~y % 61 | sum
------+----+---------+------------+----------+------
 "Ab" | 65 | 108.333 |    -109    |    38    |  -71
 "A"  | 65 | 108.333 |    -109    |     1    | -108
 "A#" | 65 | 108.333 |    -109    |    36    |  -73
 "Bb" | 66 | 110.000 |    -111    |    38    |  -73
 "B"  | 66 | 110.000 |    -111    |     1    | -110
 "B#" | 66 | 110.000 |    -111    |    36    |  -75
 "Cb" | 67 | 111.667 |    -112    |    38    |  -74
 "C"  | 67 | 111.667 |    -112    |     1    | -111
 "C#" | 67 | 111.667 |    -112    |    36    |  -76
 "Db" | 68 | 113.333 |    -114    |    38    |  -76
 "D"  | 68 | 113.333 |    -114    |     1    | -113
 "D#" | 68 | 113.333 |    -114    |    36    |  -78
 "Eb" | 69 | 115.000 |    -116    |    38    |  -78
 "E"  | 69 | 115.000 |    -116    |     1    | -115
 "E#" | 69 | 115.000 |    -116    |    36    |  -80
 "Fb" | 70 | 116.667 |    -117    |    38    |  -79
 "F"  | 70 | 116.667 |    -117    |     1    | -116
 "F#" | 70 | 116.667 |    -117    |    36    |  -81
 "Gb" | 71 | 118.333 |    -119    |    38    |  -81
 "G"  | 71 | 118.333 |    -119    |     1    | -118
 "G#" | 71 | 118.333 |    -119    |    36    |  -83

We compute the pairwise differences modulo \$12\$ between these values.

The lowest possible difference between 2 notes is \$-47\$, so it's enough to add \$4\times12=48\$ before applying the modulo to make sure that we get a positive result.

Because we apply a modulo \$12\$, the offset produced by a '#' is actually \$36 \bmod 12 = 0\$ semitone, while the offset produced by a 'b' is \$38 \bmod 12 = 2\$ semitones.

We are reusing the input variable \$a\$ to store the previous value, so the first iteration just generates \$\text{NaN}\$.

For a major scale, we should get \$[ \text{NaN}, 2, 2, 1, 2, 2, 2 ]\$.

We use the counter \$i\$ to compare the 4th value with \$1\$ rather than \$2\$.

Arnauld

Posted 2018-11-08T13:16:44.913

Reputation: 111 334

Great approach, much more interesting than my answer – Skidsdev – 2018-11-19T15:49:04.347

4

JavaScript (Node.js), 150 131 125 bytes

l=>(l=l.map(x=>'C0D0EF0G0A0B'.search(x[0])+(x[1]=='#'|-(x[1]=='b')))).slice(1).map((n,i)=>(b=n-l[i])<0?2:b)+""=='2,2,1,2,2,2'

Try it online!

-19 bytes thanks to Luis felipe
-6 bytes thanks to Shaggy

Ungolfed:

function isMajor(l) {
    // Get tone index of each entry
    let array = l.map(function (x) {
        // Use this to get indices of each note, using 0s as spacers for sharp keys
        let tones = 'C0D0EF0G0A0B';
        // Get the index of the letter component. EG D = 2, F = 5
        let tone = tones.search(x[0]);
        // Add 1 semitone if note is sharp
        // Use bool to number coercion to make this shorter
        tone += x[1] == '#' | -(x[1]=='b');
    });
    // Calculate deltas
    let deltas = array.slice(1).map(function (n,i) {
        // If delta is negative, replace it with 2
        // This accounts for octaves
        if (n - array[i] < 0) return 2;
        // Otherwise return the delta
        return n - array[i];
    });
    // Pseudo array-comparison
    return deltas+"" == '2,2,1,2,2,2';
}

Skidsdev

Posted 2018-11-08T13:16:44.913

Reputation: 9 656

1[...'C0D0EF0G0A0B'] instead of 'C0D0EF0G0A0B'.split('') and +"" instead of .toString() to save some bytes – Luis felipe De jesus Munoz – 2018-11-08T14:36:34.463

x[1]=='#'|-(x[1]=='b') instead of x[1]=='#'?1:(x[1]=='b'?-1:0) save some bytes too – Luis felipe De jesus Munoz – 2018-11-08T14:39:00.223

@LuisfelipeDejesusMunoz Oh nice thanks! I can't believe I forgot about array expansion and adding an empty string – Skidsdev – 2018-11-08T14:43:21.587

"If delta is negative, replace it with 2" sounds wrong. I think you need to take the difference modulo 12. – nwellnhof – 2018-11-08T15:58:22.573

@nwellnhof In my tests, all major scales either had the correct deltas to begin with, or, if they spanned an octave, had one delta at -10 rather than 2. Replacing negative deltas fixes that. I don't think -10 % 12 == 2. Although come to think of it this might fail in some cases... – Skidsdev – 2018-11-08T16:33:26.877

You can use (x+12)%12 because the difference is never less than -11. – nwellnhof – 2018-11-08T16:44:35.037

[...'C0D0EF0G0A0B'].indexOf(x[0]) to 'C0D0EF0G0A0B'.search(x[0]) saves a few more bytes. – Shaggy – 2018-11-08T16:45:54.193

3

Dart, 198 197 196 189 bytes

f(l){var i=0,j='',k,n=l.map((m){k=m.runes.first*2-130;k-=k>3?k>9?2:1:0;return m.length<2?k:m[1]=='#'?k+1:m[1]=='b'?k-1:k;}).toList();for(;++i<7;j+='${(n[i]-n[i-1])%12}');return'221222'==j;}

Try it online!

Loose port of the old Perl 6 answer https://codegolf.stackexchange.com/a/175522/64722

f(l){
  var i=0,j='',k,
  n=l.map((m){
    k=m.runes.first*2-130;
    k-=k>3?k>9?2:1:0;
    return m.length<2?k:m[1]=='#'?k+1:m[1]=='b'?k-1:k;
  }).toList();
  for(;++i<7;j+='${(n[i]-n[i-1])%12}');
  return'221222'==j;
}
  • -1 byte by using ternary operators for #/b
  • -1 byte by using ifs instead of ternaries for the scale shifts
  • -7 bytes thanks to @Kevin Cruijssen

Old version :

Dart, 210 bytes

f(l){var i=0,k=0,n={'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11,'b':-1,'#':1},j='',y=[0,0];for(;++i<7;j+='${(y[0]-y[1])%12}')for(k=0;k<2;k++)y[k]=n[l[i-k][0]]+(l[i-k].length>1?n[l[i-k][1]]:0);return'221222'==j;}

Try it online!

Ungolfed:

f(l){
  var i=0,k=0,n={'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11,'b':-1,'#':1},j='',y=[0,0];
  for(;++i<7;j+='${(y[0]-y[1])%12}')
    for(k=0;k<2;k++)
      y[k]=n[l[i-k][0]]+(l[i-k].length>1?n[l[i-k][1]]:0);

  return'221222'==j;
}

A whole step is 2, a quarter is 1. Mod 12 in case you jump to a higher octave. Iterates through all notes and computes the difference between the ith note and the i-1th note. Concatenates the result and should expect 221222 (2 whole, 1 half, 3 wholes).

  • -2 bytes by not assigning 0 to k
  • -4 bytes by using j as a String and not a List
  • -6 bytes thanks to @Kevin Cruijssen by removing unnecessary clutter in loops

Elcan

Posted 2018-11-08T13:16:44.913

Reputation: 913

I don't know Dart, but parts are similar as Java. Therefore: changing i=1 to i=0 can reduce a byte by changing for(;i<7;i++) to for(;++i<7;). In addition, the brackets {} can be removed around that loop, by putting the j+=... inside the third part of the loop: for(;++i<7;j+='${(y[0]-y[1])%12}'). And one last thing is changing return j=='221222'; to return'221222'==j; to get rid of the space. -6 (210 bytes) after these modifications.

– Kevin Cruijssen – 2018-11-08T15:02:13.253

Thanks, didn't know about those tricks for the loops – Elcan – 2018-11-08T15:28:41.857

Np. In your new 196-bytes version you can also golf it to 189 bytes by changing if(k>9)k--;if(k>3)k--; to k-=k>3?k>9?2:1:0; and k+=m.length<2?0:m[1]=='#'?1:m[1]=='b'?-1:0;return k; to return m.length<2?k:m[1]=='#'?k+1:m[1]=='b'?k-1:k;. :)

– Kevin Cruijssen – 2018-11-08T17:49:19.477

Damn, I still have a lot to learn it seems, thanks ! – Elcan – 2018-11-08T19:00:41.290

Well, I've been golfing for 2.5 years now, and even I get tips on things to golf all the time. :) It's pretty easy to miss something yourself initially, and with time you think about different ways to golf. :) Tips for golfing in <all languages> might be interesting to read through if you haven't yet. And some of the Tips for golfing in Java might also be applicable in Dart, since the golfs I did in your answers were based on my Java knowledge, since this is the first time I see Dart. ;)

– Kevin Cruijssen – 2018-11-08T20:38:25.987

Dart is pretty close to Java but with lots of stuff taken from JS to make the code less verbose. The only big downside is that type casting is inexistant, you can't use an int as a bool for example. Some functions are also extremely verbose, removeLast instead of pop – Elcan – 2018-11-08T22:31:27.457

Verbose methods and not able to convert int to boolean sounds like Java ;) One advantage Java has compared to JS/Python/etc. is that characters can be used as integer unicode values, which is usually the only kind of challenges where I have shorter answers in Java than in Python or JS ;) Everything else Java is usually at the bottom in terms of byte-count, unless BrainFwck or something similar has an answer as well. xD – Kevin Cruijssen – 2018-11-09T07:25:49.270

Jeez, I know C does that too. Sadly Dart has this 'runes' methods (or codeUnits for UTF-16 characters) to get the integer value. You can't get it from the character itself. Dart's very strong typing sure is problematic sometimes – Elcan – 2018-11-09T08:05:34.063

Ah well, regardless of the language, it's still fun to do these gold challenges as short as possible. Apart from Java I usually also answer in 05AB1E, an actual golfing language that does things in 5 bytes which my Java function does in 100 bytes xD And sometimes in Whitespace, Retina, or Charcoal if the challenge is suitable for those languages. Regardless of the language, I'm still having fun golfing them as much as possible. :) – Kevin Cruijssen – 2018-11-09T08:35:41.357

2

C (gcc), -DA=a[i] + 183 = 191 bytes

f(int*a){char s[9],b[9],h=0,i=0,j=0,d;for(;A;A==35?b[i-h++-1]++:A^98?(b[i-h]=A*13>>3):b[i-h++-1]--,i++);for(;j<7;d=(b[j]-b[j-1])%12,d=d<0?d+12:d,s[j++-1]=d+48);a=!strcmp(s,"221222");}

Try it online!

Based on the Perl answer.

Takes input as a wide string.

Ungolfed:

int f(int *a){
	char s[9], b[9];
	int h, i, j;
	h = 0;
        for(i = 0; a[i] != NULL; i++){
		if(a[i] == '#'){
			b[i-h-1] += 1;
			h++;
		}
		else if(a[i] == 'b'){
			b[i-1-h] -= 1;
			h++;
		}
		else{
			b[i-h] = (a[i] * 13) >> 3;
		}
	}
	for(j = 1; j < 7; j++){
		int d = (b[j] - b[j-1]) % 12;
		d = d < 0? d + 12: d;
		s[j-1] = d + '0';
	}
	return strcmp(s, "221222") == 0;
}

Logern

Posted 2018-11-08T13:16:44.913

Reputation: 845

170 bytes – ceilingcat – 2019-07-24T09:09:45.410

2

Python 3, 175 136 134 114 112 bytes

def f(t):r=[ord(x[0])//.6+ord(x[1:]or'"')%13-8for x in t];return[(y-x)%12for x,y in zip(r,r[1:])]==[2,2,1,2,2,2]

Try it online!


An one-liner Python 3 implementation.

Thanks to @Arnauld for idea of calculate tones using division and modulo.
Thanks to @Jo King for -39 bytes.

cobaltp

Posted 2018-11-08T13:16:44.913

Reputation: 401

1136 bytes – Jo King – 2018-11-09T07:27:45.557

2

[Wolfram Language (Mathematica) + Music` package], 114 bytes

I love music and found this interesting, but I was out playing real golf when this code golf opportunity came down the pike so my submission is a little tardy.

I figured I'd try this a totally different way, utilizing some actual music knowledge. It turns out the music package of Mathematica knows the fundamental frequency of the named notes. First I convert the input string into sequence of named notes. Next, I take the ratios of each successive note and double any that are less than 2 (to account for octave shift). Then I compare these ratios to the ratios of the Ionian scale which has roughly a 6% frequency difference between half notes and 12% between full notes.

More than half of the bytes spent here are to convert the input into named symbols.

.06{2,2,1,2,2,2}+1==Round[Ratios[Symbol[#~~"0"]&/@StringReplace[# ,{"b"->"flat","#"->"sharp"}]]/.x_/;x<1->2x,.01]&

Try it online!

Kelly Lowder

Posted 2018-11-08T13:16:44.913

Reputation: 3 225

1

[Python] 269 202 bytes

Improvements from Jo King:

p=lambda z:"A BC D EF G".index(z[0])+"b #".index(z[1:]or' ')-1
def d(i,j):f=abs(p(i)-p(j));return min(f,12-f)
q=input().replace(' ','').split(',')
print([d(q[i],q[i+1])for i in range(6)]==[2,2,1,2,2,2])

Try it!

Ungolfed, with test driver:

tone = "A BC D EF G"   # tones in "piano" layout
adj = "b #"            # accidentals

def note_pos(note):
    if len(note) == 1:
        note += ' '
    n,a = note
    return tone.index(n) + adj[a]

def note_diff(i, j):
    x, y = note_pos(i), note_pos(j)
    diff = abs(x-y)
    return min(diff, 12-diff)

def is_scale(str):
    seq = str.replace(' ','').split(',')
    div = [note_diff(seq[i], seq[i+1]) for i in (0,1,2,3,4,5)]
    return div == [2,2,1,2,2,2]

case = [
("C, D, E, F, G, A, B", True),
("C#, D#, E#, F#, G#, A#, B#", True),
("Db, Eb, F, Gb, Ab, Bb, C", True),
("D, E, Gb, G, A, Cb, C#", True),
("Eb, E#, G, G#, Bb, B#, D", True),

("C, D#, E, F, G, A, B", False),
("Db, Eb, F, Gb, Ab, B, C", False),
("G#, E, F, A, B, D#, C", False),
("C#, C#, E#, F#, G#, A#, B#", False),
("Eb, E#, Gb, G#, Bb, B#, D", False),
]

for test, result in case:
    print(test + ' '*(30-len(test)), result, '\t',
          "valid" if is_scale(test) == result else "ERROR")

Prune

Posted 2018-11-08T13:16:44.913

Reputation: 111

Yes, I see the white space -- still inculcated with too much PEP-8, I'm afraid. I apparently missed something; is an execution link required here? – Prune – 2018-11-09T02:03:56.830

1

Though, if you want the link, 202 bytes with some quick golfing. You could definitely golf some more by changing to a different input format

– Jo King – 2018-11-09T02:28:31.250

Ah ... I'm too used to Python returning the final expression as the process value. Thanks for the pointers and hints. – Prune – 2018-11-09T02:32:08.883

You can get 156 bytes if you switch to a function taking a list of strings. Also, TIO has an auto formatter in the link section that you can use

– Jo King – 2018-11-09T02:49:08.743

@JoKing, you're perfectly welcome to edit this answer or post your own; commenting with a link separates the improvements by one level. – Prune – 2018-11-09T16:40:44.130

1

Ruby, 109 bytes

->s{(0..11).any?{|t|s.map{|n|(w="Cef;DXg<E=Fhi>G j8A d9B:")[(w.index(""<<n.sum%107)/2-t)%12]}*''=='CfDX<=h'}}

Try it online!

G B

Posted 2018-11-08T13:16:44.913

Reputation: 11 099