nameable_record 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+