File Coverage

blib/lib/Data/Validate/VIN.pm
Criterion Covered Total %
statement 197 200 98.5
branch 44 56 78.5
condition 3 3 100.0
subroutine 17 17 100.0
pod 4 4 100.0
total 265 280 94.6


line stmt bran cond sub pod time code
1             package Data::Validate::VIN;
2              
3 2     2   137252 use 5.008;
  2         18  
4 2     2   10 use strict;
  2         4  
  2         42  
5 2     2   10 use warnings;
  2         4  
  2         5335  
6             #use Carp;
7              
8             our $VERSION = '0.05';
9              
10             sub new {
11 12     12 1 7322 my ( $class, $vin ) = @_;
12              
13 12         27 my $self = bless {}, $class;
14              
15 12         49 $self->{_allowed} = qr/[A-HJ-NPR-Z0-9]/;
16 12         25 $self->{errors} = [];
17              
18             # this next one might add to $self->{errors}, which is fine
19 12         28 $self->{vin} = $self->_checkVIN($vin);
20              
21             # we won't process the wmi, vds or vis if the vin check returned errors
22 12 100       17 unless ( scalar( @{ $self->{errors} } ) > 0 ) {
  12         34  
23 3         11 $self->{wmi} = $self->_checkWMI( $self->{vin} );
24 3         12 $self->{vds} = $self->_checkVDS( $self->{vin} );
25 3         11 $self->{vis} = $self->_checkVIS( $self->{vin} );
26 3         9 $self->{checkdigit} = $self->_checkCheckDigit( $self->{vin} );
27             }
28              
29             $self->{valid} =
30 12 100       20 scalar( @{ $self->{errors} } > 0 )
  12         34  
31             ? undef
32             : 1;
33              
34 12         36 return $self;
35             }
36              
37             sub valid {
38 3     3 1 708 my ($self) = @_;
39              
40             defined $self->{valid}
41 3 100       24 ? return 1
42             : return;
43             }
44              
45             sub errors {
46 12     12 1 4745 my ($self) = @_;
47              
48 12         49 scalar( @{ $self->{errors} } > 0 )
49             ? return $self->{errors}
50 12 100       19 : return [];
51             }
52              
53             sub get {
54 24     24 1 13055 my ( $self, $wanted ) = @_;
55              
56 24 50       163 if ( $wanted =~ /wmi|vds|vis|vin|checkdigit|country|year/i ) {
57 24 100       110 if ( $wanted =~ /vin|checkdigit/i ) {
    100          
    50          
    50          
58             defined $self->{$wanted}
59 8 100       49 ? return $self->{$wanted}
60             : return;
61             }
62             elsif ( $wanted =~ /wmi|vds|vis/i ) {
63             defined $self->{$wanted}->{$wanted}
64 12 100       78 ? return $self->{$wanted}->{$wanted}
65             : return;
66             }
67             elsif ( $wanted =~ /country/i ) {
68             defined $self->{wmi}->{$wanted}
69 0 0       0 ? return $self->{wmi}->{$wanted}
70             : return;
71             }
72             elsif ( $wanted =~ /year/i ) {
73             defined $self->{vis}->{$wanted}
74 4 100       26 ? return $self->{vis}->{$wanted}
75             : return;
76             }
77             else {
78 0         0 return;
79             }
80             }
81 0         0 return;
82             }
83              
84             sub _checkVIN {
85 12     12   26 my ( $self, $_vin ) = @_;
86              
87 12 100 100     80 if ( not defined $_vin or $_vin =~ /^$/ ) {
88 2         8 $self->_trackError("No VIN supplied");
89 2         4 return;
90             }
91              
92 10         19 chomp($_vin);
93              
94             $_vin = $self->_checkCharacters(
95             wanted => $self->{_allowed},
96 10         40 unwanted => qr/[IOQ]/,
97             toCheck => $_vin,
98             section => 'VIN'
99             );
100              
101 10 100       35 if ( length($_vin) != 17 ) {
102 6         27 my $err = sprintf( "%- 17s", $_vin ) . " is not the expected length";
103 6         14 $self->_trackError($err);
104             }
105              
106 10         20 return $_vin;
107             }
108              
109             sub _checkWMI {
110 3     3   7 my ( $self, $_vin ) = @_;
111              
112             my $wmi = {
113             wmi => $self->_checkCharacters(
114             wanted => $self->{_allowed},
115 3         17 unwanted => qr/[IOQ]/,
116             toCheck => substr( $_vin, 0, 2 ),
117             section => 'WMI'
118             ),
119             };
120              
121             # load known valid WMIs
122 3         10 my $_allowed = $self->_loadWMI();
123              
124             defined $_allowed->{ $wmi->{wmi} }
125             ? $wmi->{country} = $_allowed->{ $wmi->{wmi} }
126 3 100       18 : $self->_trackError("Unknown WMI: $wmi->{wmi}");
127              
128 3         217 return $wmi;
129             }
130              
131             sub _checkCharacters {
132 19     19   82 my ( $self, %args ) = @_;
133              
134             # wanted unwanted toCheck section
135              
136 19         46 my $checked = uc($args{toCheck});
137 19         65 my @checked = split(q{}, $checked); ## char array
138 19         29 my @illegal;
139              
140 19         50 for (my $i = 0; $i < @checked; $i++) {
141 155 100       741 unless ($checked[$i] =~ /^$args{wanted}+$/) {
142 8         26 push @illegal, $checked[$i];
143             }
144             }
145              
146 19 100       41 if (@illegal) {
147 5         18 my $err = "Illegal characters in " . $args{section} . ': ' . join(q{}, @illegal);
148 5         13 $self->_trackError($err);
149             }
150              
151 19         77 return $checked;
152             }
153              
154             sub _loadWMI {
155 3     3   9 my ($self) = @_;
156              
157 3         32 my %wmi;
158              
159 3         17 $self->_loadCountry( 'AA', 'AH', 'South Africa', \%wmi );
160 3         10 $self->_loadCountry( 'AJ', 'AN', 'Ivory Coast', \%wmi );
161 3         9 $self->_loadCountry( 'BA', 'BE', 'Angola', \%wmi );
162 3         10 $self->_loadCountry( 'BF', 'BK', 'Kenya', \%wmi );
163 3         8 $self->_loadCountry( 'BL', 'BR', 'Tanzania', \%wmi );
164 3         9 $self->_loadCountry( 'CA', 'CE', 'Benin', \%wmi );
165 3         11 $self->_loadCountry( 'CF', 'CK', 'Madagascar', \%wmi );
166 3         9 $self->_loadCountry( 'CL', 'CR', 'Tunisia', \%wmi );
167 3         9 $self->_loadCountry( 'DA', 'DE', 'Egypt', \%wmi );
168 3         10 $self->_loadCountry( 'DF', 'DK', 'Morocco', \%wmi );
169 3         9 $self->_loadCountry( 'DL', 'DR', 'Zambia', \%wmi );
170 3         9 $self->_loadCountry( 'EA', 'EE', 'Ethiopia', \%wmi );
171 3         11 $self->_loadCountry( 'EF', 'EK', 'Mozambique', \%wmi );
172 3         11 $self->_loadCountry( 'FA', 'FE', 'Ghana', \%wmi );
173 3         11 $self->_loadCountry( 'FF', 'FK', 'Nigeria', \%wmi );
174 3         10 $self->_loadCountry( 'JA', 'JT', 'Japan', \%wmi );
175 3         8 $self->_loadCountry( 'KA', 'KE', 'Sri Lanka', \%wmi );
176 3         7 $self->_loadCountry( 'KF', 'KK', 'Israel', \%wmi );
177 3         9 $self->_loadCountry( 'KL', 'KR', 'Korea (South)', \%wmi );
178 3         9 $self->_loadCountry( 'LA', 'L0', 'China', \%wmi );
179 3         9 $self->_loadCountry( 'MA', 'ME', 'India', \%wmi );
180 3         10 $self->_loadCountry( 'MF', 'MK', 'Indonesia', \%wmi );
181 3         11 $self->_loadCountry( 'ML', 'MR', 'Thailand', \%wmi );
182 3         18 $self->_loadCountry( 'NF', 'NK', 'Pakistan', \%wmi );
183 3         11 $self->_loadCountry( 'NL', 'NR', 'Turkey', \%wmi );
184 3         10 $self->_loadCountry( 'PA', 'PE', 'Philippines', \%wmi );
185 3         13 $self->_loadCountry( 'PF', 'PK', 'Singapore', \%wmi );
186 3         12 $self->_loadCountry( 'PL', 'PR', 'Malaysia', \%wmi );
187 3         11 $self->_loadCountry( 'RA', 'RE', 'United Arab Emirates', \%wmi );
188 3         9 $self->_loadCountry( 'RF', 'RK', 'Taiwan', \%wmi );
189 3         9 $self->_loadCountry( 'RL', 'RR', 'Vietnam', \%wmi );
190 3         13 $self->_loadCountry( 'SA', 'SM', 'United Kingdom', \%wmi );
191 3         9 $self->_loadCountry( 'SN', 'ST', 'Germany', \%wmi );
192 3         10 $self->_loadCountry( 'SU', 'SZ', 'Poland', \%wmi );
193 3         10 $self->_loadCountry( 'S1', 'S4', 'Latvia', \%wmi );
194 3         9 $self->_loadCountry( 'TA', 'TH', 'Switzerland', \%wmi );
195 3         9 $self->_loadCountry( 'TJ', 'TP', 'Czech Republic', \%wmi );
196 3         9 $self->_loadCountry( 'TR', 'TV', 'Hungary', \%wmi );
197 3         9 $self->_loadCountry( 'TW', 'T1', 'Portugal', \%wmi );
198 3         9 $self->_loadCountry( 'UH', 'UM', 'Denmark', \%wmi );
199 3         8 $self->_loadCountry( 'UN', 'UT', 'Ireland', \%wmi );
200 3         9 $self->_loadCountry( 'UU', 'UZ', 'Romania', \%wmi );
201 3         9 $self->_loadCountry( 'U5', 'U7', 'Slovakia', \%wmi );
202 3         10 $self->_loadCountry( 'VA', 'VE', 'Austria', \%wmi );
203 3         9 $self->_loadCountry( 'VF', 'VR', 'France', \%wmi );
204 3         9 $self->_loadCountry( 'VS', 'VW', 'Spain', \%wmi );
205 3         9 $self->_loadCountry( 'VX', 'V2', 'Serbia', \%wmi );
206 3         10 $self->_loadCountry( 'V3', 'V5', 'Croatia', \%wmi );
207 3         8 $self->_loadCountry( 'V6', 'V0', 'Estonia', \%wmi );
208 3         10 $self->_loadCountry( 'WA', 'W0', 'Germany', \%wmi );
209 3         9 $self->_loadCountry( 'XA', 'XE', 'Bulgaria', \%wmi );
210 3         9 $self->_loadCountry( 'XF', 'XK', 'Greece', \%wmi );
211 3         10 $self->_loadCountry( 'XL', 'XR', 'Netherlands', \%wmi );
212 3         8 $self->_loadCountry( 'XS', 'XW', 'USSR', \%wmi );
213 3         9 $self->_loadCountry( 'XX', 'X2', 'Luxembourg', \%wmi );
214 3         10 $self->_loadCountry( 'X3', 'X0', 'Russia', \%wmi );
215 3         21 $self->_loadCountry( 'YA', 'YE', 'Belgium', \%wmi );
216 3         8 $self->_loadCountry( 'YF', 'YK', 'Finland', \%wmi );
217 3         11 $self->_loadCountry( 'YL', 'YR', 'Malta', \%wmi );
218 3         9 $self->_loadCountry( 'YS', 'YW', 'Sweden', \%wmi );
219 3         8 $self->_loadCountry( 'YX', 'Y2', 'Norway', \%wmi );
220 3         9 $self->_loadCountry( 'Y3', 'Y5', 'Belarus', \%wmi );
221 3         9 $self->_loadCountry( 'Y6', 'Y0', 'Ukraine', \%wmi );
222 3         8 $self->_loadCountry( 'ZA', 'ZR', 'Italy', \%wmi );
223 3         12 $self->_loadCountry( 'ZX', 'Z2', 'Slovenia', \%wmi );
224 3         18 $self->_loadCountry( 'Z3', 'Z5', 'Lithuania', \%wmi );
225 3         11 $self->_loadCountry( '1A', '10', 'United States', \%wmi );
226 3         10 $self->_loadCountry( '2A', '20', 'Canada', \%wmi );
227 3         11 $self->_loadCountry( '3A', '3W', 'Mexico', \%wmi );
228 3         18 $self->_loadCountry( '3X', '37', 'Costa Rica', \%wmi );
229 3         10 $self->_loadCountry( '38', '30', 'Cayman Islands', \%wmi );
230 3         8 $self->_loadCountry( '4A', '40', 'United States', \%wmi );
231 3         14 $self->_loadCountry( '5A', '50', 'United States', \%wmi );
232 3         13 $self->_loadCountry( '6A', '6W', 'Australia', \%wmi );
233 3         14 $self->_loadCountry( '7A', '7E', 'New Zealand', \%wmi );
234 3         8 $self->_loadCountry( '8A', '8E', 'Argentina', \%wmi );
235 3         8 $self->_loadCountry( '8F', '8K', 'Chile', \%wmi );
236 3         11 $self->_loadCountry( '8L', '8R', 'Ecuador', \%wmi );
237 3         8 $self->_loadCountry( '8S', '8W', 'Peru', \%wmi );
238 3         9 $self->_loadCountry( '8X', '82', 'Venezuela', \%wmi );
239 3         7 $self->_loadCountry( '9A', '9E', 'Brazil', \%wmi );
240 3         8 $self->_loadCountry( '9F', '9K', 'Colombia', \%wmi );
241 3         9 $self->_loadCountry( '9L', '9R', 'Paraguay', \%wmi );
242 3         11 $self->_loadCountry( '9S', '9W', 'Uruguay', \%wmi );
243 3         9 $self->_loadCountry( '9X', '92', 'Trinidad & Tobago', \%wmi );
244 3         9 $self->_loadCountry( '93', '99', 'Brazil', \%wmi );
245              
246 3         8 return \%wmi;
247             }
248              
249             sub _checkVDS {
250 3     3   8 my ( $self, $_vin ) = @_;
251              
252             my $vds = {
253             vds => length($_vin) == 17
254             ? $self->_checkCharacters(
255             wanted => $self->{_allowed},
256 3 50       28 unwanted => qr/[IOQ]/,
257             toCheck => substr( $_vin, 3, 6 ),
258             section => 'VDS'
259             )
260             : undef
261             };
262              
263 3         15 return $vds;
264             }
265              
266             sub _checkVIS {
267 3     3   10 my ( $self, $_vin ) = @_;
268              
269             my $vis = {
270             vis => length($_vin) == 17
271             ? $self->_checkCharacters(
272             wanted => $self->{_allowed},
273 3 50       21 unwanted => qr/[IOQ]/,
274             toCheck => substr( $_vin, 9, 8 ),
275             section => 'VIS'
276             )
277             : undef
278             };
279              
280 3 50       12 if ( defined $vis->{vis} ) {
281              
282 3         60 my %years = (
283             A => [ 1980, 2010 ],
284             L => [ 1990, 2020 ],
285             Y => [ 2000, 2030 ],
286             B => [ 1981, 2011 ],
287             M => [ 1991, 2021 ],
288             1 => [ 2001, 2031 ],
289             C => [ 1982, 2012 ],
290             N => [ 1992, 2022 ],
291             2 => [ 2002, 2032 ],
292             D => [ 1983, 2013 ],
293             P => [ 1993, 2023 ],
294             3 => [ 2003, 2033 ],
295             E => [ 1984, 2014 ],
296             R => [ 1994, 2024 ],
297             4 => [ 2004, 2034 ],
298             F => [ 1985, 2015 ],
299             S => [ 1995, 2025 ],
300             5 => [ 2005, 2035 ],
301             G => [ 1986, 2016 ],
302             T => [ 1996, 2026 ],
303             6 => [ 2006, 2036 ],
304             H => [ 1987, 2017 ],
305             V => [ 1997, 2027 ],
306             7 => [ 2007, 2037 ],
307             J => [ 1988, 2018 ],
308             W => [ 1998, 2028 ],
309             8 => [ 2008, 2038 ],
310             K => [ 1989, 2019 ],
311             X => [ 1999, 2029 ],
312             9 => [ 2009, 2039 ],
313             );
314              
315 3         10 my $yearDigit = substr( $vis->{vis}, 0, 1 );
316              
317             my $year =
318             defined $years{$yearDigit}
319 3 100       10 ? $years{$yearDigit}
320             : undef;
321              
322 3 100       7 if ($year) {
323 2         14 $vis->{year} = $year;
324             }
325             else {
326             $self->_trackError("Illegal character in 10th position: $yearDigit")
327 1 50       6 unless $vis->{year};
328             }
329             }
330              
331 3         8 return $vis;
332             }
333              
334             sub _checkCheckDigit {
335 3     3   7 my ( $self, $_vin ) = @_;
336              
337 3 50       9 return unless length($_vin) == 17;
338              
339 3         8 my $passedCheckDigit = substr( $_vin, 8, 1 );
340              
341 3         24 my %vals = (
342             A => 1,
343             B => 2,
344             C => 3,
345             D => 4,
346             E => 5,
347             F => 6,
348             G => 7,
349             H => 8,
350             J => 1,
351             K => 2,
352             L => 3,
353             M => 4,
354             N => 5,
355             P => 7,
356             R => 9,
357             S => 2,
358             T => 3,
359             U => 4,
360             V => 5,
361             W => 6,
362             X => 7,
363             Y => 8,
364             Z => 9
365             );
366              
367             # Add the numeric pieces
368             # these are worth face value
369 3         11 for ( 0 .. 9 ) {
370 30         54 $vals{$_} = $_;
371             }
372              
373 3         26 my %wghts = (
374             1 => 8,
375             2 => 7,
376             3 => 6,
377             4 => 5,
378             5 => 4,
379             6 => 3,
380             7 => 2,
381             8 => 10,
382             9 => 0,
383             10 => 9,
384             11 => 8,
385             12 => 7,
386             13 => 6,
387             14 => 5,
388             15 => 4,
389             16 => 3,
390             17 => 2
391             );
392              
393 3         26 my @vinbits = split( // => $_vin );
394              
395 3         44 my $sum;
396              
397 3         7 my $ind = 1;
398 3         7 for my $bit (@vinbits) {
399 51         83 $sum += $vals{$bit} * $wghts{$ind};
400 51         63 $ind++;
401             }
402              
403 3         7 my $calcCheckDigit = $sum % 11;
404 3 50       10 $calcCheckDigit = 'X'
405             if $calcCheckDigit == '10';
406              
407 3 100       41 $calcCheckDigit =~ /$passedCheckDigit/
408             ? return $calcCheckDigit
409             : $self->_trackError(
410             "Checkdigit mismatch; expected $calcCheckDigit, got $passedCheckDigit");
411              
412 2         14 return;
413             }
414              
415             sub _loadCountry {
416 258     258   569 my ( $self, $start, $end, $country, $store ) = @_;
417              
418 258         455 $store->{$start} = $country;
419              
420 258         456 until ( $start eq $end ) {
421 1752         3290 my @pieces = split( // => $start );
422 1752         3214 my $next = $self->_next( $pieces[1] );
423              
424 1752         2661 $start = $pieces[0] . $next;
425              
426 1752         4650 $store->{$start} = $country;
427             }
428              
429 258         415 return;
430             }
431              
432             sub _next {
433 1752     1752   2847 my ( $self, $current ) = @_;
434              
435 1752         6467 my @fields = qw{ A B C D E F G H J K L M N P R S T
436             U V W X Y Z 1 2 3 4 5 6 7 8 9 0 };
437              
438 1752         3445 my %order = map { ( $fields[$_], $_ ) } 0 .. scalar(@fields) - 1;
  57816         86459  
439              
440 1752         5503 my $max = scalar(@fields) - 1;
441              
442             my $next =
443             $order{$current} == $max
444             ? $fields[0]
445 1752 50       4020 : $fields[ $order{$current} + 1 ];
446              
447 1752         8053 return $next;
448             }
449              
450             sub _trackError {
451 17     17   32 my ( $self, $error ) = @_;
452              
453 17         27 push @{ $self->{errors} }, $error;
  17         35  
454              
455 17         39 return;
456             }
457              
458             1;
459             __END__