I recently revisited a nice simple string Kata, and thought that this might be as good a place as any to start a series.

The RoLlErCoAsTeR program modifies an input string by alternating capitalisation of each letter. I based my solution on the CodeEval problem here: www.codeeval.com/browse/156/

There are essentially two rules

  1. The first letter should be uppercase, then case should alternate.
  2. Any characters, except for the letters, are ignored during determination of letter case.
    e.g.
"RoLlZ AlOnG" # incorrect
"RoLlZ aLoNg" # correct

The solution

First thing's first, we would ideally use a block that cycles through all the letters in our input string, do something to them and then also give them back. Also, it would be nice to have an index to use for an odd/even number count.

Fortunately, with a slightly unseemly chain, ruby lets us do this:

str.each_char.map.with_index

The each_char method is detailed in the Ruby String docs. The official docs should always be your first port of call when looking for answers.

Huzzah, our block! It does everything we want it to, now we just have to supply the case logic:

module Rollercoaster  
  def self.transform(msg)
    msg.each_char.map.with_index do |char, index|
      index.even? ? char : char.upcase
    end.join
  end
end  
puts Rollercoaster.transform(ARGV[0]) if $PROGRAM_NAME == __FILE__

Everything looks good, right?

Unfortunately, this doesn't satisfy the second rule about skipping non-letter characters in the case decisions.

freman ~/dev/kata/rollercoaster  
$ ruby rollercoaster.rb "message for tom"
mEsSaGe fOr tOm

In this example, the 'f' in 'for' should be the uppercase to be the correct continuation of the pattern.

So, lets take control of our index variable! To start with, we'll move it out into its own var

def self.transform(msg)  
  index = 0
  msg.each_char.map do |char|
    index.even? ? char : char.upcase
  end.join
end

Next, we're going to have to make a method that tells us if a character is alphabetical or not
The match method is out as of Ruby 2.4.0, and is very quick at regex matches
There is an amazing unicode regex that matches on any alphabet character from any language.

irb(main):001:0> char.match?(/\p{L}/)

=> true

So let's throw it in! The only other piece of logic to go in is to make sure we only increment the index if the character was alphabetical:

module RollercoasterV1
  def self.generate(msg)
    index = 0
    msg.each_char.map do |char|
      index += 1 if !(char =~ /\p{L}/).nil?
      index.even? ? char : char.upcase
    end.join
  end
end

puts RollercoasterV1.generate(ARGV[0]) if $PROGRAM_NAME == __FILE__
 ∫ ▻ ruby rollercoaster.rb "This is roller coast"

ThIs Is RoLlEr CoAsT  

Refactor

After finishing the first solution I was struck by the fact that the readability of the generate method could be improved.

Lets split the alphabet character test and the encrytion into their own methods.

module RollercoasterV2
  def self.generate(msg)
    index = 0
    msg.each_char.map do |char|
      index += 1 if alpha?(char)
      translate(char, index)
    end.join
  end

  def self.alpha?(char)
    !(char =~ /\p{L}/).nil?
  end

  def self.translate(char, index)
    index.even? ? char : char.upcase
  end
end

if $PROGRAM_NAME == __FILE__
  puts RollercoasterV2.generate("this is a rollercoaster message")
end

This design reveals more issues with the overall design

  • The generate method is in charge of incrementing the index
  • The generate method is in charge of switching encryption based on alpha?

So, what are we going to do?

  • Move this functionality into the translate method.
  • Make two seperate classes for iterating the string and translating a character - it means we can plug in different ciphers easily.
module StringTransform

  class RollercoasterV3

    def initialize
      @index = 0
    end

    def translate(char)
      @index += 1 if alpha?(char)
      @index.even? ? char : char.upcase
    end

    def alpha?(char)
      !(char =~ /\p{L}/).nil?
    end
  end

end

module StringTransform

  CIPHERS = {
    rollercoaster: RollercoasterV3
  }.freeze

  def self.generate(msg, cipher)
    cipher = CIPHERS[cipher].new
    msg.each_char.inject("") do |msg, char|
       msg << cipher.translate(char)
    end
  end

end

if $PROGRAM_NAME == __FILE__
  puts StringTransform.generate(
    "this is a rollercoaster message",
    :rollercoaster
  )
end

Done! Until next time.