I'm looking for some help on how to take an attribute and process it through a method to return something different. But I've never done this before and I' not sure where to start. I thought trying to change a name:string attribute from "George Washington" or "John Quincy Adams" into first names only "George" and "John".
I thought maybe a helper method would be best, such as
users_helper.rb
def first_name
end
and then call #user.name.first_name, would this be initially how it would work? Can someone explain where I'd go next to be able to pass #user.name into the method? I've seen things like this but don't quite understand it the parenthesis...
def first_name(name)
puts name
end
Could someone breakdown how rails/ruby does this type of thing? Thanks a lot!
Some people have more than two names, such as "John Clark Smith". You can choose to treat them as:
(1) first_name: "John", last_name: "Smith"
def first_name
if name.split.count > 1
name.split.first
else
name
end
end
def last_name
if name.split.count > 1
name.split.last
end
end
(2) first_name: "John Clark", last_name: "Smith"
def first_name
if name.split.count > 1
name.split[0..-2].join(' ')
else
name
end
end
def last_name
if name.split.count > 1
name.split.last
end
end
(3) first_name: "John", last_name: "Clark Smith"
def first_name
name.split.first
end
def last_name
if name.split.count > 1
name.split[1..-1].join(' ')
end
end
The above examples assume that if the name contains less than 2 words then it is a first name.
The parentheses (which are optional) enclose the parameter list.
def first_name(full_name)
full_name.split(" ")[0]
end
This assumes the parameter is not nil.
> puts first_name "Jimmy McKickems"
Jimmy
> puts first_name "Jeezy"
Jeezy
But this is not a string method, as your assumption is now:
#user.full_name.first_name # Bzzt.
Instead:
first_name #user.name
This could be wrapped up in the model class itself:
class User < ActiveRecord
# Extra stuff elided
def first_name
self.full_name.blank? ? "" : self.full_name.split(" ")[0]
end
end
The extra code checks to see if the name is nil or whitespace (blank? comes from Rails). If it is, it returns an empty string. If it isn't, it splits it on spaces and returns the first item in the resulting array.
In case you are looking to split only once and provide both parts this one liner will work:
last_name, first_name = *name.reverse.split(/\s+/, 2).collect(&:reverse)
Makes the last word the last name and everything else the first name. So if there is a prefix, "Dr.", or a middle name that will be included with the first name. Obviously for last names that have separate words, "Charles de Gaulle" it won't work but handling that is much harder (if not impossible).
Use Ruby's Array#pop
For my needs I needed to take full names that had 1, 2, 3 or more "names" in them, like "AUSTIN" or "AUSTIN J GILLEY".
The Helper Method
def split_full_name_into_first_name_and_last_name( full_name )
name_array = full_name.split(' ') # ["AUSTIN", "J", "GILLEY"]
if name_array.count > 1
last_name = name_array.pop # "GILLEY"
first_name = name_array.join(' ') # "AUSTIN J"
else
first_name = name_array.first
last_name = nil
end
return [ first_name, last_name ] # ["AUSTIN J", "GILLEY"]
end
Using It
split_full_name_into_first_name_and_last_name( "AUSTIN J GILLEY" )
# => ["AUSTIN J", "GILLEY"]
split_full_name_into_first_name_and_last_name( "AUSTIN" )
# => ["AUSTIN", nil]
And you can easily assign the first_name and last_name with:
first_name, last_name = split_full_name_into_first_name_and_last_name( "AUSTIN J GILLEY" )
first_name
# => "AUSTIN J"
last_name
# => "GILLEY"
You can modify from there based on what you need or want to do with it.
For the syntax you're asking for (#user.name.first_name) Rails does a lot of this sort of extension by adding methods to base types, in your example you could do this through defining methods on the String class.
class String
def given; self.split(' ').first end
def surname; self.split(' ').last end
end
"Phileas Fog".surname # 'fog'
Another way to do something like this is to wrap the type you whish to extend, that way you can add all the crazy syntax you wish without polluting more base types like string.
class ProperName < String
def given; self.split(' ').first end
def surname; self.split(' ').last end
end
class User
def name
ProperName.new(self.read_attribute(:name))
end
end
u = User.new(:name => 'Phileas Fog')
u.name # 'Phileas Fog'
u.name.given # 'Phileas'
u.name.surname # 'Fog'
Just as complement of great Dave Newton's answer, here is what would be the "last name" version:
def last_name
self.full_name.blank? ? "" : self.full_name.split(" ")[-1]
end
making it simple
class User < ActiveRecord::Base
def first_name
self.name.split(" ")[0..-2].join(" ")
end
def last_name
self.name.split(" ").last
end
end
User.create name: "John M. Smith"
User.first.first_name
# => "John M."
User.first.last_name
# => "Smith"
Thanks
def test_one(name)
puts "#{name.inspect} => #{yield(name).inspect}"
end
def tests(&block)
test_one nil, &block
test_one "", &block
test_one "First", &block
test_one "First Last", &block
test_one "First Middle Last", &block
test_one "First Middle Middle2 Last", &block
end
puts "First name tests"
tests do |name|
name.blank? ? "" : name.split(" ").tap{|a| a.pop if a.length > 1 }.join(" ")
end
puts "Last name tests"
tests do |name|
name.blank? ? "" : (name.split(" ").tap{|a| a.shift }.last || "")
end
Output:
First name tests
nil => ""
"" => ""
"First" => "First"
"First Last" => "First"
"First Middle Last" => "First Middle"
"First Middle Middle2 Last" => "First Middle Middle2"
Last name tests
nil => ""
"" => ""
"First" => ""
"First Last" => "Last"
"First Middle Last" => "Last"
"First Middle Middle2 Last" => "Last"
Related
So I've been over Traiblazer and Reform documentation and I often see this kind of code
class AlbumForm < Reform::Form
collection :songs, populate_if_empty: :populate_songs! do
property :name
end
def populate_songs!(fragment:, **)
Song.find_by(name: fragment["name"]) or Song.new
end
end
Notice the def populate_songs!(fragment:, **) definition?
I'm well aware of double splat named arguments (like **others) that capture all other keyword arguments. But I've never seen ** alone, without a name.
So my 2 questions are:
what does ** mean in the block above?
why use this syntax?
what does ** mean in the block above?
It's a kwsplat, but it's not assigned a name. So this method will accept arbitrary set of keyword arguments and ignore all but :fragment.
why use this syntax?
To ignore arguments you're not interested in.
A little demo
class Person
attr_reader :name, :age
def initialize(name:, age:)
#name = name
#age = age
end
def description
"name: #{name}, age: #{age}"
end
end
class Rapper < Person
def initialize(name:, **)
name = "Lil #{name}" # amend one argument
super # send name and the rest (however many there are) to super
end
end
Person.new(name: 'John', age: 25).description # => "name: John, age: 25"
Rapper.new(name: 'John', age: 25).description # => "name: Lil John, age: 25"
I want to generate full names from title, first_name, middle_name, and last_name. e.g. Mr Billy Bob Thornton. But the title and middle_name are optional and I don't want to get any leading or double spaces. I've come up with lots of ways of doing them but none of them seem really elegant. Here are my techniques so far
full_name = "#{title} #{first_name} #{middle_name} #{last_name}"
#FAIL. Leading and double spaces result
full_name = "#{title} #{first_name} #{middle_name} #{last_name}".gsub(/^ /,'').gsub(/ /,' ')
#Works but all that regex tacked on the end is very ugly
full_name = "#{title}#{title.nil? || title.empty? ? '' : ' '}#{first_name} #{middle_name}#{middle_name.nil? || middle_name.empty? ? '' : ' '}#{last_name}"
#Works but goes on forever
I'll be using rails so can reduce my .nil? || .empty? to .blank? to make the last one a little more concise but I can't help but think that there's a nicer way.
You're using Rails so you have access to String#squish:
squish()
Returns the string, first removing all whitespace on both ends of the string, and then changing remaining consecutive whitespace groups into one space each.
So you could do this:
name = "#{title} #{first_name} #{middle_name} #{last_name}".squish
Would you be looking for something like this ?
[title, first_name, middle_name, last_name].compact.reject(&:empty?).join(' ')
with Rails
[title, first_name, middle_name, last_name].select(&:present?).join(' ')
Check out Ruby's String#squeeze and String#strip
>> title=""
=> ""
>> first_name="Alice"
=> "Alice"
>> middle_name=""
=> ""
>> last_name=""
=> ""
>> full_name = "#{title} #{first_name} #{middle_name} #{last_name}".squeeze(" ").strip
=> "Alice"
>> last_name="Wu"
=> "Wu"
>> full_name = "#{title} #{first_name} #{middle_name} #{last_name}".squeeze(" ").strip
=> "Alice Wu"
Generalizing to a function should be pretty easy.
[title, first_name, middle_name, last_name].select({|v| v.present?}).join(' ')
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?
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
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
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"
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"
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 ;)
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.
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
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.
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"
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?
I'm currently using the following to parse emails:
def parse_emails(emails)
valid_emails, invalid_emails = [], []
unless emails.nil?
emails.split(/, ?/).each do |full_email|
unless full_email.blank?
if full_email.index(/\<.+\>/)
email = full_email.match(/\<.*\>/)[0].gsub(/[\<\>]/, "").strip
else
email = full_email.strip
end
email = email.delete("<").delete(">")
email_address = EmailVeracity::Address.new(email)
if email_address.valid?
valid_emails << email
else
invalid_emails << email
end
end
end
end
return valid_emails, invalid_emails
end
The problem I'm having is given an email like:
Bob Smith <bob#smith.com>
The code above is delete Bob Smith and only returning bob#smith.
But what I want is an hash of FNAME, LNAME, EMAIL. Where fname and lname are optional but email is not.
What type of ruby object would I use for that and how would I create such a record in the code above?
Thanks
I've coded so that it will work even if you have an entry like: John Bob Smith Doe <bob#smith.com>
It would retrieve:
{:email => "bob#smith.com", :fname => "John", :lname => "Bob Smith Doe" }
def parse_emails(emails)
valid_emails, invalid_emails = [], []
unless emails.nil?
emails.split(/, ?/).each do |full_email|
unless full_email.blank?
if index = full_email.index(/\<.+\>/)
email = full_email.match(/\<.*\>/)[0].gsub(/[\<\>]/, "").strip
name = full_email[0..index-1].split(" ")
fname = name.first
lname = name[1..name.size] * " "
else
email = full_email.strip
#your choice, what the string could be... only mail, only name?
end
email = email.delete("<").delete(">")
email_address = EmailVeracity::Address.new(email)
if email_address.valid?
valid_emails << { :email => email, :lname => lname, :fname => fname}
else
invalid_emails << { :email => email, :lname => lname, :fname => fname}
end
end
end
end
return valid_emails, invalid_emails
end
Here's a slightly different approach that works better for me. It grabs the name whether it is before or after the email address and whether or not the email address is in angle brackets.
I don't try to parse the first name out from the last name -- too problematic (e.g. "Mary Ann Smith" or Dr. Mary Smith"), but I do eliminate duplicate email addresses.
def parse_list(list)
r = Regexp.new('[a-z0-9\.\_\%\+\-]+#[a-z0-9\.\-]+\.[a-z]{2,4}', true)
valid_items, invalid_items = {}, []
## split the list on commas and/or newlines
list_items = list.split(/[,\n]+/)
list_items.each do |item|
if m = r.match(item)
## get the email address
email = m[0]
## get everything before the email address
before_str = item[0, m.begin(0)]
## get everything after the email address
after_str = item[m.end(0), item.length]
## enter the email as a valid_items hash key (eliminating dups)
## make the value of that key anything before the email if it contains
## any alphnumerics, stripping out any angle brackets
## and leading/trailing space
if /\w/ =~ before_str
valid_items[email] = before_str.gsub(/[\<\>\"]+/, '').strip
## if nothing before the email, make the value of that key anything after
##the email, stripping out any angle brackets and leading/trailing space
elsif /\w/ =~ after_str
valid_items[email] = after_str.gsub(/[\<\>\"]+/, '').strip
## if nothing after the email either,
## make the value of that key an empty string
else
valid_items[email] = ''
end
else
invalid_items << item.strip if item.strip.length > 0
end
end
[valid_items, invalid_items]
end
It returns a hash with valid email addresses as keys and the associated names as values. Any invalid items are returned in the invalid_items array.
See http://www.regular-expressions.info/email.html for an interesting discussion of email regexes.
I made a little gem out of this in case it might be useful to someone at https://github.com/victorgrey/email_addresses_parser
You can use rfc822 gem. It contains regular expression for seeking for emails that conform with RFC. You can easily extend it with parts for finding first and last name.
Along the lines of mspanc's answer, you can use the mail gem to do the basic email address parsing work for you, as answered here: https://stackoverflow.com/a/12187502/1019504
I'm fairly new to rails, working on a Rails 3 app with a Profile model for users.
In the profile Model I'd like to have a "name" entry, and I'd like to be able to access logical variations of it using simple syntax like:
user.profile.name = "John Doe"
user.profile.name.first = "John"
user.profile.name.last = "Doe"
Is this possible, or do I need to stick with "first_name" and "last_name" as my fields in this model?
It's possible, but I wouldn't recommend it.
I would just stick with first_name and last_name if I were you and add a method fullname:
def fullname
"#{first_name} #{last_name}"
end
Edit:
If you really do want user.profile.name, you could create a Name model like this:
class Name < ActiveRecord::Base
belongs_to :profile
def to_s
"#{first} #{last}"
end
end
This allows you to do:
user.profile.name.to_s # John Doe
user.profile.name.first # John
user.profile.name.last # Doe
The other answers are all correct, in so far as they ignore the #composed_of aggregator:
class Name
attr_reader :first, :last
def initialize(first_name, last_name)
#first, #last = first_name, last_name
end
def full_name
[#first, #last].reject(&:blank?).join(" ")
end
def to_s
full_name
end
end
class Profile < ActiveRecord::Base
composed_of :name, :mapping => %w(first_name last_name)
end
# Rails console prompt
> profile = Profile.new(:name => Name.new("Francois", "Beausoleil"))
> profile.save!
> profile = Profile.find_by_first_name("Francois")
> profile.name.first
"Francois"
As noted on the #composed_of page, you must assign a new instance of the aggregator: you cannot just replace values within the aggregator. The aggregator class acts as a Value, just like a simple string or number.
I also sent a response yesterday with a very similar answer: How best to associate an Address to multiple models in rails?
As Capt. Tokyo said that's a horrible idea but here's how you would do it:
rails g model User full_name:hash
Then you would store data in it like so:
user = User.new
user.full_name = {:first => "Forrest", :last => "Gump"}
Now your problems begin.
To search the field requires both names and you can't do a partial search like searching for all people with the same last name. Worst of all you can store anything in the field! So imagine another programmer mistypes one of the field names so for a week you have {:fist => "Name", :last => "Last"} being inserted into the database! Noooooooooooooooooo!
If you used proper field names you could do this:
user = User.new(:first_name => "First", :last_name => "Last")
Easy to read and no need for hashes. Now that you know how to do it the wrong way, do it the right way. :)
FYI (assume you have a field fullname. ie your profile.name = "John Doe")
class Profile
def name
#splited_name ||= fullname.split # #splited_name would cache the result so that no need to split the fullname every time
end
end
Now, you could do something like this:
user.profile.fullname # "John Doe"
user.profile.name.first # "John"
user.profile.name.last # "Doe"
Note the following case:
user.profile.fullname = "John Ronald Doe"
user.profile.name.first # "John"
user.profile.name.second # "Ronald"
user.profile.name.last # "Doe"
I agree with captaintokyo. You won't miss out the middle names.
Also this method assume no Chinese, Japanese names are input. It's because those names contain no spaces in between first name and last name normally.