17
\$\begingroup\$

I'm a musician, and I need more polyrhythms in my life!

A polyrhythm occurs in music (and in nature) when two events (claps, notes, fireflies flashing etc.) are occurring at two different regular intervals. The two kinds of event happen a different number of times in the same interval.

If I tap with my left hand twice, and with my right hand 3 times, in the same space of time, it looks a little bit like this:

  ------
R . . .
L .  .  

The hyphens at the top denote the length of the polyrthmic pattern, which is the lowest common multiple or 2 and 3. This can be understood as the point at which the pattern repeats.

There's also a 'metarhythm', which is the pattern produced when either hand is tapping:

  ------
R . . .
L .  .  
M . ...

This is a simple, and very common polyrhythm, with a ratio of 3:2.

Let's just say I don't want to do a simple polyrhythm that I can work out in my head, so I need something to work it out for me. I could do it long-form on paper, or...


Rules:

  • Write some code to generate and display a polyrhythm diagram, as described above.
  • Any old language, try for the fewest bytes.
  • Your code takes two arguments:
    • Number of taps with the Left hand (positive integer)
    • Number of taps with the right hand (positive integer)
  • It will work out the length, which is the lowest common multiple for the two arguments.
  • The top line will consist of two whitespace characters followed by hyphens displaying the length (length * '-')
  • The second and third lines will show the pattern for the right and left hands:
    • It will start with an R or L, do denote which hand it is, followed by a space.
    • The interval for that hand is the length divided by it's argument.
    • The taps will start at the third character, denoted by any character you choose. From then on it will display the same character 'interval' characters apart.
    • It will not be longer than the length line.
  • The fourth line is the metarhythm:
    • It will start with an upper case M, followed by a space.
    • From the third character onwards, it will show a character (any character you choose) in every position where there's a tap on either the right or the left hand.
  • Trailing whitespace is irrelevant.

Test cases:

r = 3, l = 2

  ------
R . . .
L .  .  
M . ...

r = 4, l = 3

  ------------
R .  .  .  .    
L .   .   .    
M .  .. . ..

r = 4, l = 5

  --------------------
R .    .    .    .                     
L .   .   .   .   .      
M .   ..  . . .  ..

r = 4, l = 7

  ----------------------------
R .      .      .      .      
L .   .   .   .   .   .   .   
M .   .  ..   . . .   ..  .

r = 4, l = 8

  --------
R . . . . 
L ........
M ........

Happy golfing!

\$\endgroup\$
  • \$\begingroup\$ Your test cases include lots of trailing whitespace, can we omit them/add more? \$\endgroup\$ – wastl Aug 1 '18 at 11:57
  • \$\begingroup\$ Do we have to accept r and l as two separate values? Could we accept a two-element array instead, for example? How about the order of them, is that strictly r followed by l? \$\endgroup\$ – Sok Aug 1 '18 at 15:00
  • \$\begingroup\$ @Sok That's acceptable as an interpretation of 'two arguments' \$\endgroup\$ – AJFaraday Aug 1 '18 at 15:02
  • \$\begingroup\$ Does it need to actually print the diagram out, or can it simply return it? \$\endgroup\$ – iamnotmaynard Aug 1 '18 at 17:15
  • \$\begingroup\$ @iamnotmaynard returning is fine. \$\endgroup\$ – AJFaraday Aug 1 '18 at 18:26

15 Answers 15

6
\$\begingroup\$

JavaScript (ES6), 131 bytes

Outputs 0 as the tap character.

r=>l=>`  ${g=n=>n?s.replace(/./g,(_,x)=>[,a=x%(k/r),x%=k/l,a*x][n]&&' '):++k%l|k%r?'-'+g():`-
`,s=g(k=0)}R ${g(1)}L ${g(2)}M `+g(3)

Try it online!

How?

We use the same helper function \$g()\$ for two different purposes.

When \$g()\$ is called with no argument or an argument equal to \$0\$, it recursively builds the hyphen string of length \$k=\operatorname{lcm}(l,r)\$ with a trailing linefeed:

g = _ => ++k % l | k % r ? '-' + g() : `-\n`

This string is saved in \$s\$.

When \$g()\$ is called with \$1 \le n \le 3\$, it generates a tap string by replacing each hyphen at position \$x\$ in \$s\$ with either a space or \$0\$:

g = n => s.replace(/./g, (_, x) => [, a = x % (k / r), x %= k / l, a * x][n] && ' ')
\$\endgroup\$
4
\$\begingroup\$

Java 11, 226 234 233 219 bytes

String h(int r,int l,int m){var s="";for(;m>0;)s+=m%r*(m--%l)<1?'.':32;return s;}

r->l->{int a=r,b=l,m;for(;b>0;b=a%b,a=m)m=b;m=r*l/a;return"  "+repeat("-",m)+"\nR "+h(m/r,m+1,m)+"\nL "+h(m/l,m+1,m)+"\nM "+h(m/r,m/l,m);}

Kind of lengthy; too bad Java does not have an lcm() function. Try it online here (TIO does not have Java 11 yet, so this uses a helper method instead of String.repeat()).

My initial version took the interval between taps instead of the number of taps. Fixed now. Thanks to Kevin Cruijssen for golfing 1 byte.

Ungolfed:

String h(int r, int l, int m) { // helper function returning a line of metarhythm; parameters are: tap interval (right hand), tap interval (left hand), length
    var s = ""; // start with an empty String
    for(; m > 0; ) // repeat until the length is reached
        s += m % r * (m-- % l) < 1 ? '.' : 32; // if at least one of the hands taps, add a dot, otherwise add a space (ASCII code 32 is ' ')
    return s; // return the constructed line
}

r -> l -> { // lambda taking two integers in currying syntax and returning a String
    int a = r, b = l, m; // duplicate the inputs
    for(; b > 0; b = a % b, a = m) // calculate the GCD of r,l using Euclid's algorithm:
        m=b; // swap and replace one of the inputs by the remainder of their division; stop once it hits zero
    m = r * l / a; // calculate the length: LCM of r,l using a=GCD(r,l)
    return // build and return the output:
    "  " + "-".repeat(m) // first line, m dashes preceded by two spaces
    + "\nR " + h(m / r, m + 1, m) // second line, create the right-hand rhythm; by setting l = m + 1 for a metarhythm, we ensure there will be no left-hand taps
    + "\nL " + h(m / l, m + 1, m) // third line, create the left-hand rhythm the same way; also note that we pass the tap interval instead of the number of taps
    + "\nM " + h(m / r, m / l, m); // fourth line, create  the actual metarhythm
}
\$\endgroup\$
  • \$\begingroup\$ It's not much, but -1 byte by changing ?".":" " to ?'.':32. \$\endgroup\$ – Kevin Cruijssen Aug 1 '18 at 14:42
  • \$\begingroup\$ @KevinCruijssen Every byte counts :-) Thanks! \$\endgroup\$ – O.O.Balance Aug 1 '18 at 14:49
4
\$\begingroup\$

Python 2, 187 185 183 174 166 156 148 147 145 bytes

Uses - as the tap character

a,b=r,l=input()
while b:a,b=b,a%b
w=r*l/a
for x,y,z in zip(' RLM',(w,r,l,r),(w,r,l,l)):print x,''.join('- '[i%(w/y)!=0<i%(w/z)]for i in range(w))

Try it online!


Saved:

  • -2 bytes, thanks to Jonathan Frech
\$\endgroup\$
  • \$\begingroup\$ [i%(w/y)and i%(w/z)>0] could be [i%(w/y)!=0<i%(w/z)]. \$\endgroup\$ – Jonathan Frech Aug 1 '18 at 17:27
  • \$\begingroup\$ @JonathanFrech Thanks :) \$\endgroup\$ – TFeld Aug 2 '18 at 6:57
3
\$\begingroup\$

Perl 6, 85 80 78 bytes

-2 bytes thanks to Jo King.

{«'  'R L M»Z~'-'x($!=[lcm] @_),|(@_.=map:{' '~(0~' 'x$!/$_-1)x$_}),[~|] @_}

Try it online!

Returns a list of four lines.

\$\endgroup\$
3
\$\begingroup\$

Python 2, 185 228 223 234 249 bytes

def f(r,l):
     c='.';d=' ';M,R,L=[r*l*[d]for _ in d*3]
     for i in range(r*l):
      if i%r<1:L[i]=M[i]=c
      if i%l<1:R[i]=M[i]=c
      if r<R.count(c)and l<L.count(c):R[i]=L[i]=M[i]=d;break
     print d,i*'-','\nR',''.join(R),'\nL',''.join(L),'\nM',''.join(M)

Try it online!

\$\endgroup\$
  • \$\begingroup\$ I've just copy-pasted this into TIO and taken the generated format from there. Turns out it's done in fewer bytes than you thought ;) \$\endgroup\$ – AJFaraday Aug 1 '18 at 14:05
  • \$\begingroup\$ @Tfeld r=4, l=8 works fine for me \$\endgroup\$ – sonrad10 Aug 1 '18 at 16:37
  • \$\begingroup\$ The length is supposed to be the lowest common multiple. With r=4, l=8, that should be 8, but it appears your output is much longer (8*4?). \$\endgroup\$ – O.O.Balance Aug 1 '18 at 16:57
  • 1
    \$\begingroup\$ That still does not give the LCM; eg for 15,25, it gives 375, but it should be 75. \$\endgroup\$ – O.O.Balance Aug 1 '18 at 19:08
  • 1
    \$\begingroup\$ I believe the last check can be replaced by i%r+i%l+0**i<1. Also, you can remove previous versions of code, as they will be preserved in your edit history of anyone wants to see them \$\endgroup\$ – Jo King Aug 3 '18 at 15:14
2
\$\begingroup\$

Jelly, 32 bytes

æl/Ḷ%Ɱµa/ṭ=0ị⁾. Z”-;ⱮZ“ RLM”żK€Y

Try it online!

Takes input as a list [L,R].

æl/       Get LCM of this list.
   Ḷ      Range [0..LCM-1]
    %Ɱ    Modulo by-each-right (implicitly the input, [L,R]):
           [[0%L ... (LCM-1)%L], [0%R ... (LCM-1)%R]]
µ         Take this pair of lists, and:
 a/ṭ      Append their pairwise AND to the pair.
    =0    Is zero? Now we have a result like:
              [[1 0 0 1 0 0 1 0 0 1 0 0 1 0 0]
               [1 0 0 0 0 1 0 0 0 0 1 0 0 0 0]
               [1 0 0 1 0 1 1 0 0 1 1 0 1 0 0]]

ị⁾.       Convert this into dots and spaces.
Z”-;ⱮZ    Transpose, prepend a dash to each, transpose. Now we have
              ['---------------'
               '.  .  .  .  .  '
               '.    .    .    '
               '.  . ..  .. .  ']

“ RLM”ż       zip(' RLM', this)
       K€     Join each by spaces.
         Y    Join the whole thing by newlines.
\$\endgroup\$
1
\$\begingroup\$

C (gcc), 204 bytes

p(s){printf(s);}
g(a,b){a=b?g(b,a%b):a;}
h(r,l,m){for(;m;)p(m%r*(m--%l)?" ":".");}
f(r,l,m,i){m=r*l/g(r,l);p("  ");for(i=m;i-->0;)p("-");p("\nR ");h(m/r,m+1,m);p("\nL ");h(m/l,m+1,m);p("\nM ");h(m/r,m/l,m);}

Port of my Java answer. Call with f(number_of_right_hand_taps, number_of_left_hand_taps). Try it online here.

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

Pyth, 53 bytes

j.b+NYc"  L R M "2++*\-J/*FQiFQKm*d+N*\ t/JdQsmeSd.TK

Definitely room to golf. Will do so when I have time.
Try it here

Explanation

j.b+NYc"  L R M "2++*\-J/*FQiFQKm*d+N*\ t/JdQsmeSd.TK
                       J/*FQiFQ                        Get the LCM.
                    *\-                                Take that many '-'s.
                               Km*d+N*\ t/dJQ          Fill in the taps.
                                             smeSd.TK  Get the metarhythm.
                  ++                                   Append them all.
      c"  L R M "2                                     Get the prefixes.
 .b+NY                                                 Prepend the prefixes.
j                                                      Join with newlines.
\$\endgroup\$
1
\$\begingroup\$

C# (Visual C# Interactive Compiler), 254 bytes


Golfed Try it online!

(r,l)=>{int s=l>r?l:r,S=s;while(S%l>0|S%r>0)S+=s;string q(int a){return"".PadRight(S/a,'.').Replace(".",".".PadRight(a,' '));}string R=q(S/r),L=q(S/l),M="";s=S;while(S-->0)M=(R[S]+L[S]>64?".":" ")+M;return"  ".PadRight(s+2,'-')+$"\nR {R}\nL {L}\nM {M}";}

Ungolfed

( r, l ) => {
    int
        s = l > r ? l : r,
        S = s;

    while( S % l > 0 | S % r > 0 )
        S += s;

    string q( int a ) {
        return "".PadRight( S / a, '.' ).Replace( ".", ".".PadRight( a, ' ' ) );
    }

    string
        R = q( S / r ),
        L = q( S / l ),
        M = "";

    s = S;

    while( S-- > 0 )
        M = ( R[ S ] + L[ S ] > 64 ? "." : " " ) + M;

    return "  ".PadRight( s + 2, '-') + $"\nR {R}\nL {L}\nM {M}";
}

Full code

Func<Int32, Int32, String> f = ( r, l ) => {
    int
        s = l > r ? l : r,
        S = s;

    while( S % l > 0 | S % r > 0 )
        S += s;

    string q( int a ) {
        return "".PadRight( S / a, '.' ).Replace( ".", ".".PadRight( a, ' ' ) );
    }

    string
        R = q( S / r ),
        L = q( S / l ),
        M = "";

    s = S;

    while( S-- > 0 )
        M = ( R[ S ] + L[ S ] > 64 ? "." : " " ) + M;

    return "  ".PadRight( s + 2, '-') + $"\nR {R}\nL {L}\nM {M}";
};

Int32[][]
    testCases = new Int32[][] {
        new []{ 3, 2 },
        new []{ 4, 3 },
        new []{ 4, 5 },
        new []{ 4, 7 },
        new []{ 4, 8 },
    };

foreach( Int32[] testCase in testCases ) {
    Console.Write( $" Input: R: {testCase[0]}, L: {testCase[1]}\nOutput:\n{f(testCase[0], testCase[1])}" );
    Console.WriteLine("\n");
}

Console.ReadLine();

Releases

  • v1.0 - 254 bytes - Initial solution.

Notes

  • None
\$\endgroup\$
1
\$\begingroup\$

Charcoal, 52 bytes

≔θζW﹪ζη≧⁺θζζ↙≔⮌Eζ⟦¬﹪×ιθζ¬﹪×ιηζ⟧ζFζ⊞ι⌈ι↓Eζ⭆ι§ .λ←↓RLM

Try it online! Link is to verbose version of code. Explanation:

≔θζW﹪ζη≧⁺θζ

Calculate the LCM of the inputs by taking the first multiple of R that's divisible by L.

ζ↙

Print the LCM, which automatically outputs the necessary row of -s. Then move to print the rhythm from right to left.

≔⮌Eζ⟦¬﹪×ιθζ¬﹪×ιηζ⟧ζ

Loop over the numbers from the LCM down to 0 and create an array of lists representing the beats of the right and left hands.

Fζ⊞ι⌈ι

Loop over the beats and add the metarhythm.

↓Eζ⭆ι§ .λ

Print the reversed beats downwards, but as this is an array they end up leftwards.

←↓RLM

Print the header.

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

Ruby, 130 126 bytes

->*a{puts"  "+?-*s=a[0].lcm(a[1])
r,l=a.map!{|e|(?.+' '*(s/e-1))*e}
[?R,?L,?M].zip(a<<r.gsub(/ /){l[$`.size]}){|e|puts e*" "}}

Try it online!

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

Python 2, 117 bytes

a,b=input();n=a
while n%b:n+=a
for i in-1,1,2,3:print'_RLM '[i],''.join(' -'[i%2>>m*a%n|i/2>>m*b%n]for m in range(n))

Try it online!

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

Pyth, 49 bytes

J/*FQiFQjC+c2" RLM    "ms@L" -"!M++0d*Fdm%Ld/LJQJ

Expects input in the form [r,l]. Uses - to display taps. Try it online here, or verify all test cases at once here.

J/*FQiFQjC+c2" RLM    "ms@L" -"!M++0d*Fdm%Ld/LJQJ   Implicit: Q=eval(input())
 /*FQiFQ                                            Compute LCM: (a*b)/(GCD(a,b))
J                                                   Store in J
                                        m       J   Map d in [0-LCM) using:
                                            /LJQ      Get number of beats between taps for each hand
                                         %Ld          Take d mod each of the above
                                                    This gives a pair for each beat, with 0 indicating a tap
                       m                            Map d in the above using:
                                     *Fd              Multiply each pair (effecively an AND)
                                 ++0d                 Prepend 0 and the original pair
                               !M                     NOT each element
                        s@L" -"                       Map [false, true] to [' ', '-'], concatenate strings
                                                    This gives each column of the output
           c2" RLM    "                             [' RLM','    ']
          +                                         Prepend the above to the rest of the output
         C                                          Transpose
        j                                           Join on newlines, implicit print
\$\endgroup\$
1
\$\begingroup\$

R, 161 149 146 bytes

function(a,b){l=numbers::LCM(a,b)
d=c(0,' ')
cat('  ',strrep('-',l),'\nR ',d[(x<-l:1%%a>0)+1],'\nL ',d[(y<-l:1%%b>0)+1],'\nM ',d[(x&y)+1],sep='')}

Try it online!

I definitely feel like there's room for improvement here, but I tried a few different approaches and this is the only one that stuck. Getting rid of the internal function definition would make me quite happy, and I tried a bunch of restructures of the cat() to make it happen. Nevermind, as soon as I posted I realised what I could do. Still definitely some efficiency savings to be found.

There are other LCM functions in libraries with shorter names, but TIO has numbers and I considered that more valuable at this point.

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

C++ (gcc), 197 bytes

int f(int a,int b){std::string t="  ",l="\nL ",r="\nR ",m="\nM ";int c=-1,o,p=0;for(;++p%a||p%b;);for(;o=++c<p;t+="-")l+=a*c%p&&++o?" ":".",r+=b*c%p&&++o?" ":".",m+=o-3?".":" ";std::cout<<t+l+r+m;}

Try it online!

\$\endgroup\$
  • \$\begingroup\$ Suggest ++p%a+p%b instead of ++p%a||p%b \$\endgroup\$ – ceilingcat Sep 22 '18 at 16:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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