Sam Rayner

Hi, I’m Sam. I design and build apps for iOS and the web. I live in Sheffield, UK and play Ultimate far too much. You can reach me via email or on Twitter.

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 a 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

2016

2015

July

2014

February