3
\$\begingroup\$

Below is the code I have written to capitalize all the words of a sentence except if

  1. The words belong to the littleWords list.

  2. The word would be capitalized if it's the first word of the sentence even if it is in the littleWords list.

def titleize(sentence)
    littleWords = ["end", "over", "and", "the"]
    words = sentence.split(/^(\w+)\b/)
    sentence = if words[2] 
        words[2].split(" ").map do |word| 
            littleWords.include?(word) ? (" " + word) : (" " + word.titleize)  
        end
    end
    words[1].titleize + (sentence||[]).join("")
end

SPEC

describe "titleize" do
it "capitalizes a word" do
  titleize("jaws").should == "Jaws"
end

it "capitalizes every word (aka title case)" do
  titleize("david copperfield").should == "David Copperfield"
end

it "doesn't capitalize 'little words' in a title" do
  titleize("war and peace").should == "War and Peace"
end

it "does capitalize 'little words' at the start of a title" do
  titleize("the bridge over the river kwai").should == "The Bridge over the River Kwai"
end
end

I am new to ruby/script and am coming from Java. The code above doesnt looks as nice and clean as I think could be done with ruby.

\$\endgroup\$
4
\$\begingroup\$

Rather than split and join the string, it'd be simpler to

  1. Always capitalize the sentence itself, so it always starts with an uppercase letter
  2. Pass a block to gsub, letting it do the filtering.

Also: a minor thing but unlike Java, Ruby favors snake_case rather than camelCase for names. So conventionally, it'd be little_words, not littleWords.

Here's a simplistic implementation:

def titleize(sentence)
  little_words = %w(end over and the)
  sentence.capitalize.gsub(/(\w+)/) do |word|
    little_words.include?(word) ? word : word.capitalize
  end
end

Of course, capitalize doesn't just make the leading letter uppercase, it also foribly downcases the rest of the string. So writing, say, "DNA and RNA" will incorrectly give you "Dna and Rna". It isn't fond of unicode characters either, so caveat emptor.

Basically, that "titleizing" a string is bit of hornet's nest. There are multiple schools of thought on how it should be done, some depending on context. And right when you think you have it, someone writes a URL or a name like "iPhone" in a sentence, and it comes out wrong anyway.

A slightly more clever - but still brittle! - solution might be:

def titleize(sentence)
  little_words = %w(end over and the)
  sentence.gsub(/\b(\p{Ll}+)\b/) do |word|
    # The following breaks codereview's syntax highlighting, but it's valid Ruby code.
    # I used a "full" if-else rather than a ternary just to keep the lines shorter.
    if $`.empty? || !little_words.include?(word)
      word.capitalize
    else
      word
    end
  end
end

The regex only matches words that are all-lowercase meaning "DNA" and "iPhone" will pass though undisturbed. But we can't capitalize the string in its entirety because it'd just make everything except the first word an all-lowercase word. So instead, we have the $` "magic" variable, which contains the string before the current match. If it's empty, we're at the start, and should capitalize the word even if it's on the small_words list.

But again, this isn't a great solution at all. It's just here to illustrate some regex voodoo.

And in any event this isn't a new problem. Here's a gem that sounds like it's a port of this Perl script, which, if nothing else, comes with an explanation of its working.

Edit: As tokland points out in the comments, using a Set instead of an array for litte_words would make for faster lookups; no searching necessary. It'd also be nicer to define the list of little words as a constant, rather than declare it as a local variable when the method's run:

LITTLE_WORDS = %w{ end over and the }
  #=> ["end", "over", "and", "the"]

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Two notes: 1) Why the inplace update? A simpler little_words.include?(word) ? word : word.capitalize should do. " 2) Maybe it's worth adding a note on using a set for O(1) inclusion instead of an array? \$\endgroup\$ – tokland Feb 8 '15 at 14:28
  • 1
    \$\begingroup\$ @tokland re 1) just me being sleepy and trying to code; a ternary is indeed better. And re 2) I'll add a note. \$\endgroup\$ – Flambino Feb 8 '15 at 14:40
  • \$\begingroup\$ I virtually never edit questions or answers written by others, but I could not help myself here (as it could not be done in a comment). By all means remove my addition if it displeases. \$\endgroup\$ – Cary Swoveland Mar 4 '15 at 20:53
  • \$\begingroup\$ @CarySwoveland Hehe, good one :) \$\endgroup\$ – Flambino Mar 4 '15 at 20:58
1
\$\begingroup\$

First, kudos on including specs to show what you're trying to achieve. Titleize already does most of what you want (and I presume you are running within Rails or otherwise using ActiveSupport since it's not in core Ruby). You just want to add downcasing of little_words that are not at the beginning of a sentence.

A few notes: Variables should use an underscore to separate words and you want to use two space indents and single quotes. You should call your method something different than the built-in titleize. I recommend running Rubocop to help learn Ruby style idioms.

def custom_titleize(sentence)
  little_words = %w(end over and the)
  sentence.titleize.split(/\W+/).each_with_index.map do |word, index|
    index.zero? || little_words.exclude?(word.downcase) ? word : word.downcase
  end.join(' ')
end

My other suggestion is that you might want to monkey patch String as follows, to follow the pattern of other Rails Inflectors (note that including self.titleize... is unnecessary):

class String
  def custom_titleize
    little_words = %w(end over and the)
    titleize.split(/\W+/).each_with_index.map do |word, index|
      index.zero? || little_words.exclude?(word.downcase) ? word : word.downcase
    end.join(' ')
  end
end

The you can do: 'war and peace'.custom_titleize

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.