Sam Rayner

Hi, I’m Sam. I design and build cool stuff for the Web. I’m currently studying Software Engineering at the University of Sheffield, UK. You can reach me via email or on Twitter.

Latest Blog Post

Dynamic Regular Expressions

I faced an interesting challenge while developing Sprint.ly for Alfred recently; how to provide a preview of a story title as the user typed it. A story is a concept from Agile software development that encapsulates the who, what and why of a feature.

As a site owner, I want a contact form so that visitors can get in touch.

First off, I created a Story class to hold the components of a story and a title method to get them out as a formatted string:

class Story
  attr_accessor :who, :what, :why

  def title
    #declare our separators
    prefixes = {
      who: "As a",
      what: "I want",
      why: "so that"
    }

    #go all grammar nazi
    prefixes[:who] << "n" if @who && ["a","e","i","o"].include?(@who[0,1])

    #format the story into a string
    "#{prefixes[:who]} #{@who}, #{prefixes[:what]} #{@what} #{prefixes[:why]} #{@why}"
  end
end

The tricky bit would be taking a string as input and parsing it into those attributes. I needed the title broken up to submit the parts separately to the Sprint.ly API as well as generate the preview on the fly.

I wanted the preview to start out as “As a __, I want __ so that __”, with the blanks being filled in as the user typed. I also wanted to give the option of a aa iw st shorthand to speed up entry, and to allow the blanks to be filled in any order.

Unable to write standard regex that would cut it, I ended up with this system of appending to a regex as matches were found:

def parse_title(title)
  #default values
  @who = "__"
  @what = "__"
  @why = "__"

  #define our capture triggers
  captures = {
    "who" => "as an?|aa",
    "what" => " i want| iw",
    "why" => " so that| st"
  }

  #get things started
  regex_str = '^\s*'

  #if the prefix is present, append a named capture group to the regex
  captures.each do |key,val|
    prefix = '(?:'+val+')\s+'
    if title.match(Regexp.new(prefix, true))
      regex_str << prefix+'(?<'+key+'>.+)'
    end
  end

  #apply the final regex built from the string
  matches = title.match(Regexp.new(regex_str, true))

  if matches
    matches.names.each do |name| 
      value = matches[name].strip
      #strip out any trailing comma from the who
      value.sub!(/,$/, "") if name == "who"
      #store each component in the instance variables
      self.send(name+"=", value)
    end
  end
end

The solution greedily matches everything after a trigger phrase until another trigger is encountered and then breaks the string down.

With that done, the last thing was to trigger parsing by calling a setter method on every keystroke:

def title=(value)
  self.parse_title(value)
end

And it works pretty well!

Download Sprint.ly for Alfred to give it a spin. If you know a smarter way of doing this, I’d be really interested to hear it on Twitter or via email.

Post Archive

2013

May

January

2012

November

October

September

August