piplapis-ruby 5.0.5 → 5.2.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.
data/lib/pipl/fields.rb CHANGED
@@ -1,732 +1,804 @@
1
- require 'date'
2
- require 'set'
3
- require_relative 'consts'
4
- require_relative 'utils'
5
-
6
- module Pipl
7
-
8
- class Field
9
-
10
- include Pipl::Utils
11
-
12
- attr_accessor :valid_since, :last_seen, :inferred, :current
13
-
14
- def initialize(params={})
15
- @valid_since = params[:valid_since]
16
- @last_seen = params[:last_seen]
17
- @inferred = params[:inferred]
18
- @current = params[:current]
19
- end
20
-
21
- def self.from_hash(h)
22
- params = base_params_from_hash h
23
- extra_metadata.each { |p| params[p] = h["@#{p}".to_sym] }
24
- params = h.merge params
25
- self.new(params)
26
- end
27
-
28
- def self.base_params_from_hash(h)
29
- params = {
30
- inferred: h[:@inferred],
31
- current: h[:@current],
32
- type: h[:@type],
33
- display: h[:display]
34
- }
35
- params[:valid_since] = Date.strptime(h[:@valid_since], Pipl::DATE_FORMAT) if h.key? :@valid_since
36
- params[:last_seen] = Date.strptime(h[:@last_seen], Pipl::DATE_FORMAT) if h.key? :@last_seen
37
- params[:date_range] = Pipl::DateRange.from_hash(h[:date_range]) if h.key? :date_range
38
- params
39
- end
40
-
41
- def self.extra_metadata
42
- []
43
- end
44
-
45
- def to_hash
46
-
47
- end
48
-
49
- def is_searchable?
50
- true
51
- end
52
-
53
- end
54
-
55
-
56
- class Name < Field
57
- # @!attribute first
58
- # @return [String] First name
59
- # @!attribute middle
60
- # @return [String] Middle name or initial
61
- # @!attribute last
62
- # @return [String] Last name
63
- # @!attribute prefix
64
- # @return [String] Name prefix
65
- # @!attribute suffix
66
- # @return [String] Name suffix
67
- # @!attribute type
68
- # @return [String] Type of association of this name to a person. One of `present`, `maiden`, `former` or `alias`.
69
-
70
- attr_accessor :first, :middle, :last, :prefix, :suffix, :type, :raw, :display
71
-
72
- def initialize(params={})
73
- super params
74
- @first = params[:first]
75
- @middle = params[:middle]
76
- @last = params[:last]
77
- @prefix = params[:prefix]
78
- @suffix = params[:suffix]
79
- @type = params[:type]
80
- @raw = params[:raw]
81
- @display = params[:display]
82
- end
83
-
84
- def to_hash
85
- {first: @first, middle: @middle, last: @last, prefix: @prefix, suffix: @suffix, raw: @raw}
86
- .reject { |_, value| value.nil? }
87
- end
88
-
89
- def is_searchable?
90
- first = Pipl::Utils.alpha_chars(@first || '')
91
- last = Pipl::Utils.alpha_chars(@last || '')
92
- raw = Pipl::Utils.alpha_chars(@raw || '')
93
- (first.length > 1 and last.length > 1) or raw.length > 3
94
- end
95
-
96
- def to_s
97
- return @display if @display
98
-
99
- vals = [@prefix, @first, @middle, @last, @suffix]
100
- s = vals.any? ? vals.select { |v| v }.map { |v| v.capitalize }.join(' ') : nil
101
- s ? Pipl::Utils.to_utf8(s) : ''
102
- end
103
-
104
- end
105
-
106
-
107
- class Address < Field
108
- # @!attribute country
109
- # @return [String] 2 letters country code
110
- # @!attribute state
111
- # @return [String] 2 letters state code
112
- # @!attribute city
113
- # @return [String] City
114
- # @!attribute street
115
- # @return [String] Street
116
- # @!attribute house
117
- # @return [String] House
118
- # @!attribute apartment
119
- # @return [String] Apartment
120
- # @!attribute zip_code
121
- # @return [String] Zip Code
122
- # @!attribute po_box
123
- # @return [String] Post Office box number
124
- # @!attribute type
125
- # @return [String] Type of association of this address to a person. One of `home`, `work` or `old`.
126
- # @!attribute raw
127
- # @return [String] Unparsed address.
128
- # @!attribute display
129
- # @return [String] well formatted representation of this address for display purposes.
130
-
131
- attr_accessor :country, :state, :city, :street, :house, :apartment, :zip_code, :po_box, :type, :raw, :display
132
-
133
- def initialize(params={})
134
- super params
135
- @country = params[:country]
136
- @state = params[:state]
137
- @city = params[:city]
138
- @street = params[:street]
139
- @house = params[:house]
140
- @apartment = params[:apartment]
141
- @zip_code = params[:zip_code]
142
- @po_box = params[:po_box]
143
- @type = params[:type]
144
- @raw = params[:raw]
145
- @display = params[:display]
146
- end
147
-
148
- def is_valid_country?
149
- @country and Pipl::COUNTRIES.key? @country.upcase.to_sym
150
- end
151
-
152
- def is_valid_state?
153
- is_valid_country? and Pipl::STATES.key?(@country.upcase.to_sym) and
154
- @state and Pipl::STATES[@country.upcase.to_sym].key?(@state.upcase.to_sym)
155
- end
156
-
157
- def to_hash
158
- {country: @country, state: @state, city: @city, street: @street, house: @house, apartment: @apartment,
159
- zip_code: @zip_code, po_box: @po_box, raw: @raw}
160
- .reject { |_, value| value.nil? }
161
- end
162
-
163
- def is_searchable?
164
- [@raw, @country, @state, @city].any? {|x| not x.to_s.empty?}
165
- end
166
-
167
- def is_sole_searchable?
168
- not @raw.to_s.empty? or [@city, @street, @house].all? {|x| not x.to_s.empty?}
169
- end
170
-
171
- def country_full
172
- Pipl::COUNTRIES[@country.upcase.to_sym] if @country
173
- end
174
-
175
- def state_full
176
- Pipl::STATES[@country.upcase.to_sym][@state.upcase.to_sym] if is_valid_state?
177
- end
178
-
179
- def to_s
180
- return @display if @display
181
-
182
- country = @state ? @country : country_full
183
- state = @city ? @state : state_full
184
- vals = [@street, @city, state, country]
185
- s = vals.any? ? vals.select { |v| v }.join(', ') : ''
186
-
187
- if @street and (@house or @apartment)
188
- prefix = [@house, @apartment].select { |v| v and not v.empty? }.join('-')
189
- s = prefix + ' ' + (s || '')
190
- end
191
-
192
- if @po_box and @street.nil?
193
- s = "P.O. Box #{@po_box} " + (s || '')
194
- end
195
-
196
- s ? Pipl::Utils.to_utf8(s) : ''
197
- end
198
-
199
- end
200
-
201
-
202
- class Phone < Field
203
- # @!attribute country_code
204
- # @return [Fixnum] International country calling code
205
- # @!attribute number
206
- # @return [Fixnum] Actual phone number
207
- # @!attribute extension
208
- # @return [String] Office extension
209
- # @!attribute type
210
- # @return [String] Type of association of this phone to a person.
211
- # Possible values are:
212
- # mobile
213
- # home_phone
214
- # home_fax
215
- # work_phone
216
- # work_fax
217
- # pager
218
- # @!attribute raw
219
- # @return [String] Unparsed phone number
220
- # @!attribute display
221
- # @return [String] Well formatted representation of this phone number for display purposes.
222
- # @!attribute display_international
223
- # @return [String] Well formatted international representation of this phone number for display purposes.
224
-
225
- attr_accessor :country_code, :number, :extension, :type, :raw, :display, :display_international
226
-
227
- def initialize(params={})
228
- super params
229
- @country_code = params[:country_code]
230
- @number = params[:number]
231
- @extension = params[:extension]
232
- @type = params[:type]
233
- @raw = params[:raw]
234
- @display = params[:display]
235
- @display_international = params[:display_international]
236
- end
237
-
238
- def self.extra_metadata
239
- [:display_international]
240
- end
241
-
242
- def to_hash
243
- {country_code: @country_code, number: @number, extension: @extension, raw: @raw}
244
- .reject { |_, value| value.nil? }
245
- end
246
-
247
- def is_searchable?
248
- (@raw and not @raw.empty?) or not @number.nil?
249
- end
250
-
251
- end
252
-
253
-
254
- class Email < Field
255
-
256
- RE_EMAIL = Regexp.new('^[a-zA-Z0-9\'._%\-+]+@[a-zA-Z0-9._%\-]+\.[a-zA-Z]{2,24}$')
257
-
258
- # @!attribute address
259
- # @return [String] Plain email address
260
- # @!attribute address_md5
261
- # @return [String] MD5 hash of the email address
262
- # @!attribute type
263
- # @return [String] Type of email association to a person. One of `personal` or `work`.
264
- # @!attribute disposable
265
- # @return [Boolean] Indicating if this email comes from a disposable email provider.
266
- # @!attribute email_provider
267
- # @return [Boolean] Indicating if this email comes from a well known email provider like gmail or yahoo.
268
-
269
- attr_accessor :address, :address_md5, :type, :disposable, :email_provider
270
-
271
- def initialize(params={})
272
- super params
273
- @address = params[:address]
274
- @address_md5 = params[:address_md5]
275
- @type = params[:type]
276
- @disposable = params[:disposable]
277
- @email_provider = params[:email_provider]
278
- end
279
-
280
- def self.extra_metadata
281
- [:disposable, :email_provider]
282
- end
283
-
284
- def is_valid_email?
285
- not RE_EMAIL.match(@address).nil?
286
- end
287
-
288
- def is_searchable?
289
- is_valid_email? or (not @address_md5.nil? and @address_md5.length == 32)
290
- end
291
-
292
- def to_hash
293
- {address: @address, address_md5: @address_md5}.reject { |_, value| value.nil? }
294
- end
295
-
296
- def username
297
- @address.split('@')[0] if is_valid_email?
298
- end
299
-
300
- def domain
301
- @address.split('@')[1] if is_valid_email?
302
- end
303
-
304
- end
305
-
306
-
307
- class Job < Field
308
-
309
- attr_accessor :title, :organization, :industry, :date_range, :display
310
-
311
- def initialize(params={})
312
- super params
313
- @title = params[:title]
314
- @organization = params[:organization]
315
- @industry = params[:industry]
316
- @date_range = params[:date_range]
317
- @display = params[:display]
318
- end
319
-
320
- def to_hash
321
- {title: @title, organization: @organization, industry: @industry,
322
- date_range: @date_range ? @date_range.to_hash : nil}
323
- .reject { |_, value| value.nil? }
324
- end
325
-
326
- def to_s
327
- return @display if @display
328
-
329
- if @title and @organization
330
- s = @title + ' at ' + @organization
331
- else
332
- s = @title || @organization
333
- end
334
-
335
- if s and @industry
336
- if @date_range
337
- range = @date_range.years_range
338
- s += ' (%s, %d-%d)' % [@industry, range[0], range[1]]
339
- else
340
- s += ' (%s)' % [@industry]
341
- end
342
- else
343
- s = ((s || '') + ' ' + (@industry || '')).strip
344
- if s and @date_range
345
- range = @date_range.years_range
346
- s += ' (%d-%d)' % [range[0], range[1]]
347
- end
348
- end
349
-
350
- s ? Pipl::Utils.to_utf8(s) : ''
351
- end
352
-
353
- end
354
-
355
-
356
- class Education < Field
357
-
358
- attr_accessor :degree, :school, :date_range, :display
359
-
360
- def initialize(params={})
361
- super params
362
- @degree = params[:degree]
363
- @school = params[:school]
364
- @date_range = params[:date_range]
365
- @display = params[:display]
366
- end
367
-
368
- def to_hash
369
- {degree: @degree, school: @school, date_range: @date_range ? @date_range.to_hash : nil}
370
- .reject { |_, value| value.nil? }
371
- end
372
-
373
- def to_s
374
- return @display if @display
375
-
376
- if @degree and @school
377
- s = @degree + ' from ' + @school
378
- else
379
- s = @degree || @school
380
- end
381
-
382
- if s and @date_range
383
- range = @date_range.years_range
384
- s += ' (%d-%d)' % [range[0], range[1]]
385
- end
386
-
387
- s ? Pipl::Utils.to_utf8(s) : ''
388
- end
389
-
390
- end
391
-
392
-
393
- class Image < Field
394
-
395
- attr_accessor :url, :thumbnail_token
396
-
397
- def initialize(params={})
398
- super params
399
- @url = params[:url]
400
- @thumbnail_token = params[:thumbnail_token]
401
- end
402
-
403
- def thumbnail_url(params={})
404
- return unless @thumbnail_token
405
-
406
- opts = {width: 100, height: 100, favicon: true, zoom_face: true, use_https: false}.merge(params)
407
- schema = opts.delete(:use_https) ? 'https': 'http'
408
- tokens = @thumbnail_token.gsub(/&dsid=.*/,'')
409
- tokens += ',' + opts.delete(:fallback).thumbnail_token.gsub(/&dsid=.*/,'') if opts[:fallback]
410
- query_params = ["tokens=#{tokens}"] + opts.map { |k, v| "#{k}=#{v}" unless v.nil? }
411
- "#{schema}://thumb.pipl.com/image?#{query_params.compact.join('&')}"
412
- end
413
-
414
- end
415
-
416
-
417
- class Username < Field
418
-
419
- attr_accessor :content
420
-
421
- def initialize(params={})
422
- super params
423
- @content = params[:content]
424
- end
425
-
426
- def to_hash
427
- {content: @content} if @content
428
- end
429
-
430
- def is_searchable?
431
- !@content.nil? and Pipl::Utils.alnum_chars(@content).length > 2
432
- end
433
-
434
- end
435
-
436
-
437
- class UserID < Username
438
-
439
- def is_searchable?
440
- not /.+@.+/.match(@content).nil?
441
- end
442
-
443
- end
444
-
445
-
446
- class DOB < Field
447
-
448
- attr_accessor :date_range, :display
449
-
450
- def initialize(params={})
451
- super params
452
- @date_range = params[:date_range]
453
- @display = params[:display]
454
- end
455
-
456
- def self.from_birth_year(birth_year)
457
- raise ArgumentError.new('birth_year must be positive') unless birth_year > 0
458
- self.new({date_range: DateRange.from_years_range(birth_year, birth_year)})
459
- end
460
-
461
- def self.from_birth_date(birth_date)
462
- raise ArgumentError.new('birth_date can\'t be in the future') unless birth_date <= Date.today
463
- self.new({date_range: DateRange.new(birth_date, birth_date)})
464
- end
465
-
466
- def self.from_age(age)
467
- self.from_age_range(age, age)
468
- end
469
-
470
- def self.from_age_range(start_age, end_age)
471
- raise ArgumentError.new('start_age and end_age can\'t be negative') if start_age < 0 or end_age < 0
472
-
473
- if start_age > end_age
474
- start_age, end_age = end_age, start_age
475
- end
476
-
477
- today = Date.today
478
- start_date = today << end_age * 12
479
- start_date = start_date - 1
480
- end_date = today << start_age * 12
481
- self.new({date_range: Pipl::DateRange.new(start_date, end_date)})
482
- end
483
-
484
- def to_s
485
- @display or Pipl::Utils.to_utf8(age.to_s)
486
- end
487
-
488
- def age
489
- unless @date_range.nil?
490
- dob = @date_range.middle
491
- today = Date.today
492
- diff = today.year - dob.year
493
- diff = diff - 1 if dob.month > today.month or (dob.month >= today.month and dob.day > today.day)
494
- diff
495
- end
496
- end
497
-
498
- def age_range
499
- if @date_range
500
- return [self.age, self.age] unless @date_range.start and @date_range.end
501
- start_age = DOB.new({date_range: Pipl::DateRange.new(@date_range.start, @date_range.start)}).age
502
- end_age = DOB.new({date_range: Pipl::DateRange.new(@date_range.end, @date_range.end)}).age
503
- return end_age, start_age
504
- else
505
- return nil, nil
506
- end
507
- end
508
-
509
- def to_hash
510
- {date_range: @date_range.to_hash} if @date_range
511
- end
512
-
513
- def is_searchable?
514
- not @date_range.nil?
515
- end
516
-
517
- end
518
-
519
-
520
- class Url < Field
521
- # @!attribute url
522
- # @return [String] Actual Url
523
- # @!attribute category
524
- # @return [String] Category of the domain
525
- # Possible values are:
526
- # background_reports
527
- # contact_details
528
- # email_address
529
- # media
530
- # personal_profiles
531
- # professional_and_business
532
- # public_records
533
- # publications
534
- # school_and_classmates
535
- # web_pages
536
- # @!attribute domain
537
- # @return [String] Canonical domain of the url
538
- # @!attribute name
539
- # @return [String] Name of the website hosting the url
540
- # @!attribute sponsored
541
- # @return [Boolean] Indicate if this url comes from a sponsored data source
542
- # @!attribute sponsored
543
- # @return [String] Unique identifier of this url
544
-
545
- attr_accessor :url, :category, :domain, :name, :sponsored, :source_id
546
-
547
- def initialize(params={})
548
- super params
549
- @url = params[:url]
550
- @category = params[:category]
551
- @domain = params[:domain]
552
- @name = params[:name]
553
- @sponsored = params[:sponsored]
554
- @source_id = params[:source_id]
555
- end
556
-
557
- def self.extra_metadata
558
- [:category, :domain, :name, :sponsored, :source_id]
559
- end
560
-
561
- def is_searchable?
562
- not @url.to_s.empty?
563
- end
564
-
565
- end
566
-
567
-
568
- class Gender < Field
569
-
570
- attr_accessor :content
571
-
572
- def initialize(params={})
573
- super params
574
- @content = params[:content]
575
- end
576
-
577
- def to_s
578
- Pipl::Utils.titleize @content if @content
579
- end
580
-
581
- def to_hash
582
- {content: @content} if @content
583
- end
584
-
585
- end
586
-
587
-
588
- class Ethnicity < Field
589
-
590
- # @!attribute content
591
- # @return [String] Ethnicity name based on the U.S Census Bureau.
592
- # Possible values are:
593
- # white
594
- # black
595
- # american_indian
596
- # alaska_native
597
- # asian_indian
598
- # chinese
599
- # filipino
600
- # other_asian
601
- # japanese
602
- # korean
603
- # vietnamese
604
- # native_hawaiian
605
- # guamanian
606
- # chamorro
607
- # samoan
608
- # other_pacific_islander
609
- # other
610
-
611
- attr_accessor :content
612
-
613
- def initialize(params={})
614
- super params
615
- @content = params[:content]
616
- end
617
-
618
- def to_s
619
- Pipl::Utils.titleize @content.gsub(/_/, ' ') if @content
620
- end
621
-
622
- end
623
-
624
-
625
- class Language < Field
626
-
627
- attr_accessor :language, :region, :display
628
-
629
- def initialize(params={})
630
- super params
631
- @language = params[:language]
632
- @region = params[:region]
633
- @display = params[:display]
634
- end
635
-
636
- def to_s
637
- return @display if @display
638
- return "#{@language}_#{@region}" if @language and @region
639
- return @language if @language and not @language.empty?
640
- @region
641
- end
642
-
643
- end
644
-
645
-
646
- class OriginCountry < Field
647
-
648
- attr_accessor :country
649
-
650
- def initialize(params={})
651
- super params
652
- @country = params[:country]
653
- end
654
-
655
- def to_s
656
- Pipl::COUNTRIES[@country.upcase.to_sym] if @country
657
- end
658
-
659
- end
660
-
661
-
662
- class Tag < Field
663
-
664
- attr_accessor :content, :classification
665
-
666
- def initialize(params={})
667
- super params
668
- @content = params[:content]
669
- @classification = params[:classification]
670
- end
671
-
672
- def self.extra_metadata
673
- [:classification]
674
- end
675
-
676
- def to_s
677
- @content
678
- end
679
-
680
- end
681
-
682
-
683
- class DateRange
684
-
685
- attr_reader :start, :end
686
-
687
- def initialize(start, end_)
688
- @start = start
689
- @end = end_
690
- if @start and @end and @start > @end
691
- @start, @end = @end, @start
692
- end
693
- end
694
-
695
- # def ==(other)
696
- # other.instance_of?(self.class) and inspect == other.inspect
697
- # end
698
- #
699
- # alias_method :eql?, :==
700
-
701
- def is_exact?
702
- @start and @end and @start == @end
703
- end
704
-
705
- def middle
706
- @start and @end ? @start + ((@end - @start) / 2) : @start or @end
707
- end
708
-
709
- def years_range
710
- [@start.year, @end.year] if @start and @end
711
- end
712
-
713
- def self.from_years_range(start_year, end_year)
714
- self.new(Date.new(start_year, 1, 1), Date.new(end_year, 12, 31))
715
- end
716
-
717
- def self.from_hash(h)
718
- start_, end_ = h[:start], h[:end]
719
- initializing_start = start_ ? Date.strptime(start_, Pipl::DATE_FORMAT) : nil
720
- initializing_end = end_ ? Date.strptime(end_, Pipl::DATE_FORMAT) : nil
721
- self.new(initializing_start, initializing_end)
722
- end
723
-
724
- def to_hash
725
- h = {}
726
- h[:start] = @start.strftime(Pipl::DATE_FORMAT) if @start
727
- h[:end] = @end.strftime(Pipl::DATE_FORMAT) if @end
728
- h
729
- end
730
- end
731
-
732
- end
1
+ require 'date'
2
+ require 'set'
3
+ require_relative 'consts'
4
+ require_relative 'utils'
5
+
6
+ module Pipl
7
+
8
+ class Field
9
+
10
+ include Pipl::Utils
11
+
12
+ attr_accessor :valid_since, :last_seen, :inferred, :current
13
+
14
+ def initialize(params={})
15
+ @valid_since = params[:valid_since]
16
+ @last_seen = params[:last_seen]
17
+ @inferred = params[:inferred]
18
+ @current = params[:current]
19
+ end
20
+
21
+ def self.from_hash(h)
22
+ params = base_params_from_hash h
23
+ extra_metadata.each { |p| params[p] = h["@#{p}".to_sym] }
24
+ params = h.merge params
25
+ self.new(params)
26
+ end
27
+
28
+ def self.base_params_from_hash(h)
29
+ params = {
30
+ inferred: h[:@inferred],
31
+ current: h[:@current],
32
+ type: h[:@type],
33
+ display: h[:display]
34
+ }
35
+ params[:valid_since] = Date.strptime(h[:@valid_since], Pipl::DATE_FORMAT) if h.key? :@valid_since
36
+ params[:last_seen] = Date.strptime(h[:@last_seen], Pipl::DATE_FORMAT) if h.key? :@last_seen
37
+ params[:date_range] = Pipl::DateRange.from_hash(h[:date_range]) if h.key? :date_range
38
+ params
39
+ end
40
+
41
+ def self.extra_metadata
42
+ []
43
+ end
44
+
45
+ def to_hash
46
+
47
+ end
48
+
49
+ def is_searchable?
50
+ true
51
+ end
52
+
53
+ end
54
+
55
+
56
+ class Name < Field
57
+ # @!attribute first
58
+ # @return [String] First name
59
+ # @!attribute middle
60
+ # @return [String] Middle name or initial
61
+ # @!attribute last
62
+ # @return [String] Last name
63
+ # @!attribute prefix
64
+ # @return [String] Name prefix
65
+ # @!attribute suffix
66
+ # @return [String] Name suffix
67
+ # @!attribute type
68
+ # @return [String] Type of association of this name to a person. One of `present`, `maiden`, `former` or `alias`.
69
+
70
+ attr_accessor :first, :middle, :last, :prefix, :suffix, :type, :raw, :display
71
+
72
+ def initialize(params={})
73
+ super params
74
+ @first = params[:first]
75
+ @middle = params[:middle]
76
+ @last = params[:last]
77
+ @prefix = params[:prefix]
78
+ @suffix = params[:suffix]
79
+ @type = params[:type]
80
+ @raw = params[:raw]
81
+ @display = params[:display]
82
+ end
83
+
84
+ def to_hash
85
+ {first: @first, middle: @middle, last: @last, prefix: @prefix, suffix: @suffix, raw: @raw}
86
+ .reject { |_, value| value.nil? }
87
+ end
88
+
89
+ def is_searchable?
90
+ first = Pipl::Utils.alpha_chars(@first || '')
91
+ last = Pipl::Utils.alpha_chars(@last || '')
92
+ raw = Pipl::Utils.alpha_chars(@raw || '')
93
+ first.length > 0 || last.length > 0 || raw.length > 0
94
+ end
95
+
96
+ def to_s
97
+ return @display if @display
98
+
99
+ vals = [@prefix, @first, @middle, @last, @suffix]
100
+ s = vals.any? ? vals.select { |v| v }.map { |v| v.capitalize }.join(' ') : nil
101
+ s ? Pipl::Utils.to_utf8(s) : ''
102
+ end
103
+
104
+ end
105
+
106
+
107
+ class Address < Field
108
+ # @!attribute country
109
+ # @return [String] 2 letters country code
110
+ # @!attribute state
111
+ # @return [String] 2 letters state code
112
+ # @!attribute city
113
+ # @return [String] City
114
+ # @!attribute street
115
+ # @return [String] Street
116
+ # @!attribute house
117
+ # @return [String] House
118
+ # @!attribute apartment
119
+ # @return [String] Apartment
120
+ # @!attribute zip_code
121
+ # @return [String] Zip Code
122
+ # @!attribute po_box
123
+ # @return [String] Post Office box number
124
+ # @!attribute type
125
+ # @return [String] Type of association of this address to a person. One of `home`, `work` or `old`.
126
+ # @!attribute raw
127
+ # @return [String] Unparsed address.
128
+ # @!attribute display
129
+ # @return [String] well formatted representation of this address for display purposes.
130
+
131
+ attr_accessor :country, :state, :city, :street, :house, :apartment, :zip_code, :po_box, :type, :raw, :display
132
+
133
+ def initialize(params={})
134
+ super params
135
+ @country = params[:country]
136
+ @state = params[:state]
137
+ @city = params[:city]
138
+ @street = params[:street]
139
+ @house = params[:house]
140
+ @apartment = params[:apartment]
141
+ @zip_code = params[:zip_code]
142
+ @po_box = params[:po_box]
143
+ @type = params[:type]
144
+ @raw = params[:raw]
145
+ @display = params[:display]
146
+ end
147
+
148
+ def is_valid_country?
149
+ @country && Pipl::COUNTRIES.key?(@country.upcase.to_sym)
150
+ end
151
+
152
+ def is_valid_state?
153
+ is_valid_country? && Pipl::STATES.key?(@country.upcase.to_sym) and
154
+ @state && Pipl::STATES[@country.upcase.to_sym].key?(@state.upcase.to_sym)
155
+ end
156
+
157
+ def to_hash
158
+ {country: @country, state: @state, city: @city, street: @street, house: @house, apartment: @apartment,
159
+ zip_code: @zip_code, po_box: @po_box, raw: @raw}
160
+ .reject { |_, value| value.nil? }
161
+ end
162
+
163
+ def is_searchable?
164
+ [@raw, @country, @state, @city].any? {|x| ! x.to_s.empty?}
165
+ end
166
+
167
+ def is_sole_searchable?
168
+ ! @raw.to_s.empty? || [@city, @street, @house].all? {|x| ! x.to_s.empty?}
169
+ end
170
+
171
+ def country_full
172
+ Pipl::COUNTRIES[@country.upcase.to_sym] if @country
173
+ end
174
+
175
+ def state_full
176
+ Pipl::STATES[@country.upcase.to_sym][@state.upcase.to_sym] if is_valid_state?
177
+ end
178
+
179
+ def to_s
180
+ return @display if @display
181
+
182
+ country = @state ? @country : country_full
183
+ state = @city ? @state : state_full
184
+ vals = [@street, @city, state, country]
185
+ s = vals.any? ? vals.select { |v| v }.join(', ') : ''
186
+
187
+ if @street && (@house || @apartment)
188
+ prefix = [@house, @apartment].select { |v| v && ! v.empty? }.join('-')
189
+ s = prefix + ' ' + (s || '')
190
+ end
191
+
192
+ if @po_box && @street.nil?
193
+ s = "P.O. Box #{@po_box} " + (s || '')
194
+ end
195
+
196
+ s ? Pipl::Utils.to_utf8(s) : ''
197
+ end
198
+
199
+ end
200
+
201
+
202
+ class Phone < Field
203
+ # @!attribute country_code
204
+ # @return [Fixnum] International country calling code
205
+ # @!attribute number
206
+ # @return [Fixnum] Actual phone number
207
+ # @!attribute extension
208
+ # @return [String] Office extension
209
+ # @!attribute type
210
+ # @return [String] Type of association of this phone to a person.
211
+ # Possible values are:
212
+ # mobile
213
+ # home_phone
214
+ # home_fax
215
+ # work_phone
216
+ # work_fax
217
+ # pager
218
+ # @!attribute raw
219
+ # @return [String] Unparsed phone number
220
+ # @!attribute display
221
+ # @return [String] Well formatted representation of this phone number for display purposes.
222
+ # @!attribute display_international
223
+ # @return [String] Well formatted international representation of this phone number for display purposes.
224
+
225
+ attr_accessor :country_code, :number, :extension, :type, :raw, :display, :display_international, :voip
226
+
227
+ def initialize(params={})
228
+ super params
229
+ @country_code = params[:country_code]
230
+ @number = params[:number]
231
+ @extension = params[:extension]
232
+ @type = params[:type]
233
+ @raw = params[:raw]
234
+ @display = params[:display]
235
+ @display_international = params[:display_international]
236
+ @voip = params[:@voip] unless params[:@voip].nil?
237
+ end
238
+
239
+ def self.extra_metadata
240
+ [:display_international]
241
+ end
242
+
243
+ def to_hash
244
+ {country_code: @country_code, number: @number, extension: @extension, raw: @raw}
245
+ .reject { |_, value| value.nil? }
246
+ end
247
+
248
+ def is_searchable?
249
+ (@raw && ! @raw.empty?) || ! @number.nil?
250
+ end
251
+
252
+ end
253
+
254
+
255
+ class Email < Field
256
+
257
+ RE_EMAIL = Regexp.new('^[a-zA-Z0-9\'._%\-+]+@[a-zA-Z0-9._%\-]+\.[a-zA-Z]{2,24}$')
258
+
259
+ # @!attribute address
260
+ # @return [String] Plain email address
261
+ # @!attribute address_md5
262
+ # @return [String] MD5 hash of the email address
263
+ # @!attribute type
264
+ # @return [String] Type of email association to a person. One of `personal` or `work`.
265
+ # @!attribute disposable
266
+ # @return [Boolean] Indicating if this email comes from a disposable email provider.
267
+ # @!attribute email_provider
268
+ # @return [Boolean] Indicating if this email comes from a well known email provider like gmail or yahoo.
269
+
270
+ attr_accessor :address, :address_md5, :type, :disposable, :email_provider
271
+
272
+ def initialize(params={})
273
+ super params
274
+ @address = params[:address]
275
+ @address_md5 = params[:address_md5]
276
+ @type = params[:type]
277
+ @disposable = params[:disposable]
278
+ @email_provider = params[:email_provider]
279
+ end
280
+
281
+ def self.extra_metadata
282
+ [:disposable, :email_provider]
283
+ end
284
+
285
+ def is_valid_email?
286
+ ! RE_EMAIL.match(@address).nil?
287
+ end
288
+
289
+ def is_searchable?
290
+ is_valid_email? || (! @address_md5.nil? && @address_md5.length == 32)
291
+ end
292
+
293
+ def to_hash
294
+ {address: @address, address_md5: @address_md5}.reject { |_, value| value.nil? }
295
+ end
296
+
297
+ def username
298
+ @address.split('@')[0] if is_valid_email?
299
+ end
300
+
301
+ def domain
302
+ @address.split('@')[1] if is_valid_email?
303
+ end
304
+
305
+ end
306
+
307
+
308
+ class Job < Field
309
+
310
+ attr_accessor :title, :organization, :industry, :date_range, :display
311
+
312
+ def initialize(params={})
313
+ super params
314
+ @title = params[:title]
315
+ @organization = params[:organization]
316
+ @industry = params[:industry]
317
+ @date_range = params[:date_range]
318
+ @display = params[:display]
319
+ end
320
+
321
+ def to_hash
322
+ {title: @title, organization: @organization, industry: @industry,
323
+ date_range: @date_range ? @date_range.to_hash : nil}
324
+ .reject { |_, value| value.nil? }
325
+ end
326
+
327
+ def to_s
328
+ return @display if @display
329
+
330
+ if @title && @organization
331
+ s = @title + ' at ' + @organization
332
+ else
333
+ s = @title || @organization
334
+ end
335
+
336
+ if s && @industry
337
+ if @date_range
338
+ range = @date_range.years_range
339
+ s += ' (%s, %d-%d)' % [@industry, range[0], range[1]]
340
+ else
341
+ s += ' (%s)' % [@industry]
342
+ end
343
+ else
344
+ s = ((s || '') + ' ' + (@industry || '')).strip
345
+ if s && @date_range
346
+ range = @date_range.years_range
347
+ s += ' (%d-%d)' % [range[0], range[1]]
348
+ end
349
+ end
350
+
351
+ s ? Pipl::Utils.to_utf8(s) : ''
352
+ end
353
+
354
+ end
355
+
356
+
357
+ class Education < Field
358
+
359
+ attr_accessor :degree, :school, :date_range, :display
360
+
361
+ def initialize(params={})
362
+ super params
363
+ @degree = params[:degree]
364
+ @school = params[:school]
365
+ @date_range = params[:date_range]
366
+ @display = params[:display]
367
+ end
368
+
369
+ def to_hash
370
+ {degree: @degree, school: @school, date_range: @date_range ? @date_range.to_hash : nil}
371
+ .reject { |_, value| value.nil? }
372
+ end
373
+
374
+ def to_s
375
+ return @display if @display
376
+
377
+ if @degree && @school
378
+ s = @degree + ' from ' + @school
379
+ else
380
+ s = @degree || @school
381
+ end
382
+
383
+ if s && @date_range
384
+ range = @date_range.years_range
385
+ s += ' (%d-%d)' % [range[0], range[1]]
386
+ end
387
+
388
+ s ? Pipl::Utils.to_utf8(s) : ''
389
+ end
390
+
391
+ end
392
+
393
+
394
+ class Image < Field
395
+
396
+ attr_accessor :url, :thumbnail_token
397
+
398
+ def initialize(params={})
399
+ super params
400
+ @url = params[:url]
401
+ @thumbnail_token = params[:thumbnail_token]
402
+ end
403
+
404
+ def thumbnail_url(params={})
405
+ return unless @thumbnail_token
406
+
407
+ opts = {width: 100, height: 100, favicon: true, zoom_face: true, use_https: false}.merge(params)
408
+ schema = opts.delete(:use_https) ? 'https': 'http'
409
+ tokens = @thumbnail_token.gsub(/&dsid=.*/,'')
410
+ tokens += ',' + opts.delete(:fallback).thumbnail_token.gsub(/&dsid=.*/,'') if opts[:fallback]
411
+ query_params = ["tokens=#{tokens}"] + opts.map { |k, v| "#{k}=#{v}" unless v.nil? }
412
+ "#{schema}://thumb.pipl.com/image?#{query_params.compact.join('&')}"
413
+ end
414
+
415
+ end
416
+
417
+
418
+ class Username < Field
419
+
420
+ attr_accessor :content
421
+
422
+ def initialize(params={})
423
+ super params
424
+ @content = params[:content]
425
+ end
426
+
427
+ def to_hash
428
+ {content: @content} if @content
429
+ end
430
+
431
+ def is_searchable?
432
+ !@content.nil? && Pipl::Utils.alnum_chars(@content).length > 2
433
+ end
434
+
435
+ end
436
+
437
+
438
+ class UserID < Username
439
+
440
+ def is_searchable?
441
+ ! /.+@.+/.match(@content).nil?
442
+ end
443
+
444
+ end
445
+
446
+
447
+ class DOB < Field
448
+
449
+ attr_accessor :date_range, :display
450
+
451
+ def initialize(params={})
452
+ super params
453
+ @date_range = params[:date_range]
454
+ @display = params[:display]
455
+ end
456
+
457
+ def self.from_birth_year(birth_year)
458
+ raise ArgumentError.new('birth_year must be positive') unless birth_year > 0
459
+ self.new({date_range: DateRange.from_years_range(birth_year, birth_year)})
460
+ end
461
+
462
+ def self.from_birth_date(birth_date)
463
+ raise ArgumentError.new('birth_date can\'t be in the future') unless birth_date <= Date.today
464
+ self.new({date_range: DateRange.new(birth_date, birth_date)})
465
+ end
466
+
467
+ def self.from_age(age)
468
+ self.from_age_range(age, age)
469
+ end
470
+
471
+ def self.from_age_range(start_age, end_age)
472
+ raise ArgumentError.new('start_age and end_age can\'t be negative') if start_age < 0 || end_age < 0
473
+
474
+ if start_age > end_age
475
+ start_age, end_age = end_age, start_age
476
+ end
477
+
478
+ today = Date.today
479
+ start_date = today << end_age * 12
480
+ start_date = start_date - 1
481
+ end_date = today << start_age * 12
482
+ self.new({date_range: Pipl::DateRange.new(start_date, end_date)})
483
+ end
484
+
485
+ def to_s
486
+ @display || Pipl::Utils.to_utf8(age.to_s)
487
+ end
488
+
489
+ def age
490
+ unless @date_range.nil?
491
+ dob = @date_range.middle
492
+ today = Date.today
493
+ diff = today.year - dob.year
494
+ diff = diff - 1 if dob.month > today.month || (dob.month >= today.month && dob.day > today.day)
495
+ diff
496
+ end
497
+ end
498
+
499
+ def age_range
500
+ if @date_range
501
+ return [self.age, self.age] unless @date_range.start && @date_range.end
502
+ start_age = DOB.new({date_range: Pipl::DateRange.new(@date_range.start, @date_range.start)}).age
503
+ end_age = DOB.new({date_range: Pipl::DateRange.new(@date_range.end, @date_range.end)}).age
504
+ return end_age, start_age
505
+ else
506
+ return nil, nil
507
+ end
508
+ end
509
+
510
+ def to_hash
511
+ {date_range: @date_range.to_hash} if @date_range
512
+ end
513
+
514
+ def is_searchable?
515
+ ! @date_range.nil?
516
+ end
517
+
518
+ end
519
+
520
+
521
+ class Url < Field
522
+ # @!attribute url
523
+ # @return [String] Actual Url
524
+ # @!attribute category
525
+ # @return [String] Category of the domain
526
+ # Possible values are:
527
+ # background_reports
528
+ # contact_details
529
+ # email_address
530
+ # media
531
+ # personal_profiles
532
+ # professional_and_business
533
+ # public_records
534
+ # publications
535
+ # school_and_classmates
536
+ # web_pages
537
+ # @!attribute domain
538
+ # @return [String] Canonical domain of the url
539
+ # @!attribute name
540
+ # @return [String] Name of the website hosting the url
541
+ # @!attribute sponsored
542
+ # @return [Boolean] Indicate if this url comes from a sponsored data source
543
+ # @!attribute sponsored
544
+ # @return [String] Unique identifier of this url
545
+
546
+ attr_accessor :url, :category, :domain, :name, :sponsored, :source_id
547
+
548
+ def initialize(params={})
549
+ super params
550
+ @url = params[:url]
551
+ @category = params[:category]
552
+ @domain = params[:domain]
553
+ @name = params[:name]
554
+ @sponsored = params[:sponsored]
555
+ @source_id = params[:source_id]
556
+ end
557
+
558
+ def self.extra_metadata
559
+ [:category, :domain, :name, :sponsored, :source_id]
560
+ end
561
+
562
+ def is_searchable?
563
+ ! @url.to_s.empty?
564
+ end
565
+
566
+ def to_hash
567
+ {url: @url} if @url
568
+ end
569
+
570
+ end
571
+
572
+
573
+ class Gender < Field
574
+
575
+ attr_accessor :content
576
+
577
+ def initialize(params={})
578
+ super params
579
+ @content = params[:content]
580
+ end
581
+
582
+ def to_s
583
+ Pipl::Utils.titleize @content if @content
584
+ end
585
+
586
+ def to_hash
587
+ {content: @content} if @content
588
+ end
589
+
590
+ end
591
+
592
+
593
+ class Ethnicity < Field
594
+
595
+ # @!attribute content
596
+ # @return [String] Ethnicity name based on the U.S Census Bureau.
597
+ # Possible values are:
598
+ # white
599
+ # black
600
+ # american_indian
601
+ # alaska_native
602
+ # asian_indian
603
+ # chinese
604
+ # filipino
605
+ # other_asian
606
+ # japanese
607
+ # korean
608
+ # vietnamese
609
+ # native_hawaiian
610
+ # guamanian
611
+ # chamorro
612
+ # samoan
613
+ # other_pacific_islander
614
+ # other
615
+
616
+ attr_accessor :content
617
+
618
+ def initialize(params={})
619
+ super params
620
+ @content = params[:content]
621
+ end
622
+
623
+ def to_s
624
+ Pipl::Utils.titleize @content.gsub(/_/, ' ') if @content
625
+ end
626
+
627
+ end
628
+
629
+
630
+ class Language < Field
631
+
632
+ attr_accessor :language, :region, :display
633
+
634
+ def initialize(params={})
635
+ super params
636
+ @language = params[:language]
637
+ @region = params[:region]
638
+ @display = params[:display]
639
+ end
640
+
641
+ def to_s
642
+ return @display if @display
643
+ return "#{@language}_#{@region}" if @language && @region
644
+ return @language if @language && ! @language.empty?
645
+ @region
646
+ end
647
+
648
+ end
649
+
650
+
651
+ class OriginCountry < Field
652
+
653
+ attr_accessor :country
654
+
655
+ def initialize(params={})
656
+ super params
657
+ @country = params[:country]
658
+ end
659
+
660
+ def to_s
661
+ Pipl::COUNTRIES[@country.upcase.to_sym] if @country
662
+ end
663
+
664
+ end
665
+
666
+
667
+ class Tag < Field
668
+
669
+ attr_accessor :content, :classification
670
+
671
+ def initialize(params={})
672
+ super params
673
+ @content = params[:content]
674
+ @classification = params[:classification]
675
+ end
676
+
677
+ def self.extra_metadata
678
+ [:classification]
679
+ end
680
+
681
+ def to_s
682
+ @content
683
+ end
684
+
685
+ end
686
+
687
+
688
+ class DateRange
689
+
690
+ attr_reader :start, :end
691
+
692
+ def initialize(start, end_)
693
+ @start = start
694
+ @end = end_
695
+ if @start && @end && @start > @end
696
+ @start, @end = @end, @start
697
+ end
698
+ end
699
+
700
+ # def ==(other)
701
+ # other.instance_of?(self.class) && inspect == other.inspect
702
+ # end
703
+ #
704
+ # alias_method :eql?, :==
705
+
706
+ def is_exact?
707
+ @start && @end && @start == @end
708
+ end
709
+
710
+ def middle
711
+ @start && @end ? @start + ((@end - @start) / 2) : @start || @end
712
+ end
713
+
714
+ def years_range
715
+ [@start.year, @end.year] if @start && @end
716
+ end
717
+
718
+ def self.from_years_range(start_year, end_year)
719
+ self.new(Date.new(start_year, 1, 1), Date.new(end_year, 12, 31))
720
+ end
721
+
722
+ def self.from_hash(h)
723
+ start_, end_ = h[:start], h[:end]
724
+ initializing_start = start_ ? Date.strptime(start_, Pipl::DATE_FORMAT) : nil
725
+ initializing_end = end_ ? Date.strptime(end_, Pipl::DATE_FORMAT) : nil
726
+ self.new(initializing_start, initializing_end)
727
+ end
728
+
729
+ def to_hash
730
+ h = {}
731
+ h[:start] = @start.strftime(Pipl::DATE_FORMAT) if @start
732
+ h[:end] = @end.strftime(Pipl::DATE_FORMAT) if @end
733
+ h
734
+ end
735
+ end
736
+
737
+ class Vehicle < Field
738
+ attr_accessor :vin, :year, :make, :model, :color, :vehicle_type, :display
739
+
740
+ def initialize(params={})
741
+ @vin = params[:vin]
742
+ @year = params[:year]
743
+ @make = params[:make]
744
+ @model = params[:model]
745
+ @color = params[:color]
746
+ @vehicle_type = params[:vehicle_type]
747
+ @display = params[:display]
748
+ end
749
+
750
+ def to_hash
751
+ {vin: @vin, year: @year, make: @make, model: @model, color: @color, vehicle_type: @vehicle_type}
752
+ .reject { |_, value| value.nil? }
753
+ end
754
+
755
+ def validate_vin(vin)
756
+ vin_valid = true
757
+ if vin
758
+ vin_valid = vin.length == 17 &&
759
+ !(vin.downcase.chars.to_set & %w[i o q]).any? &&
760
+ !%w[u z 0].include?(vin[9].downcase) &&
761
+ vin.match?(/\A\w+\z/)
762
+ vin_valid &&= validate_vin_checksum(vin) if vin_valid
763
+ end
764
+ vin_valid
765
+ end
766
+
767
+ def validate_vin_checksum(vin)
768
+ vin = vin.downcase
769
+ check_digit = vin[8]
770
+
771
+ return false if check_digit.nil? # Handle the case when check_digit is nil
772
+
773
+ replace_map = {
774
+ "1" => ["a", "j"],
775
+ "2" => ["b", "k", "s"],
776
+ "3" => ["c", "l", "t"],
777
+ "4" => ["d", "m", "u"],
778
+ "5" => ["e", "n", "v"],
779
+ "6" => ["f", "w"],
780
+ "7" => ["g", "p", "x"],
781
+ "8" => ["h", "y"],
782
+ "9" => ["r", "z"]
783
+ }
784
+ positional_weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
785
+
786
+ replace_map.each do |digit, replacements|
787
+ replacements.each do |c|
788
+ vin.gsub!(c, digit)
789
+ end
790
+ end
791
+
792
+ checksum = vin.chars.each_with_index.reject { |_, i| i == 8 }.sum { |num, i| num.to_i * positional_weights[i] } % 11
793
+
794
+ checksum = 'x' if checksum == 10
795
+ checksum.to_s == check_digit.to_s # Convert check_digit to string for comparison
796
+ end
797
+
798
+ def is_searchable?
799
+ validate_vin(@vin)
800
+ end
801
+
802
+ end
803
+
804
+ end