nameable_record 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,31 +1,28 @@
1
1
  module NameableRecord
2
+
2
3
  class Name
4
+
3
5
  attr_reader :first, :middle, :last, :prefix, :suffix
4
6
 
5
- def initialize( last, first, prefix=nil, middle=nil, suffix=nil )
6
- @first, @middle, @last, @prefix, @suffix = first, middle, last, prefix, suffix
7
-
8
- @name_pattern_map = {
9
- /%l/ => last || "",
10
- /%f/ => first || "",
11
- /%m/ => middle || "",
12
- /%p/ => prefix || "",
13
- /%s/ => suffix || "",
14
- }
15
-
16
- @@predefined_patterns = {
17
- :full => " %l, %f %s",
18
- :full_with_middle => " %l, %f %m %s",
19
- :full_with_prefix => " %l, %p %f %s",
20
- :full_sentence => "%p %f %l %s",
21
- :full_sentence_with_middle => "%p %f %m %l %s"
22
- }
7
+ def initialize( *args )
8
+ @last, @first, @prefix, @middle, @suffix = args
23
9
 
24
10
  self.freeze
25
11
  end
26
12
 
27
- # Creates a name based on pattern provided. Defaults to given + additional
28
- # given + family names concatentated.
13
+ def ==( another_name )
14
+ return false unless another_name.is_a?( self.class )
15
+
16
+ %w(last first middle suffix prefix).map do |attr|
17
+ another_name.send( attr ) == send( attr )
18
+ end.all? { |r| r == true }
19
+ end
20
+
21
+ def eql?( another_name )
22
+ self == another_name
23
+ end
24
+
25
+ # Creates a name based on pattern provided. Defaults to last, first.
29
26
  #
30
27
  # Symbols:
31
28
  # %l - last name
@@ -34,19 +31,46 @@ module NameableRecord
34
31
  # %p - prefix
35
32
  # %s - suffix
36
33
  #
37
- def to_s( pattern="" )
38
- if pattern.is_a?( Symbol )
39
- to_return = @@predefined_patterns[pattern]
40
- else
41
- to_return = pattern
42
- end
43
- to_return = @@predefined_patterns[:full] if to_return.empty?
34
+ def to_s( pattern='%l, %f' )
35
+ pattern = PREDEFINED_PATTERNS[pattern] if pattern.is_a?( Symbol )
44
36
 
45
- @name_pattern_map.each do |pat, replacement|
46
- to_return = to_return.gsub( pat, replacement )
37
+ PATTERN_MAP.inject( pattern ) do |name, mapping|
38
+ name = name.gsub( mapping.first,
39
+ (send( mapping.last ) || '') )
47
40
  end
48
-
49
- to_return.strip
50
41
  end
42
+
43
+ PREDEFINED_PATTERNS = {
44
+ :full => "%l, %f %s",
45
+ :full_with_middle => "%l, %f %m %s",
46
+ :full_with_prefix => "%l, %p %f %s",
47
+ :full_sentence => "%p %f %l %s",
48
+ :full_sentence_with_middle => "%p %f %m %l %s"
49
+ }.freeze
50
+
51
+ PATTERN_MAP = {
52
+ /%l/ => :last,
53
+ /%f/ => :first,
54
+ /%m/ => :middle,
55
+ /%p/ => :prefix,
56
+ /%s/ => :suffix
57
+ }.freeze
58
+
59
+ PREFIX_BASE = %w(Mr Mrs Miss Dr General) # The order of this matters because of PREFIXES_CORRECTIONS
60
+ SUFFIX_BASE = %w(Jr III V IV Esq) # The order of this matters because of SUFFIXES_CORRECTIONS
61
+
62
+ PREFIXES = PREFIX_BASE.map { |p| [p, "#{p}.", p.upcase, "#{p.upcase}.", p.downcase, "#{p.downcase}."] }.flatten.sort
63
+ SUFFIXES = SUFFIX_BASE.map { |p| [p, "#{p}.", p.upcase, "#{p.upcase}.", p.downcase, "#{p.downcase}."] }.flatten.sort
64
+
65
+ PREFIXES_CORRECTIONS = Hash[*PREFIX_BASE.map do |base|
66
+ PREFIXES.grep( /#{base}/i ).map { |p| [p,base] }
67
+ end.flatten]
68
+
69
+ SUFFIXES_CORRECTIONS = Hash[*SUFFIX_BASE.map do |base|
70
+ SUFFIXES.grep( /#{base}/i ).map { |p| [p,base] }
71
+ end.flatten]
72
+
51
73
  end
52
- end
74
+
75
+ end
76
+
@@ -0,0 +1,119 @@
1
+ module NameableRecord::NameRecognition
2
+
3
+ def surnames
4
+ @surnames ||= surnames_from_file
5
+ end
6
+
7
+ def given_names
8
+ @given_names ||= given_names_from_file
9
+ end
10
+
11
+ %w(downcase upcase).each do |desired_case|
12
+ define_method "surnames_all_#{desired_case}" do
13
+ surnames.map( &desired_case.to_sym )
14
+ end
15
+
16
+ define_method "given_names_all_#{desired_case}" do
17
+ given_names.map( &desired_case.to_sym )
18
+ end
19
+
20
+ define_method "all_name_words_#{desired_case}" do
21
+ (surnames + given_names + prefixes + suffixes + initials_downcase).map { |n| n.send( desired_case ) }.uniq
22
+ end
23
+ end
24
+
25
+ def human_name?( name, lowest_affirmative_probabilty=100, disqualifying_words=[] )
26
+ probability_is_human_name( name, disqualifying_words ) >= lowest_affirmative_probabilty
27
+ end
28
+
29
+ def probability_is_human_name( name, disqualifying_words=[] )
30
+ if name.match( /^[a-zA-Z]*\s*[a-zA-Z]{2}$/ )
31
+ return probability_from_last_and_intials( name, :last_name_first => true )
32
+ elsif name.match( /^[a-zA-Z]{2}\s*[a-zA-Z]*$/ )
33
+ return probability_from_last_and_intials( name, :last_name_first => false )
34
+ end
35
+
36
+ name_parts = split_and_clean_name( name )
37
+
38
+ return 0 if ((default_disqualifying_words + disqualifying_words) & name_parts).size > 0
39
+
40
+ score = (100 - ((name_parts.size - (name_parts & all_name_words_downcase).size) * points_per_additional_word))
41
+ score < 0 ? 0 : score
42
+ end
43
+
44
+ def default_disqualifying_words
45
+ %w(
46
+ corporation
47
+ corp
48
+ co
49
+ llc
50
+ ltd
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ def points_per_additional_word
57
+ 20
58
+ end
59
+
60
+ def probability_from_last_and_intials( name, options )
61
+ name_parts = split_and_clean_name( name )
62
+
63
+ last_name = options[:last_name_first] ? name_parts.first : name_parts.last
64
+
65
+ return ([last_name] & surnames_all_downcase).size == 1 ?
66
+ 100 :
67
+ 50
68
+ end
69
+
70
+ def split_and_clean_name( name )
71
+ name.split( ' ' ).compact.map { |p| p.strip.downcase }
72
+ end
73
+
74
+ def prefixes
75
+ NameableRecord::Name::PREFIXES
76
+ end
77
+
78
+ def suffixes
79
+ NameableRecord::Name::SUFFIXES
80
+ end
81
+
82
+ def initials_downcase
83
+ alphabet_downcase + alphabet_downcase.map { |l| "#{l}." }
84
+ end
85
+
86
+ def alphabet_downcase
87
+ ('a'..'z').to_a
88
+ end
89
+
90
+ def surnames_from_file
91
+ File.open( surnames_file_path, 'r' ) { |f| f.readlines }.map { |n| n.strip }
92
+ end
93
+
94
+ def surnames_file_path
95
+ File.join File.dirname(__FILE__),
96
+ 'data',
97
+ surnames_file_name
98
+ end
99
+
100
+ def surnames_file_name
101
+ 'surnames.txt'
102
+ end
103
+
104
+ def given_names_from_file
105
+ File.open( given_names_file_path, 'r' ) { |f| f.readlines }.map { |n| n.strip }
106
+ end
107
+
108
+ def given_names_file_path
109
+ File.join File.dirname(__FILE__),
110
+ 'data',
111
+ given_names_file_name
112
+ end
113
+
114
+ def given_names_file_name
115
+ 'given-names.txt'
116
+ end
117
+
118
+ end
119
+
@@ -0,0 +1,10 @@
1
+ module NameableRecord
2
+
3
+ class NameRecognizer
4
+
5
+ include NameableRecord::NameRecognition
6
+
7
+ end
8
+
9
+ end
10
+
@@ -0,0 +1,14 @@
1
+ require 'rails'
2
+
3
+ module NameableRecord
4
+
5
+ class Railtie < ::Rails::Railtie
6
+
7
+ initializer "nameable_record.initialize" do
8
+ ActiveRecord::Base.send( :include, NameableRecord::ActiveRecordExtensions ) if defined?( ActiveRecord::Base )
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+
@@ -0,0 +1,4 @@
1
+ module NameableRecord
2
+ VERSION = "1.0.0"
3
+ end
4
+
@@ -1,10 +1,12 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
- require 'nameable_record/active_record_extensions'
4
- require 'nameable_record/name'
1
+ require "nameable_record/railtie" if defined?( ::Rails )
2
+ require "nameable_record/version"
5
3
 
6
4
  module NameableRecord
7
- VERSION = '0.1.0'
5
+
6
+ autoload :ActiveRecordExtensions, 'nameable_record/active_record_extensions'
7
+ autoload :Name, 'nameable_record/name'
8
+ autoload :NameRecognition, 'nameable_record/name_recognition'
9
+ autoload :NameRecognizer, 'nameable_record/name_recognizer'
10
+
8
11
  end
9
12
 
10
- ActiveRecord::Base.send( :include, NameableRecord::ActiveRecordExtensions ) if defined?( ActiveRecord::Base )
@@ -1,57 +1,32 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "nameable_record/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{nameable_record}
8
- s.version = "0.2.0"
6
+ s.name = "nameable_record"
7
+ s.version = NameableRecord::VERSION
8
+ s.authors = ["Jason Harrelson"]
9
+ s.email = ["jason@lookforwardenterprises.com"]
10
+ s.homepage = "https://github.com/midas/nameable_record"
11
+ s.summary = %q{Abstracts the ActiveRecord composed_of pattern for names.}
12
+ s.description = %q{Abstracts the ActiveRecord composed_of pattern for names. Also provides other convenience utilieis for working with human names.}
9
13
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["C. Jason Harrelson"]
12
- s.date = %q{2010-10-02}
13
- s.description = %q{Encapsulates the composed of pattern for names into any easy to use library.}
14
- s.email = %q{jason@lookforwardenterprises.com}
15
- s.extra_rdoc_files = [
16
- "LICENSE",
17
- "README.rdoc"
18
- ]
19
- s.files = [
20
- ".document",
21
- ".gitignore",
22
- "LICENSE",
23
- "README.rdoc",
24
- "Rakefile",
25
- "VERSION",
26
- "lib/nameable_record.rb",
27
- "lib/nameable_record/active_record_extensions.rb",
28
- "lib/nameable_record/name.rb",
29
- "nameable_record.gemspec",
30
- "spec/nameable_record_spec.rb",
31
- "spec/spec.opts",
32
- "spec/spec_helper.rb"
33
- ]
34
- s.homepage = %q{http://github.com/midas/nameable_record}
35
- s.rdoc_options = ["--charset=UTF-8"]
36
- s.require_paths = ["lib"]
37
- s.rubygems_version = %q{1.3.7}
38
- s.summary = %q{Encapsulates the composed of pattern for names into any easy to use library.}
39
- s.test_files = [
40
- "spec/nameable_record_spec.rb",
41
- "spec/spec_helper.rb"
42
- ]
14
+ s.rubyforge_project = "nameable_record"
43
15
 
44
- if s.respond_to? :specification_version then
45
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
46
- s.specification_version = 3
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
47
20
 
48
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
- s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
50
- else
51
- s.add_dependency(%q<rspec>, [">= 1.2.9"])
52
- end
53
- else
54
- s.add_dependency(%q<rspec>, [">= 1.2.9"])
21
+ # specify any dependencies here; for example:
22
+ %w(
23
+ gem-dandy
24
+ rspec
25
+ rails
26
+ ).each do |development_dependency|
27
+ s.add_development_dependency development_dependency
55
28
  end
29
+
30
+ # s.add_runtime_dependency "rest-client"
56
31
  end
57
32
 
@@ -0,0 +1,17 @@
1
+ ActiveRecord::Base.configurations = YAML::load( IO.read( File.dirname(__FILE__) + '/../spec/database.yml' ) )
2
+ ActiveRecord::Base.establish_connection( 'test' )
3
+
4
+ silence_stream STDOUT do
5
+
6
+ ActiveRecord::Schema.define :version => 1 do
7
+ create_table :users, :force => true do |t|
8
+ t.string :name_first, :anem_last, :name_middle, :name_prefix, :name_suffix
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ class User < ActiveRecord::Base
15
+ has_name :name
16
+ end
17
+
data/spec/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ :adapter: sqlite3
3
+ :database: ":memory:"
4
+
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'rails_spec_helper'
3
+
4
+ describe NameableRecord::ActiveRecordExtensions do
5
+
6
+ let :user do
7
+ User.new
8
+ end
9
+
10
+ let :name do
11
+ NameableRecord::Name.new *name_parts
12
+ end
13
+
14
+ context 'setting the composed of fields from a NameableRecord::Name instance' do
15
+
16
+ before :each do
17
+ user.name = name
18
+ end
19
+
20
+ it '#name_last' do
21
+ user.name_last.should == 'Smith'
22
+ end
23
+
24
+ it '#name_first' do
25
+ user.name_first.should == 'John'
26
+ end
27
+
28
+ it '#name_middle' do
29
+ user.name_middle.should == 'Jacob'
30
+ end
31
+
32
+ it '#name_prefix' do
33
+ user.name_prefix.should == 'Mr.'
34
+ end
35
+
36
+ it '#name_suffix' do
37
+ user.name_suffix.should == 'III'
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'any name recognition' do
4
+
5
+ context '#surnames' do
6
+
7
+ subject { recognition_instance.surnames.size }
8
+
9
+ it { should == 9527 }
10
+
11
+ end
12
+
13
+ context '#surnames_all_downcase' do
14
+
15
+ subject { recognition_instance.surnames_all_downcase.first }
16
+
17
+ it { should == 'aaron' }
18
+
19
+ end
20
+
21
+ context '#surnames_all_upcase' do
22
+
23
+ subject { recognition_instance.surnames_all_upcase.first }
24
+
25
+ it { should == 'AARON' }
26
+
27
+ end
28
+
29
+ context '#human_name?' do
30
+
31
+ context 'when the name matches the pattern {last name} {first initial}{middle initial}' do
32
+
33
+ subject { recognition_instance.human_name?( 'HOMER SA' ) }
34
+
35
+ it { should be_true }
36
+
37
+ end
38
+
39
+ context 'when the name matches the pattern {first initial}{middle initial} {last name}' do
40
+
41
+ subject { recognition_instance.human_name?( 'SA HOMER' ) }
42
+
43
+ it { should be_true }
44
+
45
+ end
46
+
47
+ context 'when the middle initial has no .' do
48
+
49
+ subject { recognition_instance.human_name?( 'HOMER STEPHEN A' ) }
50
+
51
+ it { should be_true }
52
+
53
+ end
54
+
55
+ context 'when the middle initial has a .' do
56
+
57
+ subject { recognition_instance.human_name?( 'HOMER STEPHEN A.' ) }
58
+
59
+ it { should be_true }
60
+
61
+ end
62
+
63
+ context 'when the name is an 80% match' do
64
+
65
+ subject { recognition_instance.human_name?( 'CUP STEPHEN A' ) }
66
+
67
+ it { should be_false }
68
+
69
+ end
70
+
71
+ context 'when the name is less an 80% match with a 70% threshold' do
72
+
73
+ subject { recognition_instance.human_name?( 'CUP STEPHEN A', 80 ) }
74
+
75
+ it { should be_true }
76
+
77
+ end
78
+
79
+ end
80
+
81
+ context '#given_names' do
82
+
83
+ subject { recognition_instance.given_names.size }
84
+
85
+ it { should == 5678 }
86
+
87
+ end
88
+
89
+ context '#given_names_all_downcase' do
90
+
91
+ subject { recognition_instance.given_names_all_downcase.first }
92
+
93
+ it { should == 'aj' }
94
+
95
+ end
96
+
97
+ context '#given_names_all_upcase' do
98
+
99
+ subject { recognition_instance.given_names_all_upcase.first }
100
+
101
+ it { should == 'AJ' }
102
+
103
+ end
104
+
105
+ context '#all_name_words_downcase' do
106
+
107
+ subject { recognition_instance.all_name_words_downcase.size }
108
+
109
+ it { should == 14297 }
110
+
111
+ end
112
+
113
+ context '#all_name_words_upcase' do
114
+
115
+ subject { recognition_instance.all_name_words_upcase.size }
116
+
117
+ it { should == 14297 }
118
+
119
+ end
120
+
121
+ context '#prefixes' do
122
+
123
+ subject { recognition_instance.send( :prefixes ).sort }
124
+
125
+ it { should == ["DR", "DR.", "Dr", "Dr.", "GENERAL", "GENERAL.", "General", "General.", "MISS", "MISS.", "MR", "MR.", "MRS", "MRS.", "Miss", "Miss.", "Mr", "Mr.", "Mrs", "Mrs.", "dr", "dr.", "general", "general.", "miss", "miss.", "mr", "mr.", "mrs", "mrs."] }
126
+
127
+ end
128
+
129
+ context '#suffixes' do
130
+
131
+ subject { recognition_instance.send( :suffixes ).sort }
132
+
133
+ it { should == ["ESQ", "ESQ.", "Esq", "Esq.", "III", "III", "III.", "III.", "IV", "IV", "IV.", "IV.", "JR", "JR.", "Jr", "Jr.", "V", "V", "V.", "V.", "esq", "esq.", "iii", "iii.", "iv", "iv.", "jr", "jr.", "v", "v."] }
134
+
135
+ end
136
+
137
+ context '#probability_is_human_name' do
138
+
139
+ context 'when all words are a name part' do
140
+
141
+ subject { recognition_instance.probability_is_human_name( 'Mr. Charles Langford III' ) }
142
+
143
+ it { should == 100 }
144
+
145
+ end
146
+
147
+ context 'when there is one word that is not a name part' do
148
+
149
+ subject { recognition_instance.probability_is_human_name( 'Introducing Mr. Charles Langford III' ) }
150
+
151
+ it { should == 80 }
152
+
153
+ end
154
+
155
+ context 'when there are two words that are not a name part' do
156
+
157
+ subject { recognition_instance.probability_is_human_name( 'Home of Mr. Charles Langford III' ) }
158
+
159
+ it { should == 60 }
160
+
161
+ end
162
+
163
+ context 'when there are five words that are not a name part' do
164
+
165
+ subject { recognition_instance.probability_is_human_name( 'This is the home of Mr. Charles Langford III' ) }
166
+
167
+ it { should == 0 }
168
+
169
+ end
170
+
171
+ context 'when there are six words that are not a name part' do
172
+
173
+ subject { recognition_instance.probability_is_human_name( 'It is at the home of Mr. Charles Langford III' ) }
174
+
175
+ it { should == 0 }
176
+
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require File.expand_path( "#{File.dirname __FILE__}/name_recognition_sharedspec" )
3
+
4
+ describe NameableRecord::NameRecognition do
5
+
6
+ class SomeClass
7
+ include NameableRecord::NameRecognition
8
+ end
9
+
10
+ let :recognition_instance do
11
+ SomeClass.new
12
+ end
13
+
14
+ it_should_behave_like 'any name recognition'
15
+
16
+ end
17
+
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require File.expand_path( "#{File.dirname __FILE__}/name_recognition_sharedspec" )
3
+
4
+ describe NameableRecord::NameRecognizer do
5
+
6
+ let :recognition_instance do
7
+ described_class.new
8
+ end
9
+
10
+ it 'should include the NameRecognition module' do
11
+ described_class.should include NameableRecord::NameRecognition
12
+ end
13
+
14
+ it_should_behave_like 'any name recognition'
15
+
16
+ end
17
+