name_tamer 0.6.1 → 1.0.2

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.
@@ -3,7 +3,7 @@
3
3
  module NameTamer
4
4
  NONBREAKING_SPACE = "\u00a0"
5
5
  ASCII_SPACE = ' '
6
- ADFIX_JOINERS = "[#{ASCII_SPACE}-]"
6
+ ADFIX_JOINERS = "[#{ASCII_SPACE}-]".freeze
7
7
  SLUG_DELIMITER = '-'
8
8
  ZERO_WIDTH_FILTER = /[\u180E\u200B\u200C\u200D\u2063\uFEFF]/
9
9
 
@@ -39,9 +39,9 @@ module NameTamer
39
39
  ALPHA = 'A-Za-z'
40
40
  DIGIT = '0-9'
41
41
  UCSCHAR = '\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'
42
- IUNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~#{UCSCHAR}"
42
+ IUNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~#{UCSCHAR}".freeze
43
43
  SUBDELIMS = '!$&\'\(\)\*+,;='
44
- ISEGMENT_NZ_NC = "#{IUNRESERVED}#{SUBDELIMS}@" # pct-encoded not needed
44
+ ISEGMENT_NZ_NC = "#{IUNRESERVED}#{SUBDELIMS}@".freeze # pct-encoded not needed
45
45
  FILTER_RFC3987 = /[^#{ISEGMENT_NZ_NC}]/
46
46
  FILTER_COMPAT = /[^#{ALPHA}#{DIGIT}\-_#{UCSCHAR}]/
47
47
 
@@ -49,54 +49,14 @@ module NameTamer
49
49
  # If you add to the list, you can use spaces and dots where appropriate
50
50
  # Ensure any single letters are followed by a dot because we'll add one to the string
51
51
  # during processing, e.g. "y Cia." should be "y. Cia."
52
+
53
+ private_class_method def self.get_constants_from(filename)
54
+ File.readlines(Pathname.new(__dir__).join('constants', filename), chomp: true).reject(&:empty?)
55
+ end
56
+
52
57
  ADFIXES = {
53
58
  prefix: {
54
- person: [
55
- 'Baroness',
56
- 'Capt.',
57
- 'Captain',
58
- 'Col.',
59
- 'Colonel',
60
- 'Dame',
61
- 'Doctor',
62
- 'Doktor',
63
- 'Dr.',
64
- 'Frau',
65
- 'Herr',
66
- 'Judge',
67
- 'Justice',
68
- 'Lady',
69
- 'Lieut.',
70
- 'Lieutenant',
71
- 'Lord',
72
- 'Madame',
73
- 'Major',
74
- 'Master',
75
- 'Matron',
76
- 'Messrs.',
77
- 'Mgr.',
78
- 'Miss',
79
- 'Mister',
80
- 'Mlle.',
81
- 'Mme.',
82
- 'Mons.',
83
- 'Mr. & Mrs.',
84
- 'Mr. and Mrs.',
85
- 'Mr.',
86
- 'Mrs.',
87
- 'Ms.',
88
- 'Msgr.',
89
- 'Prof.',
90
- 'Professor',
91
- 'Rev.',
92
- 'Reverend',
93
- 'Sir',
94
- 'Sister',
95
- 'The Hon.',
96
- 'The Lady.',
97
- 'The Lord',
98
- 'The Rt. Hon.',
99
- ],
59
+ person: get_constants_from('adfixes_prefix_person'),
100
60
  organization: [
101
61
  'Fa.',
102
62
  'P.T. Tbk.',
@@ -106,369 +66,24 @@ module NameTamer
106
66
  before: '\\A', after: ADFIX_JOINERS
107
67
  },
108
68
  suffix: {
109
- person: [
110
- 'A.A.M.S.',
111
- 'A.C.A.',
112
- 'A.C.C.',
113
- 'A.C.C.A.',
114
- 'A.C.M.A.',
115
- 'A.E.P.',
116
- 'A.I.F.',
117
- 'A.I.F.A.',
118
- 'A.S.A.',
119
- 'A.W.M.A.',
120
- 'B.A.',
121
- 'B.Ed.',
122
- 'B.Eng.',
123
- 'B.Sc.',
124
- 'B.Tech.',
125
- 'C.A.',
126
- 'C.A.I.A.',
127
- 'C.A.P.M.',
128
- 'C.B.V.',
129
- 'C.C.I.M.',
130
- 'C.D.F.A.',
131
- 'C.E.M.',
132
- 'C.E.P.P.',
133
- 'C.Eng.',
134
- 'C.F.A.',
135
- 'C.F.B.S.',
136
- 'C.F.F.',
137
- 'C.F.P.',
138
- 'C.F.S.',
139
- 'C.G.A.',
140
- 'C.G.B.',
141
- 'C.G.M.A.',
142
- 'C.G.P.',
143
- 'C.I.M.',
144
- 'C.I.S.S.P.',
145
- 'C.I.T.P.',
146
- 'C.L.P.',
147
- 'C.L.T.C.',
148
- 'C.L.U.',
149
- 'C.M.A.',
150
- 'C.M.T.',
151
- 'C.P.A.',
152
- 'C.P.C.C.',
153
- 'C.R.P.C.',
154
- 'C.R.P.S.',
155
- 'C.S.O.X.',
156
- 'C.S.S.D.',
157
- 'C.T.A.',
158
- 'C.W.S.',
159
- 'Cantab.',
160
- 'Ch.F.C.',
161
- 'Chartered F.C.S.I.',
162
- 'Chartered M.C.S.I.',
163
- 'D.B.E.',
164
- 'D.D.S.',
165
- 'D.Phil.',
166
- 'D.V.M.',
167
- 'Dip. D.M.',
168
- 'E.A.',
169
- 'E.R.P.',
170
- 'Ed.D.',
171
- 'Ed.M.',
172
- 'Eng.D.',
173
- 'Esq.',
174
- 'F.B.C.S.',
175
- 'F.C.A.',
176
- 'F.C.C.A.',
177
- 'F.C.I.P.S.',
178
- 'F.C.M.I.',
179
- 'F.C.S.I.',
180
- 'F.I.E.T.',
181
- 'F.I.R.P.',
182
- 'F.Inst.L.M.',
183
- 'F.P.C.',
184
- 'F.R.M.',
185
- 'F.R.M.',
186
- 'G.S.P.',
187
- 'Hons.',
188
- 'I.F.R.S. Certified',
189
- 'I.T.I.L. v3',
190
- 'II',
191
- 'III',
192
- 'IV',
193
- 'J.D.',
194
- 'Jr.',
195
- 'K.C.',
196
- 'L.P.S.',
197
- 'LL.B.',
198
- 'LL.D.',
199
- 'LL.M.',
200
- 'M.A.',
201
- 'M.B.A.',
202
- 'M.B.E.',
203
- 'M.D.',
204
- 'M.E.P.',
205
- 'M.Ed.',
206
- 'M.Eng.',
207
- 'M.I.E.T.',
208
- 'M.Io.D.',
209
- 'M.Jur.',
210
- 'M.P.',
211
- 'M.P.A.',
212
- 'M.R.I.C.S.',
213
- 'M.S.',
214
- 'M.S.F.',
215
- 'M.S.F.S.',
216
- 'M.S.P.',
217
- 'M.Sc. D.',
218
- 'M.Sc.',
219
- 'O.B.E.',
220
- 'O.K.',
221
- 'O.R.S.C.',
222
- 'Oxon.',
223
- 'P.A.',
224
- 'P.C.C.',
225
- 'P.F.S.',
226
- 'P.H.R.',
227
- 'P.M.C.',
228
- 'P.M.P.',
229
- 'P.M.P.',
230
- 'P.S.P.',
231
- 'Ph.D.',
232
- 'Q.C.',
233
- 'R.D.',
234
- 'R.F.C.',
235
- 'R.I.C.P.',
236
- 'S.C.M.P',
237
- 'Sr.',
238
- 'T.M.I.E.T.',
239
- 'V',
240
- 'V.M.D.',
241
- ],
242
- organization: [
243
- # These are sorted in length order: don't alphabetize them.
244
- 'S. de R.L. de C.V.',
245
- 'S.A.P.I. de C.V.',
246
- 'y. Cía. S. en C.',
247
- 'Private Limited',
248
- 'S.M. Pte. Ltd.',
249
- 'Cía. S. C. A.',
250
- 'y. Cía. S. C.',
251
- 'S.A. de C.V.',
252
- 'spol. s.r.o.',
253
- '(Pty.) Ltd.',
254
- '(Pvt.) Ltd.',
255
- 'A.D.S.I.Tz.',
256
- 'S.p. z.o.o.',
257
- '(Pvt.)Ltd.',
258
- 'akc. spol.',
259
- 'Cía. Ltda.',
260
- 'E.B.V.B.A.',
261
- 'P. Limited',
262
- 'S. de R.L.',
263
- 'S.I.C.A.V.',
264
- 'S.P.R.L.U.',
265
- 'А.Д.С.И.Ц.',
266
- '(P.) Ltd.',
267
- 'C. por A.',
268
- 'Comm.V.A.',
269
- 'Ltd. Şti.',
270
- 'Plc. Ltd.',
271
- 'Pte. Ltd.',
272
- 'Pty. Ltd.',
273
- 'Pvt. Ltd.',
274
- 'Soc. Col.',
275
- 'A.M.B.A.',
276
- 'A.S.B.L.',
277
- 'A.V.E.E.',
278
- 'B.V.B.A.',
279
- 'B.V.I.O.',
280
- 'C.V.B.A.',
281
- 'C.V.O.A.',
282
- 'E.E.I.G.',
283
- 'E.I.R.L.',
284
- 'E.O.O.D.',
285
- 'E.U.R.L.',
286
- 'F.M.B.A.',
287
- 'G.m.b.H.',
288
- 'Ges.b.R.',
289
- 'K.G.a.A.',
290
- 'L.L.L.P.',
291
- 'Ltd. Co.',
292
- 'Ltd. Co.',
293
- 'M.E.P.E.',
294
- 'n.y.r.t.',
295
- 'O.V.E.E.',
296
- 'P.E.E.C.',
297
- 'P.L.L.C.',
298
- 'P.L.L.C.',
299
- 'S. en C.',
300
- 'S.a.p.a.',
301
- 'S.A.R.L.',
302
- 'S.à.R.L.',
303
- 'S.A.S.U.',
304
- 'S.C.e.I.',
305
- 'S.C.O.P.',
306
- 'S.C.p.A.',
307
- 'S.C.R.I.',
308
- 'S.C.R.L.',
309
- 'S.M.B.A.',
310
- 'S.P.R.L.',
311
- 'Е.О.О.Д.',
312
- '&. Cie.',
313
- 'and Co.',
314
- 'Comm.V.',
315
- 'Limited',
316
- 'P. Ltd.',
317
- 'Part.G.',
318
- 'Sh.p.k.',
319
- '&. Co.',
320
- 'C.X.A.',
321
- 'd.n.o.',
322
- 'd.o.o.',
323
- 'E.A.D.',
324
- 'e.h.f.',
325
- 'E.P.E.',
326
- 'E.S.V.',
327
- 'F.C.P.',
328
- 'F.I.E.',
329
- 'G.b.R.',
330
- 'G.I.E.',
331
- 'G.M.K.',
332
- 'G.S.K.',
333
- 'H.U.F.',
334
- 'K.D.A.',
335
- 'k.f.t.',
336
- 'k.h.t.',
337
- 'k.k.t.',
338
- 'L.L.C.',
339
- 'L.L.P.',
340
- 'o.h.f.',
341
- 'O.H.G.',
342
- 'O.O.D.',
343
- 'O.y.j.',
344
- 'p.l.c.',
345
- 'P.S.U.',
346
- 'S.A.E.',
347
- 'S.A.S.',
348
- 'S.C.A.',
349
- 'S.C.E.',
350
- 'S.C.S.',
351
- 'S.E.M.',
352
- 'S.E.P.',
353
- 's.e.s.',
354
- 'S.G.R.',
355
- 'S.N.C.',
356
- 'S.p.A.',
357
- 'S.P.E.',
358
- 'S.R.L.',
359
- 's.r.o.',
360
- 'Unltd.',
361
- 'V.O.F.',
362
- 'V.o.G.',
363
- 'v.o.s.',
364
- 'V.Z.W.',
365
- 'z.r.t.',
366
- 'А.А.Т.',
367
- 'Е.А.Д.',
368
- 'З.А.Т.',
369
- 'К.Д.А.',
370
- 'О.О.Д.',
371
- 'Т.А.А.',
372
- '股份有限公司',
373
- 'Ap.S.',
374
- 'Corp.',
375
- 'ltda.',
376
- 'Sh.A.',
377
- 'st.G.',
378
- 'Ultd.',
379
- 'a.b.',
380
- 'A.D.',
381
- 'A.E.',
382
- 'A.G.',
383
- 'A.S.',
384
- 'A.Ş.',
385
- 'A.y.',
386
- 'B.M.',
387
- 'b.t.',
388
- 'B.V.',
389
- 'C.A.',
390
- 'C.V.',
391
- 'd.d.',
392
- 'e.c.',
393
- 'E.E.',
394
- 'e.G.',
395
- 'E.I.',
396
- 'E.P.',
397
- 'E.T.',
398
- 'E.U.',
399
- 'e.v.',
400
- 'G.K.',
401
- 'G.P.',
402
- 'h.f.',
403
- 'Inc.',
404
- 'K.D.',
405
- 'K.G.',
406
- 'K.K.',
407
- 'k.s.',
408
- 'k.v.',
409
- 'K.y.',
410
- 'L.C.',
411
- 'L.P.',
412
- 'Ltd.',
413
- 'N.K.',
414
- 'N.L.',
415
- 'N.V.',
416
- 'O.E.',
417
- 'O.G.',
418
- 'O.Ü.',
419
- 'O.y.',
420
- 'P.C.',
421
- 'p.l.',
422
- 'Pty.',
423
- 'PUP.',
424
- 'Pvt.',
425
- 'r.t.',
426
- 'S.A.',
427
- 'S.D.',
428
- 'S.E.',
429
- 's.f.',
430
- 'S.L.',
431
- 'S.P.',
432
- 'S.s.',
433
- 'T.K.',
434
- 'T.Ü.',
435
- 'U.Ü.',
436
- 'Y.K.',
437
- 'А.Д.',
438
- 'І.П.',
439
- 'К.Д.',
440
- 'ПУП.',
441
- 'С.Д.',
442
- 'בע"מ',
443
- '任意組合',
444
- '匿名組合',
445
- '合同会社',
446
- '合名会社',
447
- '合資会社',
448
- '有限会社',
449
- '有限公司',
450
- '株式会社',
451
- 'A/S',
452
- 'G/S',
453
- 'I/S',
454
- 'K/S',
455
- 'P/S',
456
- 'S/A',
457
- ],
69
+ person: get_constants_from('adfixes_suffix_person'),
70
+ organization: get_constants_from('adfixes_suffix_organization'),
458
71
  before: ADFIX_JOINERS, after: '\\z'
459
72
  },
460
73
  }.freeze
461
74
 
462
- ADFIX_PATTERNS = Hash[%i[prefix suffix].map do |adfix_type|
75
+ CONTACT_TYPES = %i[person organization].freeze
76
+
77
+ ADFIX_PATTERNS = %i[prefix suffix].to_h do |adfix_type|
463
78
  patterns = {}
464
79
  adfix = ADFIXES[adfix_type]
465
80
 
466
- %i[person organization].each do |ct|
81
+ CONTACT_TYPES.each do |ct|
467
82
  with_optional_spaces = adfix[ct].map { |p| p.gsub(ASCII_SPACE, ' *') }
468
83
  pattern_string = with_optional_spaces.join('|').gsub('.', '\.*')
469
- patterns[ct] = /#{adfix[:before]}\(*(?:#{pattern_string})[®™\)]*#{adfix[:after]}/i
84
+ patterns[ct] = /#{adfix[:before]}\(*(?:#{pattern_string})[®™)]*#{adfix[:after]}/i
470
85
  end
471
86
 
472
87
  [adfix_type, patterns]
473
- end]
88
+ end
474
89
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NameTamer
4
+ class Name
5
+ module PrivateMethodsNiceName
6
+ def unescape
7
+ @tidy_name = Strings.unescape_html(Strings.safe_unescape(Strings.ensure_safe(@tidy_name)))
8
+ end
9
+
10
+ def remove_zero_width
11
+ @tidy_name = Strings.strip_unwanted(@tidy_name, ZERO_WIDTH_FILTER)
12
+ end
13
+
14
+ def tidy_spacing
15
+ @tidy_name = Strings.whitespace_to(Strings.space_around_comma(@tidy_name).strip, ASCII_SPACE)
16
+ end
17
+
18
+ def fix_encoding_errors
19
+ @tidy_name = Strings.fix_encoding_errors(@tidy_name)
20
+ end
21
+
22
+ # Remove spaces from groups of initials
23
+ def consolidate_initials
24
+ @tidy_name = Strings.ensure_space_after_initials(Strings.remove_spaces_from_initials(@tidy_name))
25
+ end
26
+
27
+ # An adfix is either a prefix or a suffix
28
+ def remove_adfixes
29
+ if @last_name.nil?
30
+ # Our name is still in one part, not two
31
+ loop do
32
+ @nice_name = remove_outermost_adfix(:suffix, @nice_name)
33
+ break unless @adfix_found
34
+ end
35
+
36
+ loop do
37
+ @nice_name = remove_outermost_adfix(:prefix, @nice_name)
38
+ break unless @adfix_found
39
+ end
40
+ else
41
+ # Our name is currently in two halves
42
+ loop do
43
+ @last_name = remove_outermost_adfix(:suffix, @last_name)
44
+ break unless @adfix_found
45
+ end
46
+
47
+ loop do
48
+ @remainder = remove_outermost_adfix(:prefix, @remainder)
49
+ break unless @adfix_found
50
+ end
51
+ end
52
+ end
53
+
54
+ # Names in the form "Smith, John" need to be turned around to "John Smith"
55
+ def fixup_last_name_first
56
+ return if @contact_type == :organization
57
+
58
+ parts = @nice_name.split ', '
59
+
60
+ return unless parts.count == 2
61
+
62
+ @last_name = parts[0] # Sometimes the last name alone is all caps and we can name-case it
63
+ @remainder = parts[1]
64
+ end
65
+
66
+ # Sometimes we end up with mismatched braces after adfix stripping
67
+ # e.g. "Ceres (Ceres Holdings LLC)" -> "Ceres (Ceres Holdings"
68
+ def fixup_mismatched_braces
69
+ left_brace_count = @nice_name.count '('
70
+ right_brace_count = @nice_name.count ')'
71
+
72
+ if left_brace_count > right_brace_count
73
+ @nice_name += ')'
74
+ elsif left_brace_count < right_brace_count
75
+ @nice_name = "(#{@nice_name}"
76
+ end
77
+ end
78
+
79
+ def name_wrangle
80
+ # Fix case if all caps or all lowercase
81
+ if @last_name.nil?
82
+ name_wrangle_single_name
83
+ else
84
+ name_wrangle_split_name
85
+ end
86
+ end
87
+
88
+ def name_wrangle_single_name
89
+ lowercase = @nice_name.downcase
90
+ uppercase = @nice_name.upcase
91
+ fix_case = false
92
+
93
+ if @contact_type == :organization
94
+ fix_case = true if @nice_name == uppercase && @nice_name.length > 4
95
+ elsif [uppercase, lowercase].include?(@nice_name)
96
+ fix_case = true
97
+ end
98
+
99
+ @nice_name = name_case(lowercase) if fix_case
100
+ end
101
+
102
+ def name_wrangle_split_name
103
+ # It's a person if we've split the name, so no organization logic here
104
+ lowercase = @last_name.downcase
105
+ uppercase = @last_name.upcase
106
+ @last_name = name_case(lowercase) if [uppercase, lowercase].include?(@last_name)
107
+ @nice_name = "#{@remainder} #{@last_name}"
108
+ end
109
+
110
+ # Conjoin compound names with non-breaking spaces
111
+ def use_nonbreaking_spaces_in_compound_names
112
+ @nice_name = Strings.nbsp_in_name_modifier(Strings.nbsp_in_compound_name(@nice_name))
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NameTamer
4
+ class Name
5
+ module PrivateMethodsSimpleName
6
+ # Remove initials from personal names unless they are the only identifier.
7
+ # i.e. only remove initials if there's also a proper name there
8
+ def remove_initials
9
+ return unless @contact_type == :person
10
+
11
+ temp_name = @simple_name.gsub(/\b([a-z](?:\.*\s+|\.))/i, '')
12
+
13
+ # If the name still has at least one space we're OK
14
+ @simple_name = temp_name if temp_name.include?(ASCII_SPACE)
15
+ end
16
+
17
+ def remove_middle_names
18
+ return unless @contact_type == :person
19
+
20
+ first_name, parts = find_first_usable_name(@simple_name.split)
21
+ last_name, = find_last_usable_name(parts)
22
+
23
+ return unless first_name || last_name
24
+
25
+ separator = first_name && last_name ? ' ' : ''
26
+ @simple_name = "#{first_name}#{separator}#{last_name}"
27
+ end
28
+
29
+ def find_first_usable_name(parts)
30
+ part = nil
31
+
32
+ parts.each_index do |i|
33
+ part = parts[i]
34
+ next if part.gsub(FILTER_COMPAT, '').empty?
35
+
36
+ parts = parts.slice(i + 1, parts.length) # don't use "slice!"
37
+ break
38
+ end
39
+
40
+ [part, parts]
41
+ end
42
+
43
+ def find_last_usable_name(parts)
44
+ part = nil
45
+
46
+ parts.reverse_each do |p|
47
+ next if p.gsub(FILTER_COMPAT, '').empty?
48
+
49
+ part = p
50
+ break
51
+ end
52
+
53
+ part
54
+ end
55
+
56
+ def remove_periods_from_initials
57
+ @simple_name = Strings.remove_periods_from_initials(@simple_name)
58
+ end
59
+
60
+ def standardize_words
61
+ words = @simple_name
62
+ .gsub(/ *& */, ' and ') # replace ampersand characters with ' and '
63
+ .gsub(/ *\+ */, ' plus ') # replace plus signs with ' plus '
64
+ .gsub(/\bintl\b/i, 'International') # replace 'intl' with 'International'
65
+ .gsub(/[־‐‑‒–—―−﹘﹣-]/, SLUG_DELIMITER) # Replace Unicode dashes with ASCII hyphen
66
+
67
+ @simple_name = Strings.strip_unwanted(words, /["“”™℠®©℗]/) # remove quotes and commercial decoration
68
+ end
69
+ end
70
+ end
71
+ end