Ruby/Rails Parsing Emails - ruby-on-rails

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

Related

Ruby Loop through some attributes of an object

In Rails, I have a class name User, in which I just want to look at :name, :address, :age
I would like to write a piece of code that's something like:
user = User.new
[name, address, age].zip(["Name", "Address", 10]).each do |attribute, val|
user.attribute = val
end
The thing is I don't know how to do it properly, since user.attribute is obviously not a valid line. In other word, is there anyway so that user.attribute gets evaluated as user.name, user.address, user.age depends on the loop?
You should use send method
user.send "#{attribute}=", val
If attribute is, say, :name, then the line above is equivalent to
user.name = val
Actually I can do it this way:
[name, address, age].zip(["Name", "Address", 10]).each do |attribute, val|
user[attribute] = val
end
This way works, too
user.send(:name) would be the same as calling user.name, so you may want to try that.
user = User.new
[name, address, age].zip(["Name", "Address", 10]).each do |attribute, val|
user.write_attribute(attribute, val)
end
But that's what I'd write:
user = User.new
user.attributes = {name => "Name", address => "Address", age => 10}

creating name helper, to split first and last names apart

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"

Rails 3 custom formatted validation errors?

With this model:
validates_presence_of :email, :message => "We need your email address"
as a rather contrived example. The error comes out as:
Email We need your email address
How can I provide the format myself?
I looked at the source code of ActiveModel::Errors#full_messages and it does this:
def full_messages
full_messages = []
each do |attribute, messages|
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
full_messages
end
Notice the :default format string in the options? So I tried:
validates_presence_of :email, :message => "We need your email address", :default => "something"
But then the error message actually appears as:
Email something
So then I tried including the interpolation string %{message}, thus overriding the %{attribute} %{message} version Rails uses by default. This causes an Exception:
I18n::MissingInterpolationArgument in SubscriptionsController#create
missing interpolation argument in "%{message}" ({:model=>"Subscription", :attribute=>"Email", :value=>""} given
Yet if I use the interpolation string %{attribute}, it doesn't error, it just spits out the humanized attribute name twice.
Anyone got any experience with this? I could always have the attribute name first, but quite often we need some other string (marketing guys always make things more complicated!).
Errors on :base are not specific to any attribute, so the humanized attribute name is not appended to the message. This allows us to add error messages about email, but not attach them to the email attribute, and get the intended result:
class User < ActiveRecord::Base
validate :email_with_custom_message
...
private
def email_with_custom_message
errors.add(:base, "We need your email address") if
email.blank?
end
end
Using internationalization for this is probably your best bet. Take a look at
http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
Particularly this section:
5.1.2 Error Message Interpolation
The translated model name, translated
attribute name, and value are always
available for interpolation.
So, for example, instead of the
default error message "can not be
blank" you could use the attribute
name like this : "Please fill in your
%{attribute}"

Rails Model: Name -- First, Last

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.

How do you validate the presence of one field from many

I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""

Resources