File Coverage

blib/lib/SSN/Validate.pm
Criterion Covered Total %
statement 101 107 94.3
branch 50 54 92.5
condition 13 17 76.4
subroutine 13 14 92.8
pod 7 9 77.7
total 184 201 91.5


line stmt bran cond sub pod time code
1             package SSN::Validate;
2              
3 10     10   46077 use 5.006;
  10         40  
  10         450  
4 10     10   60 use strict;
  10         23  
  10         370  
5 10     10   59 use warnings;
  10         17  
  10         13794  
6              
7             our $VERSION = '0.18';
8              
9             # Preloaded methods go here.
10              
11             # Data won't change, no need to _init() for every object
12             my $SSN = _init();
13              
14             ## "Within each area, the group number (middle two (2) digits)
15             ## range from 01 to 99 but are not assigned in consecutive
16             ## order. For administrative reasons, group numbers issued
17             ## first consist of the ODD numbers from 01 through 09 and
18             ## then EVEN numbers from 10 through 98, within each area
19             ## number allocated to a State. After all numbers in group 98
20             ## of a particular area have been issued, the EVEN Groups 02
21             ## through 08 are used, followed by ODD Groups 11 through 99."
22             ##
23             ## ODD - 01, 03, 05, 07, 09
24             ## EVEN - 10 to 98
25             ## EVEN - 02, 04, 06, 08
26             ## ODD - 11 to 99
27             my $GROUP_ORDER = [
28             '01', '03', '05', '07', '09', 10, 12, 14, 16, 18, 20, 22,
29             24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46,
30             48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,
31             72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94,
32             96, 98, '02', '04', '06', '08', 11, 13, 15, 17, 19, 21,
33             23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45,
34             47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69,
35             71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93,
36             95, 97, 99
37             ];
38              
39             my $ADVERTISING_SSN = [
40             '042103580',
41             '062360749',
42             '078051120',
43             '095073645',
44             128036045,
45             135016629,
46             141186941,
47             165167999,
48             165187999,
49             165207999,
50             165227999,
51             165247999,
52             189092294,
53             212097694,
54             212099999,
55             306302348,
56             308125070,
57             468288779,
58             549241889,
59             987654320,
60             987654321,
61             987654322,
62             987654323,
63             987654324,
64             987654325,
65             987654326,
66             987654327,
67             987654328,
68             987654329,
69             ];
70              
71             my $BAD_COMBO = [
72             55019,
73             58619,
74             58629,
75             58659,
76             58659,
77             58679..58699
78             ];
79              
80             sub new {
81 10     10 1 7375 my ( $proto, $args ) = @_;
82              
83 10   33     75 my $class = ref($proto) || $proto;
84 10         31 my $self = {};
85 10         29 bless( $self, $class );
86              
87 10         96 $self->{'SSN'} = $SSN;
88 10         29 $self->{'GROUP_ORDER'} = $GROUP_ORDER;
89 10         27 $self->{'AD_SSN'} = $ADVERTISING_SSN;
90 10         21 $self->{'BAD_COMBO'} = $BAD_COMBO;
91 10   100     73 $self->{'BAD_COMBO_IGNORE'} = $args->{'ignore_bad_combo'} || 0;
92              
93 10         40 return $self;
94             }
95              
96             sub valid_ssn {
97 84     84 1 34389 my ( $self, $ssn ) = @_;
98              
99 84         352 $ssn =~ s!\D!!g;
100              
101 84 100       230 if ( length $ssn != 9 ) {
102 1         3 $self->{'ERROR'} = "Bad SSN length";
103 1         5 return 0;
104             }
105              
106             # Check for known invalid SSNs
107             # Start with Advertising SSNs. The SSA suggests the range of
108             # 987-65-4320 thru 987-65-4329 but these have also been used
109             # in ads.
110              
111 83 100       378 if (in_array($ssn, $self->{'AD_SSN'})) {
112 29         48 $self->{'ERROR'} = 'Advertising SSN';
113 29         483 return 0;
114             }
115              
116 54         176 my $area = substr( $ssn, 0, 3 );
117 54         91 my $group = substr( $ssn, 3, 2 );
118 54         76 my $serial = substr( $ssn, 5, 4 );
119              
120             # Some groups are invalid with certain areas.
121             # Rhyme and reason are not a part of the SSA.
122              
123 54 100 100     246 if (!$self->{'BAD_COMBO_IGNORE'} && in_array($area . $group, $self->{'BAD_COMBO'})) {
124 9         13 $self->{'ERROR'} = 'Invalid area/group combo';
125 9         44 return 0;
126             }
127            
128 45 100       205 if ( !$self->valid_area($area) ) {
    100          
    100          
129 6         11 $self->{'ERROR'} = "Bad Area";
130 6         31 return 0;
131             }
132             elsif ( !$self->valid_group($ssn) ) {
133 6         14 $self->{'ERROR'} = "Bad Group";
134 6         34 return 0;
135             }
136             elsif ( !$self->valid_serial($serial) ) {
137 1         2 $self->{'ERROR'} = "Bad Serial";
138 1         6 return 0;
139             }
140             else {
141 32         206 return 1;
142             }
143              
144             }
145              
146             sub valid_area {
147 155     155 1 4726 my ( $self, $area ) = @_;
148              
149 155 50       381 $area = substr( $area, 0, 3) if length $area > 3;
150              
151 155 100       838 return exists $self->{'SSN'}->{$area}->{valid} ? 1 : 0;
152             }
153              
154             sub valid_group {
155 48     48 1 4240 my ( $self, $group ) = @_;
156              
157 48         109 $group =~ s!\D!!g;
158              
159             #if ( length $group == 9 ) {
160 48 100       124 if ( length $group > 2 ) {
161 45         87 my $area = substr( $group, 0, 3 );
162 45         77 $group = substr( $group, 3, 2 );
163 45 100       107 return 0 if $group eq '00';
164              
165 42 100 100     188 if (!$self->{'BAD_COMBO_IGNORE'} && in_array($area . $group, $self->{'BAD_COMBO'})) {
166 1         4 $self->{'ERROR'} = 'Invalid area/group combo';
167 1         5 return 0;
168             }
169              
170 41 100       157 if ( defined $self->{'SSN'}{$area}{'highgroup'} ) {
    100          
171             # We're igno
172 36 100 66     118 if ($self->{'BAD_COMBO_IGNORE'} && in_array($area . $group, $self->{'BAD_COMBO'})) {
173 8         29 return 1;
174             }
175              
176 28         84 return in_array( $group,
177             $self->get_group_range( $self->{'SSN'}{$area}{'highgroup'} ) );
178             }
179             elsif ( defined $self->{'SSN'}{$area}{'group_range'} ) {
180 4         14 return in_array( $group, $self->{'SSN'}{$area}{'group_range'} );
181             }
182             else {
183 1         4 return 1;
184             }
185              
186             }
187 3 100       24 return $group eq '00' ? 0 : 1;
188             }
189              
190             sub valid_serial {
191 33     33 1 53 my ( $self, $serial ) = @_;
192              
193 33 100       100 return $serial eq '0000' ? 0 : 1;
194             }
195              
196             sub get_state {
197 97     97 1 173 my ( $self, $ssn ) = @_;
198              
199 97         193 my $area = substr( $ssn, 0, 3 );
200              
201 97 100       215 if ( $self->valid_area($area) ) {
202 87 50       692 return defined $self->{'SSN'}->{$area}->{'state'}
203             ? $self->{'SSN'}->{$area}->{'state'} : '';
204             }
205             else {
206 10         50 return '';
207             }
208             }
209              
210             sub get_description {
211 0     0 1 0 my ( $self, $ssn ) = @_;
212              
213 0         0 my $area = substr( $ssn, 0, 3 );
214              
215 0 0       0 if ( $self->valid_area($area) ) {
216 0         0 return $self->{'SSN'}->{$area}->{'description'};
217             }
218             else {
219 0         0 return 0;
220             }
221             }
222              
223             ## given a high group number, generate a list of valid
224             ## group numbers using that wild and carazy SSA algorithm.
225             sub get_group_range {
226 28     28 0 54 my ( $self, $highgroup ) = @_;
227              
228 28         69 for ( my $i = 0 ; $i < 100 ; $i++ ) {
229 1815 100       8100 if (
230             sprintf( "%02d", $self->{'GROUP_ORDER'}[$i] ) ==
231             sprintf( "%02d", $highgroup ) )
232             {
233 28         164 return [ @{ $self->{'GROUP_ORDER'} }[ 0 .. $i + 1 ] ]; # array slice
  28         289  
234             }
235             }
236              
237 0         0 return [];
238             }
239              
240             sub in_array {
241 203     203 0 329 my ( $needle, $haystack ) = @_;
242              
243 203         325 foreach my $hay (@$haystack) {
244 4815 100       10457 return 1 if $hay == $needle;
245             }
246 129         487 return 0;
247             }
248              
249             sub _init {
250 10     10   20 my %by_ssn;
251              
252 10     10   71 no warnings 'once';
  10         20  
  10         5310  
253              
254             # parse data into memory...
255 10         67 while () {
256 8440         9278 chomp;
257              
258             # skip stuff that doesn't "look" like our data
259 8440 100       28650 next unless m/^[0-9]{3}/;
260              
261 8360 100       27728 if (/^(\d{3}),(\d{2})\-*(\d*)\D*$/) {
262 7570 100 66     35601 if ( !defined $3 || $3 eq '' ) {
263 7550         26086 $by_ssn{$1}->{'highgroup'} = $2;
264             }
265             else {
266 20 100       91 if ( defined $by_ssn{$1}->{'group_range'} ) {
267 10         27 push @{ $by_ssn{$1}->{'group_range'} }, ( $2 .. $3 );
  10         64  
268             }
269             else {
270 10         121 $by_ssn{$1}->{'group_range'} = [ $2 .. $3 ];
271             }
272             }
273 7570         21445 next;
274             }
275              
276 790         2768 my ( $numeric, $state_abbr, $description ) = split /\s+/, $_, 3;
277              
278             # deal with the numeric stuff...
279 790         1658 $numeric =~ s/[^0-9,-]//; # sanitize for fun
280              
281             # loop over , groups, if any...
282 790         1606 for my $group ( split ',', $numeric ) {
283              
284             # Skip over invalid ranges. Although they may be assigned
285             # if they are not yet issued, then no one has an area from
286             # it, so it is invalid by the SSA.
287             # May make a 'loose' bit to allow these to validate
288 790 100       4059 next if $description =~ /not yet issued/i;
289              
290             # pull apart hypened ranges
291 710         1488 my ( $min, $max ) = split '-', $group;
292              
293             # see whether a range to deal with exists...
294 710 100       1212 if ( defined $max ) {
295 610         1464 for my $number ( $min .. $max ) {
296 9290         53084 $by_ssn{$number} = {
297             'state' => $state_abbr,
298             'description' => $description,
299             'valid' => 1,
300             };
301             }
302             }
303             else {
304 100         827 $by_ssn{$min} = {
305             'state' => $state_abbr,
306             'description' => $description,
307             'valid' => 1,
308             };
309             }
310             }
311             }
312              
313 10         44 return \%by_ssn;
314             }
315              
316             # Autoload methods go after =cut, and are processed by the autosplit program.
317              
318             1;
319              
320             =pod
321              
322             =head1 NAME
323              
324             SSN::Validate - Perl extension to do SSN Validation
325              
326             =head1 SYNOPSIS
327              
328             use SSN::Validate;
329              
330             my $ssn = new SSN::Validate;
331              
332             or
333              
334             my $ssn = SSN::Validate->new({'ignore_bad_combo' => 1});
335              
336             if ($ssn->valid_ssn("123-45-6789")) {
337             print "It's a valid SSN";
338             }
339              
340             my $state = $ssn->get_state("123456789");
341             my $state = $ssn->get_state("123");
342              
343             print $ssn->valid_area('123') ? "Valid" : "Invalid";
344             print $ssn->valid_area('123-56-7890') ? "Valid" : "Invalid";
345              
346             =head1 DESCRIPTION
347              
348             This module is intented to do some Social Security Number validation (not
349             verification) beyond just seeing if it contains 9 digits and isn't all 0s. The
350             data is taken from the Social Security Admin. website, specifically:
351              
352             http://www.ssa.gov/foia/stateweb.html
353              
354             As of this initial version, SSNs are validated by ensuring it is 9 digits, the
355             area, group and serial are not all 0s, and that the area is within a valid
356             range used by the SSA.
357              
358             It will also return the state which the SSN was issues, if that data is known
359             (state of "??" for unknown states/regions).
360              
361             A SSN is formed by 3 parts, the area (A), group (G) and serial (S):
362              
363             AAAA-GG-SSSS
364              
365             =head2 METHODS
366              
367             =over 4
368              
369             =item new();
370              
371             You can pass an arg of 'ignore_bad_combo' as true if you wish to ignore
372             any defined bad AAAA-GG combinations. Things will be on the list until
373             I see otherwise on the SSA website or some other means of proof.
374              
375             =item valid_ssn($ssn);
376              
377             The SSN can be of any format (111-11-1111, 111111111, etc...). All non-digits
378             are stripped.
379              
380             This method will return true if it is valid, and false if it isn't. It
381             uses the below methods to check the validity of each section.
382              
383             =item valid_area($ssn);
384              
385             This will see if the area is valid by using the ranges in use by the SSA. You
386             can pass this method a full SSN, or just the 3 digit area.
387              
388             =item valid_group($group);
389              
390             Will make sure that the group isn't "00", as well as check the
391             AREA/GROUP combo for known invalid ones, and the SSA High Groups.
392              
393             If given a 2 digit GROUP, it will only make sure that that GROUP isn't
394             "00".
395              
396             If given a number in length above 2 digits, it will attempt to split
397             into an AREA and GROUP and do further validation.
398              
399             =item valid_serial($serial);
400              
401             This is currently only making sure the serial isn't "0000", and that's all it
402             will ever do. From my reading, "0000" is the only non-valid serial.
403              
404             This is also semi-useful right now, as it expects only 4 digits. Later it will
405             take both 4 digits or a full serial.
406              
407             =item get_state($ssn);
408              
409             You can give this a full SSN or 3 digit area. It will return the state, if
410             known, from where the given area is issued. Invalid areas will return
411             false.
412              
413             =item get_description($ssn);
414              
415             If there is a description associated with the state or region, this will return
416             it.. or will return an empty string.
417              
418             =back
419              
420             =head2 TODO
421              
422             * Change how the data is stored. I don't like how it is done now... but works.
423              
424             * Find out state(s) for areas which aren't known right now.
425              
426             * Automate this module almost as completely as possible for
427             distribution.
428              
429             * Consider SSN::Validate::SSDI for Social Security Death Index (SSDI)
430              
431             * Possibly change how data is stored (first on TODO), and provide my
432             extract script for people to run on their own. This way, maybe they
433             could update the SSA changes on their own, instead of being dependant
434             on the module for this, or having to update the module when the SSA
435             makes changes. I think I like this idea.
436              
437             =head2 EXPORT
438              
439             None by default.
440              
441             =head1 BUGS
442              
443             Please let me know if something doesn't work as expected.
444              
445             You can report bugs via the CPAN RT:
446             L
447              
448             If you are feeling nice, and would like quicker fixes, please provide a
449             diff against F and the appropriate test file(s). If you
450             are making something invalid which is currently valid, or vice versa,
451             please provide a reference to why the change is needed. Thanks!
452              
453             Patches Welcome!
454              
455             =head1 AUTHOR
456              
457             Kevin Meltzer, Ekmeltz@cpan.orgE
458              
459             =head1 LICENSE
460              
461             SSN::Validate is free software which you can redistribute and/or
462             modify it under the same terms as Perl itself.
463              
464             =head1 SEE ALSO
465              
466             L,
467             L,
468             L.
469              
470             =cut
471              
472             # store SSN information inside the script down here...
473             #
474             # format is simple, three bits of data separated by tabs:
475             # numeric_range state_abbr description
476             #
477             # Leave state_abbr empty if not applicable. numeric_range consits
478             # of three-digit numbers, with -'s to denote ranges and ,'s to denote
479             # series of numbers or ranges.
480             __DATA__