10
\$\begingroup\$

Background

In Western music, every single music note has an assigned name. Within each octave, there are twelve unique notes in the following order: "C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B C", where the final C is one octave above the first.

To tell the difference between notes of different octaves, a number (for this challenge restricted to a single digit) is appended to the end of the note name. Thus, C5 is the note that is one octave above C4. Bb6 is above B5.

An important fact is that B5 and C6 are notes that are right next to each other, and that C0 and B9 are the lowest and highest notes.

Between any two notes, there is a distance which is the number of semitones between them. Bb4 is one semitone below B4, which is itself one semitone below C5. There are twelve semitones in an octave, so Bb4 is a distance of 12 from A#3 since it is an octave above it (notice how a single note can have up to two names).

The Challenge

Your challenge is to write the shortest possible program that can take a list of music notes from STDIN and print the list of interval changes to STDOUT.

Input will be a space-separated list of music notes. Each note will consist of an uppercase letter A-G, an optional b or # sign, and a single digit number. You will not have to deal with E#/Fb or B#/Cb. Example input:

C4 D4 E4 F4 G4 A4 B4 C5 C4

Output will be a space-separated list of integers which represent the distance between each successive note, always prefixed with an + or - to show whether the note was ascending or descending relative to the one before it. There will always be one less number outputted than notes inputted. Example output for the above input:

+2 +2 +1 +2 +2 +2 +1 -12

Some more example inputs:

E5 D#5 E5 B4 E5 F#5 E5 B4
C0 B0 Bb1 A2 G#3 G4 F#5 F6
G4 Ab4 Gb4 A4 F4 A#4

And their corresponding outputs:

-1 +1 -5 +5 +2 -2 -5
+11 +11 +11 +11 +11 +11 +11
+1 -2 +3 -4 +5

Rules and Restrictions

  1. The winner is determined by the number of characters in the source code

  2. Your program should consist of only printable ASCII characters

  3. You are not allowed to use any sort of built-in function that is related to music or sound

  4. Other than that, standard code golf rules apply

\$\endgroup\$
4
  • \$\begingroup\$ Should it print +0 or -0 or 0 for two identical notes? \$\endgroup\$
    – Howard
    Apr 7, 2012 at 8:53
  • \$\begingroup\$ @Howard Since I didn't specify, either one is acceptable. \$\endgroup\$
    – PhiNotPi
    Apr 7, 2012 at 11:47
  • 1
    \$\begingroup\$ "Bb4 is one semitone below B4, which is itself one semitone below C4". You mean C5 at the end of that, right? \$\endgroup\$ Apr 8, 2012 at 2:36
  • \$\begingroup\$ Wow, never noticed that. Thanks for spotting the error. It's fixed now. \$\endgroup\$
    – PhiNotPi
    Apr 8, 2012 at 2:43

6 Answers 6

6
\$\begingroup\$

GolfScript, 61

" "/{)12*\{"bC#D EF G A B"?(+}/}%(\{.@-.`\0>{"+"\+}*\}/;]" "*
\$\endgroup\$
4
\$\begingroup\$

Haskell, 161 characters

f(c:r)=maybe(12*read[c])(+f r).lookup c$zip"bC#D.EF.G.A.B"[-1..]
g n|n<0=show n|1<3='+':show n
h x=zipWith(-)(tail x)x
main=interact$unwords.map g.h.map f.words
\$\endgroup\$
4
\$\begingroup\$

Perl, 103

#!/usr/bin/perl -an
/.((b)|(\D))?/,(($~,$,)=($,,12*$'+ord()%20%7*2+(ord()%7>3)-$-[2]+$-[3]))[0]&&printf'%+d ',$,-$~for@F
\$\endgroup\$
3
\$\begingroup\$

C, 123 characters

Based on leftaroundabout's solution, with some improvements.

main(c,b,d)
    char*b;
{
    while(d=c,~scanf("%s",b)?c=-~*b*1.6,c%=12,c+=b[~b[1]&16?c+=1-b[1]/40,2:1]*12:0)
        d>1&&printf("%+d ",c-d);
}

Some tricks that I think are worth mention:
1. argv[0] (here called b) is a pointer to the program name, but used here as a scratch buffer. We only need 4 bytes (e.g. C#2\0), so we have enough.
2. c is the number of arguments, so it starts as 1 (when run without arguments). We use it to prevent printing on the first round.

Possible problem - c+=b[..c+=..] is kind-of strange. I don't think it's undefined behavior, because ?: is a sequence point, but maybe I'm wrong.

\$\endgroup\$
3
  • \$\begingroup\$ If you think of it as c = c + b[..c+=..], then it's pretty clearly undefined behavior. Regardless of the sequencing within [..], you don't know if the outer c is fetched before, during, or after b[..]. \$\endgroup\$
    – ephemient
    Apr 12, 2012 at 4:17
  • \$\begingroup\$ @ephemient, I guess in theory a compiler could do REG=c;REG+=b[..c+=..];c=REG. However, I'll be surprised to see something like this in practice. But it's still UB. \$\endgroup\$
    – ugoren
    Apr 12, 2012 at 8:39
  • \$\begingroup\$ It's Code Golf – we've already invoked UB by using scanf without a prototype, and that's okay. It's just good to know what is and isn't legal in real life :) \$\endgroup\$
    – ephemient
    Apr 12, 2012 at 20:32
2
\$\begingroup\$

C, 241 229 183

F(c){c-=65;return c*1.6+sin(c/5.+.3)+9;}
#define r if(scanf("%s",b)>0){c=F(*b)%12;c+=b[b[1]<36&&++c||b[1]>97&&c--?2:1]*12
main(e,c,d){char b[4];r;}s:d=c;r;printf("%+d ",c-d);goto s;}}
\$\endgroup\$
5
  • \$\begingroup\$ Instead of doing the plus sign yourself, you can just do printf("%+d ",c-d). \$\endgroup\$
    – hammar
    Apr 7, 2012 at 2:18
  • \$\begingroup\$ You can omit includes ideone.com/G00fS \$\endgroup\$
    – Hauleth
    Apr 7, 2012 at 14:54
  • \$\begingroup\$ Very nice. Some suggestions: F(*b-65) instead of c-=65;, b[1]<36&&++c||b[1]>97&&c--?2:1 -> b[1]&16?1:(c+=b[1]%2*2-1,2), abuse argv by: main(e,b,c,d)char*b{ (Use the first argument pointer as a work buffer). \$\endgroup\$
    – ugoren
    Apr 8, 2012 at 11:49
  • \$\begingroup\$ Another one - I think c=F(*b)%12 can be replaced with c=-~*b*1.6;c%=12. Why? sin in the original F can be replaced with 9.6. c*1.6+9.6 is (c+6)*1.6, c-=65 and (c+6) become c-59, and then c+1 (60*96%12==0). \$\endgroup\$
    – ugoren
    Apr 8, 2012 at 12:27
  • \$\begingroup\$ Thanks for all the suggestions! They work fine and make it shorter, but I think I'll leave it as it is now; it wouldn't really be my solution anymore without the sine. \$\endgroup\$ Apr 8, 2012 at 19:38
1
\$\begingroup\$

Factor, 303 characters

USING: combinators formatting io kernel math regexp sequences ;
f contents R/ [#-b]+/ all-matching-slices
[ 0 swap [ {
{ [ dup 48 < ] [ drop 1 ] }
{ [ dup 65 < ] [ 48 - 12 * ] }
{ [ dup 98 < ] [ "C-D-EF-G-A-B" index ] }
[ drop -1 ]
} cond + ] each
swap [ over [ - "%+d " printf ] dip ] when* ] each drop

With comments,

! combinators => cond
! formatting => printf
! io => contents
! kernel => swap dup drop over dip when*
! math => < - * +
! regexp => R/ all-matching-slices
! sequences => each
USING: combinators formatting io kernel math regexp sequences ;

f       ! Push false, no previous note value.

! Find notes (as string slices) in standard input. The golfed regexp
! R/ [#-b]+/ matches all notes and no whitespace.
contents R/ [#-b]+/ all-matching-slices

! For each string slice:
[
    0       ! Push 0, initial note value.
    swap    ! Move note slice to top of stack, above note value.

    ! For each Unicode codepoint in note:
    [
        ! Convert the Unicode codepoint to its value in semitones.
        ! For golf, [ 48 ] is shorter than [ CHAR: A ].
        {
            ! Sharp # {35} has 1 semitone.
            { [ dup 48 < ] [ drop 1 ] }
            ! 0-9 {48-57} has 0 to 9 octaves (1 octave = 12 semitones).
            { [ dup 65 < ] [ 48 - 12 * ] }
            ! A-G {65-71} has 0 to 11 semitones.
            { [ dup 98 < ] [ "C-D-EF-G-A-B" index ] }
            ! Flat b {98} has -1 semitone.
            [ drop -1 ]
        } cond

        +       ! Add semitones to cumulative note value.
    ] each

    swap    ! Move previous note value to top of stack.
    ! When there is a previous note value:
    [
        ! Keep current note on stack.
        over [
            ! Compute and print interval.
            - "%+d " printf
        ] dip
    ] when*
    ! Current note replaces previous note at top of stack.
] each

drop    ! Drop previous note, so stack is empty.

For this script, a "space-separated list" can have 1 or more spaces between elements, and 0 or more spaces at the beginning or end. This script does print an extra space at end of output, but it also accepts an extra space (or newline) at end of input.

If I would adopt a stricter definition, where a "space-separated list" has exactly 1 space between elements, and 0 spaces at the beginning or end, then I can shorten contents R/ [#-b]+/ all-matching-slices to contents " " split (using splitting, not regexp). However, I would need to add more code to prevent the extra space at end of output.

If I use the deprecated word dupd, I can shorten over [ - "%+d " printf ] dip to dupd - "%+d " printf, saving 8 characters. I am not using deprecated words because they "are intended to be removed soon."

\$\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.