19

Why does the titlecase mess up the name? I have:

John Mark McMillan

and it turns it into:

>> "john mark McMillan".titlecase
=> "John Mark Mc Millan"

Why is there a space added to the last name?

Basically I have this in my model:

before_save :capitalize_name

def capitalize_name
  self.artist = self.artist.titlecase
end

I am trying to make sure that all the names are titlecase in the DB, but in situtations with a camelcase name it fails. Any ideas how to fix this?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Matt Elhotiby
  • 43,028
  • 85
  • 218
  • 321
  • 2
    Just to be clear: this is not Ruby per se, String#titlecase is added by ActiveSupport, a part of Rails. – Rein Henrichs Apr 11 '11 at 00:32
  • my bad...your right because it doesnt work via irb – Matt Elhotiby Apr 11 '11 at 00:37
  • it *would* work via `irb` if you `require`d `active_support` first – iconoclast Jan 09 '13 at 16:17
  • 2
    This is a great question; surprised it hasn't come up more often! I saw a "McClure" come up as "Mc Clure" in my app's view, but when I checked the Admin, it was entered properly, which led me here. Thanks for asking this question. For others: the last answer on the page is actually the one that solved it for me, though I believe all the solutions work; I dropped it in my inflections.rb file (seemed reasonable enough since that initializer deals with words), but it could go anywhere. What won't work is putting a new String class inside eg ApplicationController class as it's then a subclass. – rcd Apr 03 '13 at 18:37

10 Answers10

17

You can always do it yourself if Rails isn't good enough:

class String
    def another_titlecase
        self.split(" ").collect{|word| word[0] = word[0].upcase; word}.join(" ")
    end
end

"john mark McMillan".another_titlecase
 => "John Mark McMillan" 

This method is a small fraction of a second faster than the regex solution:

My solution:

ruby-1.9.2-p136 :034 > Benchmark.ms do
ruby-1.9.2-p136 :035 >     "john mark McMillan".split(" ").collect{|word|word[0] = word[0].upcase; word}.join(" ")
ruby-1.9.2-p136 :036?>   end
 =>  0.019311904907226562 

Regex solution:

ruby-1.9.2-p136 :042 > Benchmark.ms do
ruby-1.9.2-p136 :043 >     "john mark McMillan".gsub(/\b\w/) { |w| w.upcase }
ruby-1.9.2-p136 :044?>   end
 => 0.04482269287109375 
Mike Lewis
  • 63,433
  • 20
  • 141
  • 111
  • 1
    A word of caution: Joerg Mittag once tried to roll his own, and regarded it as impractical, given all the edge cases. – Andrew Grimm Apr 11 '11 at 01:59
  • of course, but in this specific case if Rails is not a valid solution, obviously you must use something else. – Mike Lewis Apr 11 '11 at 01:59
  • 1
    But your first answer changes the 'm' after 'Mc' into a small letter. That is not what is asked. – sawa Apr 11 '11 at 02:57
  • Here's the link to Joerg Mittag's attempt: http://stackoverflow.com/questions/1791639/converting-upper-case-string-into-title-case-using-ruby/1792102#1792102 – Andrew Grimm Apr 14 '11 at 02:35
  • This does address the question, however, if you're trying to normalize, this solution doesn't account for all caps- name will remain all caps. – webaholik Feb 12 '19 at 21:23
  • Automatically adjusting the case of names never going to be reliable. You have edge cases like "della Porta", but you never know if a name was changed. Both MacDonald and Macdonald exist. Probably also Della Porta. Van Der Meer and van der Meer. A complete solution needs to handle exceptional cases properly. – iconoclast Aug 18 '23 at 14:24
  • Also, the above attempt only accounts for spaces, not hyphens or apostrophes. Don't you want to correct Rhys-davies to Rhys-Davies, and O'brien to O'Brien? (At least usually "O'brien" is probably a mistake, but you'd need to ask the individual to know with 100% certainty.) – iconoclast Aug 18 '23 at 14:26
4

Hmm, that's odd.. but you could write a quick custom regex to avoid using that method.

class String
    def custom_titlecase
        self.gsub(/\b\w/) { |w| w.upcase }
    end
end

"John Mark McMillan".custom_titlecase    # => "John Mark McMillan"

Source

Jon Gauthier
  • 25,202
  • 6
  • 63
  • 69
3

If all you want is to ensure that each word starts with a capital:

class String
  def titlecase2
    self.split(' ').map { |w| w[0] = w[0].upcase; w }.join(' ')
  end
end

irb(main):016:0> "john mark McMillan".titlecase2
=> "John Mark McMillan"
2

If you want to handle the case where someone has entered JOHN CAPSLOCK JOE as well as the others, I combined this one:

class String
  def proper_titlecase
    if self.titleize.split.length == self.split.length
      self.titleize
    else
      self.split(" ").collect{|word| word[0] = word[0].upcase; word}.join(" ")
    end
  end
end

Depends if you want that kinda logic on a String method ;)

nickj
  • 100
  • 1
  • 7
2

Edited (inspired by The Tin Man's suggestion)

A hack will be:

class String
  def titlecase
    gsub(/(?:_|\b)(.)/){$1.upcase}
  end
end

p "john mark McMillan".titlecase
# => "John Mark McMillan"

Note that the string 'john mark McMillan' is inconsistent in capitalization, and is somewhat unexpected as a human input, or if it is not from a human input, you probably should not have the strings stored in that way. A string like 'john mark mc_millan' is more consistent, and would more likely appear as a human input if you define such convention. My answer will handle these cases as well:

p "john mark mc_millan".titlecase
# => "John Mark McMillan"
sawa
  • 165,429
  • 45
  • 277
  • 381
  • 2
    And, what if the input came from a user, who typed it in without proper case? Should the user be chastened or criticized for not putting in the caps? Or should they be told they can't type their name in spelled correctly but, instead, have to use an unexpected character in it to keep the code from mangling it? That is a totally unacceptable thing to do from a human-interface aspect. – the Tin Man Apr 11 '11 at 02:40
  • I agree that using an underscore is not so expectable from a human input. But having a wierdly capitalized 'john mark McMillan' is even less likely. A human, in that case, would rather type in correct capitalization or with lower case all the way (in which case the information of the boundary after 'Mc' would be lost), and suggesting the user to type an underscore would be better than that. However, I wasn't thinking about the possibility that the input may come from human, and I think you made a point. – sawa Apr 11 '11 at 02:48
  • This works; is elegant, simple, and if you don't want to have to rewrite anything, it's pretty much copy and paste and you're done, app-wide. Kudos. – rcd Apr 03 '13 at 18:17
1

The documentation for titlecase says ([emphasis added]):

Capitalizes all the words and replaces some characters in the string to create a nicer looking title. titleize is meant for creating pretty output. It is not used in the Rails internals.

I'm only guessing here, but perhaps it regards PascalCase as a problem - maybe it thinks it's the name of a ActiveRecordModelClass.

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
  • I always treated the methods in [ActiveSupport::CoreExtensions::String::Inflections](http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html) as useful for messing with table names and mapping to and from internal strings to more human readable ones, but there's no attempt to make the results fit into spelling of names. That is a more complex problem. – the Tin Man Apr 11 '11 at 02:48
1

We have just added this which supports a few different cases that we face.

class String
  # Default titlecase converts McKay to Mc Kay, which is not great
  # May even need to remove titlecase completely in the future to leave 
  # strings unchanged
  def self.custom_title_case(string = "")
    return "" if !string.is_a?(String) || string.empty?

    split = string.split(" ").collect do |word|
      word = word.titlecase

      # If we titlecase and it turns in to 2 words, then we need to merge back
      word = word.match?(/\w/) ? word.split(" ").join("") : word
      
      word
    end
    
    return split.join(" ")
  end
end

And the rspec test

# spec/lib/modules/string_spec.rb
require 'rails_helper'
require 'modules/string'

describe "String" do
  describe "self.custom_title_case" do
    it "returns empty string if incorrect params" do
      result_one = String.custom_title_case({ test: 'object' })
      result_two = String.custom_title_case([1, 2])
      result_three = String.custom_title_case()

      expect(result_one).to eq("")
      expect(result_two).to eq("")
      expect(result_three).to eq("")
    end

    it "returns string in title case" do
      result = String.custom_title_case("smiths hill")
      expect(result).to eq("Smiths Hill")
    end

    it "caters for 'Mc' i.e. 'john mark McMillan' edge cases" do
      result_one = String.custom_title_case("burger king McDonalds")
      result_two = String.custom_title_case("john mark McMillan")
      result_three = String.custom_title_case("McKay bay")

      expect(result_one).to eq("Burger King McDonalds")
      expect(result_two).to eq("John Mark McMillan")
      expect(result_three).to eq("McKay Bay")
    end

    it "correctly cases uppercase words" do
      result = String.custom_title_case("NORTH NARRABEEN")
      expect(result).to eq("North Narrabeen")
    end
  end
end
Ken Greeff
  • 55
  • 5
0

You may also encounter names with two capital letters, such as McLaren, McDonald etc.

Have not spent time trying to improve it, but you could always do

Code

# Rails.root/config/initializers/string.rb
class String
  def titleize_name
    self.split(" ")
      .collect{|word| word[0] = word[0].upcase; word}
      .join(" ").gsub(/\b('?[a-z])/) { $1.capitalize }
  end
end

Examples

[2] pry(main)> "test name".titleize_name
=> "Test Name"
[3] pry(main)> "test name-name".titleize_name
=> "Test Name-Name"
[4] pry(main)> "test McName-name".titleize_name
=> "Test McName-Name"
cb24
  • 577
  • 5
  • 7
0

You're trying to use a generic method for converting Rail's internal strings into more human readable names. It's not designed to handle "Mc" and "Mac" and "Van Der" and any number of other compound spellings.

You can use it as a starting point, then special case the results looking for the places it breaks and do some fix-ups, or you can write your own method that includes special-casing those edge cases. I've had to do that several times in different apps over the years.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
-1

The "Why" question has already been answered...but as evidenced by the selected answer and upvotes, I think what most of us are ACTUALLY wanting is a silver bullet to deal with the hell that is name-formatting...While multiple capitals trigger that behavior, I've found that hyphenated names do the same.

These cases and many more have already been handled in the gem, NameCase.

In version 2.0 it only converts a string if the string is all uppercase or all lowercase, based on a defined ruleset as a best guess. I like this, because I'm sure the ruleset can never be 100% correct. Example, Ian McDonald (from Scotland) has a different capitalization from Ian Mcdonald (from Ireland)...however those names will be handled correctly at the time of input if the user is particular and if not, the name can be corrected if needed and retain its formatting.

My Solution:

# If desired, add string method once NameCase gem is added
class String

  def namecase
    NameCase(self)
  end

end

Tests: (name.namecase)

test_names = ["john mark McMillan", "JOHN CAPSLOCK JOE", "test name", "test name-name", "test McName-name", "John w McHENRY", "ian mcdonald", "Ian McDonald", "Ian Mcdonald"]

test_names.each { |name| puts '# "' + name + '" => "' + name.namecase + '"' }
  # "john mark McMillan" => "John Mark McMillan"
  # "JOHN CAPSLOCK JOE" => "John Capslock Joe"
  # "test name" => "Test Name"
  # "test name-name" => "Test Name-Name"
  # "test McName-name" => "Test McName-Name"
  # "John w McHENRY" => "John w McHENRY" -FAIL
  # "ian mcdonald" => "Ian McDonald"
  # "Ian McDonald" => "Ian McDonald"
  # "Ian Mcdonald" => "Ian Mcdonald"

If you feel you need to handle all of the corner cases on this page and don't care about losing names that may have been formatted at the start, eg. Ian Mcdonald (from Ireland)...you could use upcase first:

Tests: (name.upcase.namecase)

test_names.each { |name| puts '# "' + name + '" => "' + name.upcase.namecase + '"' }
  # "john mark McMillan" => "John Mark McMillan"
  # "JOHN CAPSLOCK JOE" => "John Capslock Joe"
  # "test name" => "Test Name"
  # "test name-name" => "Test Name-Name"
  # "test McName-name" => "Test McName-Name"
  # "John w McHENRY" => "John W McHenry"
  # "ian mcdonald" => "Ian McDonald"
  # "Ian McDonald" => "Ian McDonald"
  # "Ian Mcdonald" => "Ian McDonald"

The only silver bullet is to go old school...ALL CAPS. But who wants that eyesore in their modern web app?

webaholik
  • 1,619
  • 1
  • 19
  • 30