Joe Letizia

This is where I write my simple ramblings on martial arts, software, life, and other nonsense.

Truncate, Ruby Arrays, and Negative Indexing

14 Nov 2013

Many times when working with user input, you won't necessarily want to display the entire string if it is over a certain length. Let's say I allow my users to set their company name:

"Kirk, McCoy, Chekov & Associates Law Firm of New Jersey"

If I wanted to display this text in a grid view, this may be significantly more text than I want to display. To deal with that, we can truncate the string on presentation. A fair way to do this is to display a set of characters to denote that a string has been truncated so that the user doesn't get worried that their data has been manipulated unexpectedly. A nice demarkation of that is the ellipsis (…).

Rails comes with this functionality built into ActiveSupport.

company_name = "Kirk, McCoy, Chekov & Associates Law Firm of New Jersey"
company_name.truncate(15) 
# => "Kirk, McCoy, Chekov & Assoc..."

I ran into a weird bug with this method the other night. I wasn't actually trying to use #truncate this way, but I was certainly surprised by the behavior. Calling #truncate with a length less than the length of your omission (ellipsis by default) yields some unexpected results.

company_name.truncate(2) 
# => "Kirk, McCoy, Chekov & Associates Law Firm of New Jerse..."

Huh?! What's going on here? I was a bit surprised by this, so my pair and I pulled up the code for the #truncate method, seen below.

def truncate(truncate_at, options = {})
  return dup unless length > truncate_at

  options[:omission] ||= '...'
  length_with_room_for_omission = truncate_at - options[:omission].length
  stop = if options[:separator]
    rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
  else
    length_with_room_for_omission
  end

  "#{self[0...stop]}#{options[:omission]}"
end

The key part to note here is the calculation of length_with_room_for_omission. Since the ellipsis is a 3 character string, passing a value of 2 for truncate_at will result in a negative number. Ruby has a property for arrays known as negative indexing. In many other languages (C#, Java, C), passing in a negative value as an index to an array results in some kind of indexing error/exception. However, in Ruby, negative values will wrap around the end of the string.

"hello"[-2]
# => "l"

So, passing stop as an index to the range for the string index results in the string wrapping around.