How many semitones

21

1

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.

aimorris

Posted 2018-02-20T06:54:53.920

Reputation: 1 217

I get 2 for G -> G# because they're both included. – HyperNeutrino – 2018-02-20T07:05:17.083

@HyperNeutrino Yep sorry. Mistake on my behalf. – aimorris – 2018-02-20T07:06:15.210

Must we support both F# and Gb or can we assume that we'll only receive one of them? – Stewie Griffin – 2018-02-20T07:53:54.657

1Do we have to cater for notes like Cb or E#? What about double sharps/flats? – Sok – 2018-02-20T08:20:29.163

@StewieGriffin Yes your code must support enharmonic equivalents. I've updated the question to make it clearer. Sorry about any confusion. – aimorris – 2018-02-20T11:37:00.530

1@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. – aimorris – 2018-02-20T11:37:46.330

Such restrictive input format is not well received. Why necessarily comma-separated? Why cannot we take for example two strings? – Luis Mendo – 2018-02-20T22:34:04.527

2Just 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. – Dom – 2018-02-21T04:48:46.590

@Dom Yes I do know this. – aimorris – 2018-02-21T07:11:21.857

@LuisMendo You CAN take it as two strings, in an array. – aimorris – 2018-02-21T07:11:43.733

This is great - much better than your previous music interval challenge, +1 – Level River St – 2018-02-21T23:48:54.920

Amorris re @Dom's note you'll need to change your first point. The largest distance is not 13 semitones, but 12. Apologies for being picky, but we are mods on Music Stack Exchange, so we kinda should be :-) – Rory Alsop – 2018-02-22T07:39:03.933

@RoryAlsop Not sure if you have read the whole challenge, but if you have read it you will realise that it actually is 13 semitones for the scope of this task. – aimorris – 2018-02-23T20:15:58.657

Answers

7

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!

xnor

Posted 2018-02-20T06:54:53.920

Reputation: 115 687

Extra points for being able to handle notes like B# and Fb, while still remaining the shortest so far. – aimorris – 2018-02-20T11:44:30.177

7

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

Arnauld

Posted 2018-02-20T06:54:53.920

Reputation: 111 334

@Neil Thanks! Your optimization saves more bytes than mine. – Arnauld – 2018-02-20T10:09:02.623

Huh, I actually found the reverse with my Batch solution... – Neil – 2018-02-20T11:33:56.363

@Neil Because the sign of the modulo in Batch is the sign of the divisor, I guess? – Arnauld – 2018-02-20T11:42:04.823

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. – Neil – 2018-02-20T12:18:51.000

4

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+$.}{

Ton Hospel

Posted 2018-02-20T06:54:53.920

Reputation: 14 114

Command line flags are free now – wastl – 2018-02-20T16:39:20.423

@wastl So I've been told. I'd like to know which meta post though so I can go there and disagree :-) – Ton Hospel – 2018-02-20T18:06:53.867

My comment is a link. Feel free to click it. – wastl – 2018-02-20T18:32:52.923

Looks like this works very similarly to mine - but awesomely short for Perl, +1 – Level River St – 2018-02-21T23:55:31.793

@LevelRiverSt well, this is Ton Hospel. – msh210 – 2018-02-25T17:01:25.683

4

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.

ETHproductions

Posted 2018-02-20T06:54:53.920

Reputation: 47 880

2

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

wastl

Posted 2018-02-20T06:54:53.920

Reputation: 3 089

2

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

Level River St

Posted 2018-02-20T06:54:53.920

Reputation: 22 049

2

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.

recursive

Posted 2018-02-20T06:54:53.920

Reputation: 8 616

1["F#","B"] should be 6. – Weijun Zhou – 2018-02-26T05:33:40.307

1Thanks. I changed one half of the calculation without adjusting the other. It's fixed. – recursive – 2018-02-26T05:58:35.620

1

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

HyperNeutrino

Posted 2018-02-20T06:54:53.920

Reputation: 26 575

-8 bytes with ord(q[0])-65 replacing "ABCDEFG".find(q[0]) ;) – Shieru Asakoto – 2018-02-20T07:18:57.540

Oh, -6 more bytes with (g(b)+~g(a))%12+2 replacing 1+((g(b)-g(a))%12or 12) – Shieru Asakoto – 2018-02-20T07:20:35.373

@user71546 oh cool, thanks! – HyperNeutrino – 2018-02-20T12:26:13.830

1

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.

Neil

Posted 2018-02-20T06:54:53.920

Reputation: 95 035

1

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

Jonathan Allan

Posted 2018-02-20T06:54:53.920

Reputation: 67 804

0

CJam, 67 bytes

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

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

Chiel ten Brinke

Posted 2018-02-20T06:54:53.920

Reputation: 201