Perl Weekly Challenge – 2019-03-25

I’m not sure that I’ll have time to do these every week, but here are my answers to this week’s two Perl Weekly Challenges.

Challenge #1

Write a script to replace the character ‘e’ with ‘E’ in the string ‘Perl Weekly Challenge’. Also print the number of times the character ‘e’ found in the string.

Nothing really complicated here. We can discuss why I reached for tr/.../.../ rather than s/.../.../. I just think it’s silly to invoke the full power of the regex engine when you’re only changing a few letters.

Challenge #2

Write one-liner to solve FizzBuzz problem and print number 1-20. However, any number divisible by 3 should be replaced by the word fizz and any divisible by 5 by the word buzz. Numbers divisible by both become fizz buzz.

This is a bit more interesting. And I have to say that I think this is deeply dirty code. Over the twenty plus years I’ve been writing Perl I’ve trained myself to write code that is use strict and use warnings clean. This code is very much not that and with warnings turned on, you’d see a lot of warnings!

So how does it work?

The core is the (string)[index] construct.  We all know that you can access an individual element in an array with code like $my_array[$index]. But you can also do this with a list of values. For example:

returns “bar”. I often use this when I only need some of the values returned by localtime().

Here, I’m using something very similar. (fizz)[$_ % 3] is looking up a value in a list that contains only a single value (the string “fizz”). So if the index is zero then we get the first (and only) value in the list. If the index is anything other than zero then we’re looking for an element that’s off the end of the list and Perl gives us undef, which is interpreted as an empty string. And the index expression, $_ % 3, gives us zero if $_ is exactly divisable by 3. There are two things here that would throw warnings under use strict and use warnings. Firstly, “fizz” is a bareword and secondly, we’re using undef in a concatenation.

We then repeat the same logic using “buzz” and 5 and we concatenate together the results of those two expressions. That gives us either “fizz”, “buzz”, “fizzbuzz” or the empty string. Of those values, only the empty string is seen as false, so we can use || $_ to replace that with the original number.

There’s one mystery left. Why have I put that + before the first expression? I think I’m going to leave that as an exercise for the reader. If you work it out, please leave a comment.

And there we have it. A solution that is simultaneously both horribly dirty and yet, I think, rather clever.

Have you come up with your own solution yet?

Update:

It’s been pointed out to me that my solution for challenge #2 prints “fizzbuzz” for 15, when the specification clearly asks for “fizz buzz” (with a space). So here’s a version which fixes that:

8 thoughts on “Perl Weekly Challenge – 2019-03-25

  1. From the print documentation: “Be careful not to follow the print keyword with a left parenthesis unless you want the corresponding right parenthesis to terminate the arguments to the print; put parentheses around all arguments (or interpose a +, but that doesn’t look as good).”

    1. That’s a very puzzling piece of documentation. Not because it isn’t true, but because in this aspect, print isn’t different from any other function or subroutine in Perl. Why the documentation feels the need to warn for this at print() is just baffling.

  2. perl -MAcme::FizzBuzz -e ''

    Or

    perl -E 'say for 1,2,fizz,4,buzz,fizz,7,8,fizz,buzz,10,11,fizz,13,14,fizzbuzz,16,17,fizz,19,buzz'

    You need to put the + after the say because if you don’t the program won’t work.

  3. Building off your solution:

    perl -E 'say join (" ", grep {$_} ("fizz" x !($_ % 3)), ("buzz" x !($_ % 5))) || $_ for 1 .. 20'

  4. Seems like everybody except me knew about tr and y returning number of matches instead of just 1 and undef 😔 I’m proposing the following challenge for week III: to find where this is documented in perlop 😊

    #2 oneliner can be written as

    perl -E 'say((join" ",$_%3?():"fizz",$_%5?():"buzz")||$_)for(1..20)'

    without the need for grep.

    My solution for #1 is:

    perl -pe "s/.*/('-;-,}k:@+,$^?@>,@@.]>{#'^'}^_@]<_%@@]~|(_@,%@:[[|')/e;s/([e_])/\$i++,\$1eq'e'?'E':--\$i/ge"<<<""

  5. I’m a bit confused about the update regarding the space between fizz and buzz. Looking back at the challenge, it says:

    Those numbers that are both divisible by 3 and 5 become ‘fizzbuzz’.

    I don’t see a space between fizz and buzz.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.