23
\$\begingroup\$

Guidelines

Task

Given two notes, inputted as strings or lists/arrays, calculate how many semitones apart they are (inclusive of the notes themselves), outputting as a number.

Explanation of a semitone:

A semitone is one step up or down the keyboard. An example is C to C#. As you can see below the note C is on a white note and C# is the black note just one above it. Semitones are the leaps from a black note to the next white note, up or down, except for:

  • B to C
  • C to B
  • E to F
  • F to E

keyboard

Examples

'A, C' -> 4

'G, G#' -> 2

'F#, B' -> 6

'Bb, Bb' -> 13


Rules

  • The largest distance between two notes is 13 semitones.
  • The second inputted note will always be above the first inputted note.
  • You can take input as either a string, or an array/list. If you take it as a string, the notes will be comma-separated (e.g. String -> 'A, F', Array -> ['A', 'F']).
  • You can assume that you will always be given two valid notes.
  • Sharps will be denoted as # and flats will be denoted as b
  • Your code must support enharmonic equivalents (e.g. It must support both F# and Gb)
  • Your code does not need to support notes that are named with, but can be named without a sharp or flat (i.e. You do not need to support E#, or Cb). Bonus points if your code does support it though.
  • Your code does not need to support double sharps or double flats.
  • You can assume that if you get the both the same notes, or same pitch (e.g. 'Gb, Gb' or 'A#, Bb'), the second not will be exactly one octave above the first.
  • This is code golf so the answer with the least amount of bytes wins.
\$\endgroup\$
13
  • \$\begingroup\$ I get 2 for G -> G# because they're both included. \$\endgroup\$
    – hyper-neutrino
    Feb 20, 2018 at 7:05
  • \$\begingroup\$ @HyperNeutrino Yep sorry. Mistake on my behalf. \$\endgroup\$
    – aimorris
    Feb 20, 2018 at 7:06
  • 1
    \$\begingroup\$ Do we have to cater for notes like Cb or E#? What about double sharps/flats? \$\endgroup\$
    – Sok
    Feb 20, 2018 at 8:20
  • 1
    \$\begingroup\$ @Sok No, your code does not need to support notes such as E# or Cb, and it does not need to support double sharps or flats. I've updated the question to make it clearer. Sorry about any confusion. \$\endgroup\$
    – aimorris
    Feb 20, 2018 at 11:37
  • 4
    \$\begingroup\$ Just to be clear, when talking from a music theory sense distance in semitones does not include the note you start on. In math it wold be represented as (X, Y] so C to C# is 1 semitone and C to C is 12 semitones. \$\endgroup\$
    – Dom
    Feb 21, 2018 at 4:48

12 Answers 12

7
\$\begingroup\$

Python 2, 66 bytes

r=1
for s in input():r=cmp(s[1:]+s,s)-ord(s[0])*5/3-r
print-r%12+2

Try it online!


Python 2, 68 bytes

lambda s,t:13-(q(s)-q(t))%12
q=lambda s:ord(s[0])*5/3+cmp(s,s[1:]+s)

Try it online!

\$\endgroup\$
1
  • \$\begingroup\$ Extra points for being able to handle notes like B# and Fb, while still remaining the shortest so far. \$\endgroup\$
    – aimorris
    Feb 20, 2018 at 11:44
7
\$\begingroup\$

JavaScript (ES6), 78 bytes

Saved 1 byte thanks to @Neil

Takes the notes in currying syntax (a)(b).

a=>b=>((g=n=>'0x'+'_46280ab_91735'[parseInt(n+3,36)*2%37%14])(b)-g(a)+23)%12+2

Test cases

let f =

a=>b=>((g=n=>'0x'+'_46280ab_91735'[parseInt(n+3,36)*2%37%14])(b)-g(a)+23)%12+2

console.log(f('A')('C'))   // 4
console.log(f('G')('G#'))  // 2
console.log(f('F#')('B'))  // 6
console.log(f('Bb')('Bb')) // 13

Hash function

The purpose of the hash function is to convert a note into a pointer in a lookup table containing the semitone offsets (C = 0, C# = 1, ..., B = 11), stored in hexadecimal.

We first append a '3' to the note and parse the resulting string in base-36, leading to an integer N. Because '#' is an invalid character, it is simply ignored, along with any character following it.

Then we compute:

H(N) = ((N * 2) MOD 37) MOD 14

Below is a summary of the results.

 note | +'3' | parsed as | base 36->10 |   *2  | %37 | %14 | offset
------+------+-----------+-------------+-------+-----+-----+--------
  C   |  C3  |    c3     |         435 |   870 |  19 |   5 |  0x0
  C#  |  C#3 |    c      |          12 |    24 |  24 |  10 |  0x1
  Db  |  Db3 |    db3    |       17247 | 34494 |  10 |  10 |  0x1
  D   |  D3  |    d3     |         471 |   942 |  17 |   3 |  0x2
  D#  |  D#3 |    d      |          13 |    26 |  26 |  12 |  0x3
  Eb  |  Eb3 |    eb3    |       18543 | 37086 |  12 |  12 |  0x3
  E   |  E3  |    e3     |         507 |  1014 |  15 |   1 |  0x4
  F   |  F3  |    f3     |         543 |  1086 |  13 |  13 |  0x5
  F#  |  F#3 |    f      |          15 |    30 |  30 |   2 |  0x6
  Gb  |  Gb3 |    gb3    |       21135 | 42270 |  16 |   2 |  0x6
  G   |  G3  |    g3     |         579 |  1158 |  11 |  11 |  0x7
  G#  |  G#3 |    g      |          16 |    32 |  32 |   4 |  0x8
  Ab  |  Ab3 |    ab3    |       13359 | 26718 |   4 |   4 |  0x8
  A   |  A3  |    a3     |         363 |   726 |  23 |   9 |  0x9
  A#  |  A#3 |    a      |          10 |    20 |  20 |   6 |  0xa
  Bb  |  Bb3 |    bb3    |       14655 | 29310 |   6 |   6 |  0xa
  B   |  B3  |    b3     |         399 |   798 |  21 |   7 |  0xb

About flats and sharps

Below is the proof that this hash function ensures that a note followed by a '#' gives the same result than the next note followed by a 'b'. In this paragraph, we use the prefix @ for base-36 quantities.

For instance, Db will be converted to @db3 and C# will be converted to @c (see the previous paragraph). We want to prove that:

H(@db3) = H(@c)

Or in the general case, with Y = X + 1:

H(@Yb3) = H(@X)

@b3 is 399 in decimal. Therefore:

H(@Yb3) =
@Yb3 * 2 % 37 % 14 =
(@Y * 36 * 36 + 399) * 2 % 37 % 14 =
((@X + 1) * 36 * 36 + 399) * 2 % 37 % 14 =
(@X * 1296 + 1695) * 2 % 37 % 14

1296 is congruent to 1 modulo 37, so this can be simplified as:

(@X + 1695) * 2 % 37 % 14 =
((@X * 2 % 37 % 14) + (1695 * 2 % 37 % 14)) % 37 % 14 =
((@X * 2 % 37) + 23) % 37 % 14 =
((@X * 2 % 37) + 37 - 14) % 37 % 14 =
@X * 2 % 37 % 14 =
H(@X)

A special case is the transition from G# to Ab, as we'd expect Hb in order to comply with the above formulae. However, this one also works because:

@ab3 * 2 % 37 % 14 = @hb3 * 2 % 37 % 14 = 4
\$\endgroup\$
4
  • \$\begingroup\$ @Neil Thanks! Your optimization saves more bytes than mine. \$\endgroup\$
    – Arnauld
    Feb 20, 2018 at 10:09
  • \$\begingroup\$ Huh, I actually found the reverse with my Batch solution... \$\endgroup\$
    – Neil
    Feb 20, 2018 at 11:33
  • \$\begingroup\$ @Neil Because the sign of the modulo in Batch is the sign of the divisor, I guess? \$\endgroup\$
    – Arnauld
    Feb 20, 2018 at 11:42
  • \$\begingroup\$ No, it's the sign of the dividend, as in JS, but it turned out to be a slightly golfier of correcting for the sign of the result which had been inverted due to an earlier golf. \$\endgroup\$
    – Neil
    Feb 20, 2018 at 12:18
4
\$\begingroup\$

Perl, 39 32 bytes

Includes +1 for p

Give the start and end notes as two lines on STDIN

(echo "A"; echo "C") | perl -pe '$\=(/#/-/b/-$\+5/3*ord)%12+$.}{'; echo

Just the code:

$\=(/#/-/b/-$\+5/3*ord)%12+$.}{
\$\endgroup\$
5
  • \$\begingroup\$ Command line flags are free now \$\endgroup\$
    – wastl
    Feb 20, 2018 at 16:39
  • \$\begingroup\$ @wastl So I've been told. I'd like to know which meta post though so I can go there and disagree :-) \$\endgroup\$
    – Ton Hospel
    Feb 20, 2018 at 18:06
  • \$\begingroup\$ My comment is a link. Feel free to click it. \$\endgroup\$
    – wastl
    Feb 20, 2018 at 18:32
  • \$\begingroup\$ Looks like this works very similarly to mine - but awesomely short for Perl, +1 \$\endgroup\$ Feb 21, 2018 at 23:55
  • \$\begingroup\$ @LevelRiverSt well, this is Ton Hospel. \$\endgroup\$
    – msh210
    Feb 25, 2018 at 17:01
4
\$\begingroup\$

Japt, 27 bytes

®¬x!b"C#D EF G A"ÃrnJ uC +2

Test it online! Takes input as an array of two strings.

Also works for any amount of sharps or flats on any base note!

Explanation

®¬x!b"C#D EF G A"ÃrnJ uC +2   Let's call the two semitones X and Y.
®                Ã            Map X and Y by
 ¬                              splitting each into characters,
  x                             then taking the sum of
   !b"C#D EF G A"               the 0-based index in this string of each char.
                                C -> 0, D -> 2, E -> 4, F -> 5, G -> 7, A -> 9.
                                # -> 1, adding 1 for each sharp in the note.
                                b -> -1, subtracting 1 for each flat in the note.
                                B also -> -1, which happens to be equivalent to 11 mod 12.
                                The sum will be -2 for Bb, 2 for D, 6 for F#, etc.
                              Now we have a list of the positions of the X and Y.
                  rnJ         Reduce this list with reversed subtraction, starting at -1.
                              This gets the difference Y - (X - (-1)), or (Y - X) - 1.
                      uC      Find the result modulo 12. This is 0 if the notes are 1
                              semitone apart, 11 if they're a full octave apart.
                         +2   Add 2 to the result.
\$\endgroup\$
2
\$\begingroup\$

Perl 5 + -p, 66 bytes

s/,/)+0x/;y/B-G/013568/;s/#/+1/g;s/b/-1/g;$_=eval"(-(0x$_-1)%12+2"

Try it online!

Takes comma-separated values. Does also work for Cb, B#, E#, Fb and multiple #/b.

Explanation:

# input example: 'G,G#'
s/,/)+0x/; # replace separator with )+0x (0x for hex) => 'G)+0xG#'
y/B-G/013568/; # replace keys with numbers (A stays hex 10) => '8)+0x8#'
s/#/+1/g; s/b/-1/g; # replace accidentals with +1/-1 => '8)+0x8+1'
$_ = eval # evaluate => 2
    "(-(0x$_-1)%12+2" # add some math => '(-(0x8)+0x8+1-1)%12+2'

Explanation for eval:

(
    - (0x8) # subtract the first key => -8
    + 0x8 + 1 # add the second key => 1
    - 1 # subtract 1 => 0
) % 12 # mod 12 => 0
+ 2 # add 2 => 2
# I can't use % 12 + 1 because 12 (octave) % 12 + 1 = 1, which is not allowed
\$\endgroup\$
2
\$\begingroup\$

Ruby, 56 bytes

->a{a.map!{|s|s.ord*5/3-s[-1].ord/32}
13-(a[0]-a[1])%12}

Try it online!

The letters are parsed according to their ASCII code times 5/3 as follows (this gives the required number of semitones plus an offset of 108)

A    B    C    D    E    F    G
108  110  111  113  115  116  118

The last character (#, b or the letter again) is parsed as its ASCII code divided by 32 as follows

# letter (natural) b 
1  { --- 2 --- }   3

This is subtracted from the letter code.

Then the final result is returned as 13-(difference in semitones)%12

\$\endgroup\$
2
\$\begingroup\$

Stax, 25 24 bytes

╝─°U┤ƒXz☺=≡eA╕δ┴╬\¿☺zt┼§

Run and debug it online

The corresponding ascii representation of the same program is this.

{h9%H_H32/-c4>-c9>-mrE-v12%^^

Effectively, it calculates the keyboard index of each note using a formula, then calculates the resulting interval.

  1. Start from the base note, A = 2, B = 4, ... G = 14
  2. Calculate the accidental offset 2 - code / 32 where code is the ascii code of the last character.
  3. Add them together.
  4. If the result is > 4, subtract 1 to remove B#.
  5. If the result is > 7, subtract 1 to remove E#.
  6. Modularly subtract the two resulting note indexes, and add 1.
\$\endgroup\$
2
  • 1
    \$\begingroup\$ ["F#","B"] should be 6. \$\endgroup\$ Feb 26, 2018 at 5:33
  • 1
    \$\begingroup\$ Thanks. I changed one half of the calculation without adjusting the other. It's fixed. \$\endgroup\$
    – recursive
    Feb 26, 2018 at 5:58
1
\$\begingroup\$

Batch, 136 135 bytes

@set/ac=0,d=2,e=4,f=5,g=7,a=9,r=24
@call:c %2
:c
@set s=%1
@set s=%s:b=-1%
@set/ar=%s:#=+1%-r
@if not "%2"=="" cmd/cset/a13-r%%12

Explanation: The substitutions in the c subroutine replace # in the note name with +1 and b with -1. As this is case insensitive, Bb becomes -1-1. The variables for C...A (also case insensitive) are therefore chosen to be the appropriate number of semitones away from B=-1. The resulting string is then evaluated, and @xnor's trick of subtracting the result from the value gives the desired effect of subtracting the note values from each other. Edit: Finally I use @Arnauld's trick of subtracting the modulo from 13 to achieve the desired answer, saving 1 byte.

\$\endgroup\$
1
\$\begingroup\$

Python 3, 95 bytes

lambda a,b:(g(b)+~g(a))%12+2
g=lambda q:[0,2,3,5,7,8,10][ord(q[0])-65]+" #".find(q.ljust(2)[1])

Try it online!

-14 bytes thanks to user71546

\$\endgroup\$
3
  • \$\begingroup\$ -8 bytes with ord(q[0])-65 replacing "ABCDEFG".find(q[0]) ;) \$\endgroup\$ Feb 20, 2018 at 7:18
  • \$\begingroup\$ Oh, -6 more bytes with (g(b)+~g(a))%12+2 replacing 1+((g(b)-g(a))%12or 12) \$\endgroup\$ Feb 20, 2018 at 7:20
  • \$\begingroup\$ @user71546 oh cool, thanks! \$\endgroup\$
    – hyper-neutrino
    Feb 20, 2018 at 12:26
1
\$\begingroup\$

Jelly, 28 bytes

O64_ṠH$2¦ḅ-AḤ’d5ḅ4µ€IḞṃ12FṪ‘

A monadic link accepting a list of two lists of characters and returning an integer.

Try it online! or see all possible cases.

How?

Performs some bizarre arithmetic on the ordinals of the input characters to map the notes onto the integers zero to twelve and then performs a base-decompression as a proxy for modulo by twelve where zero is then replaced by 12 then adds one.

O64_ṠH$2¦ḅ-AḤ’d5ḅ4µ€IḞṃ12FṪ‘ - Main link, list of lists    e.g. [['F','#'],['B']]  ...or [['A','b'],['G','#']]
                  µ€         - for €ach note list          e.g.  ['F','#'] ['B']          ['A','b'] ['G','#']
O                            - { cast to ordinal (vectorises)    [70,35]   [66]           [65,98]   [71,35]
 64                          -   literal 64
   _                         -   subtract (vectorises)           [-6,29]   [-2]           [-1,-34]  [-7,29]
        ¦                    -   sparse application...
       2                     -   ...to indices: [2] (just index 2)
      $                      -   ...do: last two links as a monad:
    Ṡ                        -          sign                     [-6,1]    [-2]           [-1,-1]   [-7,1]
     H                       -          halve                    [-6,-0.5] [-2]           [-1,-0.5] [-7,0.5]
         ḅ-                  -   convert from base -1            5.5       -2             0.5       7.5
           A                 -   absolute value                  5.5       2              0.5       7.5
            Ḥ                -   double                          11.0      4              1.0       15.0
             ’               -   decrement                       10.0      3              0.0       14.0
              d5             -   divmod by 5                     [2.0,2.0] [0,3]          [0.0,0.0] [2.0,4.0]
                ḅ4           -   convert from base 4             10.0      3              0.0       12.0
                             - } -->                             [10.0,3]                 [0.0,12.0]
                    I        - incremental differences           [-7.0]                   [12.0]
                     Ḟ       - floor (vectorises)                [-7]                     [12]
                      ṃ12    - base decompress using [1-12]      [[5]]                    [[1,12]]
                         F   - flatten                           [5]                      [1,12]
                          Ṫ  - tail                              5                        12
                           ‘ - increment                         6                        13

Also at 28 bytes...

A (not so direct) port of xnor's Python 2 answer...

O×5:3z60_Ṡ¥2¦60U1¦Fḅ-‘N%12+2

Try all possible cases

\$\endgroup\$
0
\$\begingroup\$

CJam, 67 bytes

l',/~)@)@{ciW*50+_z/}_@\~@@~-@@{ci65- 2*_9>-_3>-}_@\~@@~12+\- 12%+)

Online interpreter: http://cjam.aditsu.net/

\$\endgroup\$
0
\$\begingroup\$

Raku, 121 bytes

I write quite a lot of music in EDOs other than 12 so I implemented an option to choose a particular EDO.

{1+(round($^c*log2(3))*([-](map {m:P5/([A-G])(\#*)(b*)/;4613502.comb[$0.ord-65]+7*($1.comb-$2.comb)},($^b,$^a)))%$c||$c)}

No TIO link because TIO has an outdated Rakudo, so here are some examples:

f("A", "C", 12) # => 4
f("Bb", "Bb", 12) # => 13
f("F#", "B", 19) # => 9
f("G", "G#", 31) # => 3
f("C", "Dbb", 31) # => 2
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.