Archive for marzo, 2012

Coding dojo en CSD, segundo intento

Esta mañana los BeCodianos me han llamado “el rey del if-then-else” por mi solución al Coding kata propuesto por CSD la semana pasada. ¿Ah si?

La solución, refactorizada (en pastebin, para mayor legibilidad), y los tests.

La mejoras:

  • He dividido la clase Parseator en dos, creando una clase separada para el modelo de la gramática a parsear, y otro para el parseador en si.
  • He usado atajos de Ruby y construcciones tipo case para simplificar (o no) el código.

Coding Dojo en CSD

La semana pasada me pasé por el segundo coding dojo en CSD, para conocer a otros desarrolladores y estirar los músculos de programar.

La kata en si no era muy compleja (enunciado en PDF), pero el truco estaba en que había que programarla usando técnicas de TDD. Interesante, puesto que una de las desventajas del TDD es que la velocidad inicial de desarrollo es bastante baja.

Para desarrollar la kata nos emparejaron e hicimos dos pomodoros de desarrollo con cinco minutos de descanso para discutir en grupo por dónde íbamos.

Resumen de la kata

La kata en si consistía en desarrollar un intérprete de líneas de comando al que se le pasan flags de tres tipos:

  • -l: Flag booleano
  • -p 8080: Flag numérico
  • -d /tmp/log: Flag de texto

El primer paso es desarrollar un esquema en el que se indiquen los flags que acepta el programa, de qué tipo son y el valor por defecto si se omite el flag. Una vez tenemos el esquema, el programa tiene que ser capaz de interpretar una cadena de entrada, analizar si es sintácticamente correcta, analizar si es semánticamente correcta y lanzar excepciones si la entrada no es correcta.

Finalmente… antes de enseñaos la solución

Desde aquí, gracias a la gente de CSD por organizar el Dojo en las mazmorras de su oficina y por echarnos de comer y beber (free beer!) mientras programábamos. ;)

Nuestra solución

Aunque no conseguimos resolver la kata en dos pomodoros, aquí estos son los tests y la solución en Ruby que creamos mi compañero Yago y yo.

Los tests:

require_relative '../lib/parseator.rb'

describe Parseator do
  before :each do
    @example_in = {"l" => ["bool", false],
                           "p" => ["number", 8080],
                           "d" => ["string", "/usr/local"] } 
    @parser = Parseator.new(@example_in)
  end
  describe ", Sintactic Tests" do
    it "instantiates a Parseator object" do
     @parser.should be_an_instance_of Parseator
    end
    it "parses the empty string as valid" do
      @parser.valid?("").should ==  true
    end
    it "fails if first flag does not start with -" do
      @parser.valid?("poop").should ==  false
    end
    it "parses a single boolean flag" do
      @parser.valid?("-t").should ==  true
    end
    it "parses N boolean flags" do
      @parser.valid?("-p -m").should == true
    end 
    it "parses a single flag with args" do
      @parser.valid?("-p 8080").should == true
    end 
    it "parses a mix of boolean and arg flags without negative numbers" do
      @parser.valid?("-p 8080 -m -l /hola/yo").should == true
    end 
    it "parses a mix of boolean and arg flags without negative numbers with traling and leading spaces" do
      @parser.valid?("-p 8080 -m -l /hola/yo").should == true
    end 
    it "fails with an incorrect mix of boolean and arg flags with negative numbers" do
      @parser.valid?("-p 8080 -m -500 p -l /hola/yo").should == false
    end 
    it "parses a mix of boolean and arg flags with negative numbers" do
      @parser.valid?("-p 8080 -m -500 -l /hola/yo").should == true
    end 
    it "parses a mix of boolean and arg flags with negative numbers without spaces before argument" do
      @parser.valid?("-p8080 -m -500 -l /hola/yo").should == true
    end 
  end

  describe ", Semantic Tests, " do
    it "raises exception when passing a flag that is not defined" do
      lambda { @parser.parse("-t") }.should raise_error UndefinedParam
    end
    it "returns a hash with default value when passing a single boolean flag" do
      test_string = "-l"
      @parser.parse(test_string).should have_key("l") 
      @parser.parse(test_string)["l"].should == true
    end
    it "returns a hash with false when passing an empty string, for the boolean flag" do
      @parser.parse("").should have_key("l") 
      @parser.parse("")["l"].should == false
    end
    it "returns a hash with value when passing a single string" do
      test_string = "-d /usr/bin"
      @parser.parse(test_string).should have_key("d") 
      @parser.parse(test_string)["d"].should == "/usr/bin"
    end
    it "returns a hash with value when passing a single number" do
      test_string = "-p 8080"
      @parser.parse(test_string).should have_key("p") 
      @parser.parse(test_string)["p"].should == 8080 
    end
    it "returns a hash with value when passing a single negative number" do
      test_string = "-p -8080"
      @parser.parse(test_string).should have_key("p") 
      @parser.parse(test_string)["p"].should == -8080 
    end
    it "returns correctly when passing mix of strings, booleans and numbers" do
      test_string = "-p -8080 -l -d /usr/test"
      @parser.parse(test_string).should have_key("p") 
      @parser.parse(test_string)["p"].should == -8080 
      @parser.parse(test_string).should have_key("d") 
      @parser.parse(test_string)["d"].should == "/usr/test"
      @parser.parse(test_string).should have_key("l") 
      @parser.parse(test_string)["l"].should == true
    end
    it "defaults correctly with some params defined" do
      test_string = "-p -8080 -d /usr/test"
      @parser.parse(test_string).should have_key("l") 
      @parser.parse("")["l"].should == false
    end
    it "defaults correctly with no params defined, aka empty string test" do
      test_string = "-p -8080 -d /usr/test"
      @parser.parse(test_string).should have_key("l") 
      @parser.parse("")["l"].should == false
    end
    it "fails when passing valid and invalid flags" do
      test_string = "-p -8080 -m /usr/test"
      lambda { @parser.parse(test_string) }.should raise_error UndefinedParam
    end
    it "raises exception, with explanation test when passing a flag that is not defined" do
      lambda { @parser.parse("-t") }.should raise_error UndefinedParam, "Syntax error. Correct format is UNIMPLEMENTED"
    end
    it "raises exception, with explanation, passing a defined flag with wrong param" do
      lambda { @parser.parse("-l /usr/test") }.should raise_error IncorrectParam, "Incorrect parameter -l /usr/test"
    end
    it "raises exception, with explanation, passing a number flag to a string flag" do
      lambda { @parser.parse("-d 8080") }.should raise_error IncorrectParam, "Incorrect parameter -d 8080"
    end
    it "raises exception, with explanation, passing a number flag with wrong param" do
      lambda { @parser.parse("-p /usr/test") }.should raise_error IncorrectParam, "Incorrect parameter -p /usr/test"
    end
  end
end

El código:

class UndefinedParam < StandardError; end
class IncorrectSyntax < StandardError; end
class IncorrectParam < StandardError; end

# Extend class string to check if params are numbers (floats included)
class String
  def is_numeric?
    begin Float(self) ; true end rescue false
  end
end

class Parseator
  # Constructor takes as parameter a hash with the valid flags, type and default value
  def initialize(model_hash)
    @model = model_hash
  end

  # valid? returns true if params is syntactically correct
  def valid?(params)
    if params == ""
      return true
    else
      if params =~ /^\s*(\-[a-zA-Z]{1}\s*(\-?[0-9]+|[^-\s]+)*\s*)*\s*$/
        return true
      else
        return false
      end
    end
  end

  # returns a Hash with the parameters and the set values
  # or raises an exception if there are Syntax or Semantic errors
  def parse(params)
    params_out = Hash.new
    if valid?(params) # First, check there are no syntax errors
      # first go through the input string and add that to the results 
      # match[0] is the flag
      # match[1] is the value
      params.scan(/\-([a-zA-Z]{1})\s*(\-?[0-9]+|[^-\s]+)*/).each do |match|
        if @model.has_key?(match[0])
          # bool params
          if @model[match[0]][0] == "bool"
            unless match[1].nil? # bool types do not have a value
              raise IncorrectParam, "Incorrect parameter -#{match[0]} #{match[1]}"
            end
            params_out[match[0]] = true
          # string params
          elsif @model[match[0]][0] == "string"
            if match[1].nil? || match[1].is_numeric? 
              raise IncorrectParam, "Incorrect parameter -#{match[0]} #{match[1]}"
            end
            params_out[match[0]] = match[1]
          # number params
          elsif @model[match[0]][0] == "number"
            if match[1].nil? || !match[1].is_numeric?
              raise IncorrectParam, "Incorrect parameter -#{match[0]} #{match[1]}"
            end
            params_out[match[0]] = match[1].to_i
          end
        else
          raise UndefinedParam, "Syntax error. Correct format is UNIMPLEMENTED"
        end
      end

      # second, go through the model and see what params are missing, to add the defaults 
      @model.keys.each do |key|
        unless params_out.has_key?(key)
          params_out[key] = @model[key][1] # default value
        end
      end
      return params_out
    else # Raise syntax error if not valid?
      # This method can be refined to analyze the string for the location of the syntax error(s)
      # Left as an exercise for the reader
      raise IncorrectSyntax
    end
  end
end

Ahora que lo miro… @model huele a refactoring…