File Coverage

blib/lib/Data/VRM/GB.pm
Criterion Covered Total %
statement 51 51 100.0
branch 20 20 100.0
condition n/a
subroutine 12 12 100.0
pod 1 1 100.0
total 84 84 100.0


line stmt bran cond sub pod time code
1             package Data::VRM::GB;
2              
3 1     1   239433 use strict;
  1         4  
  1         26  
4 1     1   5 use warnings;
  1         2  
  1         32  
5             use Exporter::Easy (
6 1         7 OK => [ qw/decode_vrm/ ],
7 1     1   751 );
  1         1552  
8              
9 1     1   110 use DateTime;
  1         2  
  1         880  
10              
11             our $VERSION = '0.01'; # TRIAL
12              
13             sub _normalise_vrm($) {
14 29     29   37 my ($vrm) = @_;
15 29         105 $vrm =~ s/\s//g;
16 29         49 $vrm =~ tr/a-z/A-Z/;
17 29         56 return $vrm;
18             }
19              
20             my $SUFFIX_TABLE = {
21             A => [[1963, 2], [1963, 12]],
22             B => [[1964, 1], [1964, 12]],
23             C => [[1965, 1], [1965, 12]],
24             D => [[1966, 1], [1966, 12]],
25             E => [[1967, 1], [1967, 7]],
26             F => [[1967, 8], [1968, 7]],
27             G => [[1968, 8], [1969, 7]],
28             H => [[1969, 8], [1970, 7]],
29             # No I
30             J => [[1970, 8], [1971, 7]],
31             K => [[1971, 8], [1972, 7]],
32             L => [[1972, 8], [1973, 7]],
33             M => [[1973, 8], [1974, 7]],
34             N => [[1974, 8], [1975, 7]],
35             # No O
36             P => [[1975, 8], [1976, 7]],
37             # No Q
38             R => [[1976, 8], [1977, 7]],
39             S => [[1977, 8], [1978, 7]],
40             T => [[1978, 8], [1979, 7]],
41             # No U
42             V => [[1979, 8], [1980, 7]],
43             W => [[1980, 8], [1981, 7]],
44             X => [[1981, 7], [1982, 7]],
45             Y => [[1982, 8], [1983, 7]],
46             };
47              
48             my $PREFIX_TABLE = {
49             A => [[1983, 8], [1984, 7]],
50             B => [[1984, 8], [1985, 7]],
51             C => [[1985, 8], [1986, 7]],
52             D => [[1986, 8], [1987, 7]],
53             E => [[1987, 8], [1988, 7]],
54             F => [[1988, 8], [1989, 7]],
55             G => [[1989, 8], [1990, 7]],
56             H => [[1990, 8], [1991, 7]],
57             # There's no I
58             J => [[1991, 8], [1992, 7]],
59             K => [[1992, 8], [1993, 7]],
60             L => [[1993, 8], [1994, 7]],
61             M => [[1994, 8], [1995, 7]],
62             N => [[1995, 8], [1996, 7]],
63             # There's no O
64             P => [[1996, 8], [1997, 7]],
65             # There's no Q
66             R => [[1997, 8], [1998, 7]],
67             S => [[1998, 8], [1999, 2]],
68             T => [[1999, 3], [1999, 8]],
69             # There's no U
70             V => [[1999, 9], [2000, 2]],
71             W => [[2000, 3], [2000, 8]],
72             X => [[2000, 9], [2001, 2]],
73             Y => [[2001, 3], [2001, 8]],
74             # There's no Z
75             };
76              
77             # Apply this to end dates to push the time portion close to midnight the following day,
78             # to make the code that bit more tolerant if comparison is done with untruncated DateTime.
79             # The recommendation remains that the user should truncate their DateTime before comparing, however.
80             my $tolerant_end_date = sub { shift->set(hour => 23, minute => 59, second => 59) };
81              
82             sub decode_vrm($) {
83 29     29 1 37352 my ($vrm) = @_;
84 29         66 $vrm = _normalise_vrm($vrm);
85 29 100       361 if ($vrm =~ /^[A-Z]{2}([0-9]{2})[A-Z]{3}$/) {
    100          
    100          
86             # The normal case
87 17         37 my ($start_year, $start_month) = _split_age_numbers($1);
88 17 100       42 return undef unless defined $start_year;
89 16         59 my $start_date = DateTime->new(year => $start_year, month => $start_month, day => 1);
90 16         5592 my $e = $start_date->clone->add(months => 5);
91 16         9780 my $end_date = DateTime->last_day_of_month(year => $e->year, month => $e->month);
92             return {
93 16         2522 start_date => $start_date,
94             end_date => $end_date->$tolerant_end_date,
95             };
96             }
97             elsif ($vrm =~ /^([A-Z])[0-9]{1,3}[A-Z]{3}$/) {
98 6         13 return _resolve_letter_mark($PREFIX_TABLE, $1);
99             }
100             elsif ($vrm =~ /^[A-Z]{3}[0-9]{1,3}([A-Z])$/) {
101 5         11 return _resolve_letter_mark($SUFFIX_TABLE, $1);
102             }
103             # No patterns matched, can't parse this type of VRM
104 1         5 return undef;
105             }
106              
107             sub _split_age_numbers {
108 17     17   32 my ($age_pair) = @_;
109             # Special cases
110 17 100       62 if ($age_pair eq '50') {
    100          
    100          
111 2         5 return (2050, 3);
112             }
113             elsif ($age_pair eq '00') {
114 2         4 return (2050, 9);
115             }
116             elsif ($age_pair eq '01') {
117 1         3 return undef;
118             }
119              
120             # Usual case
121 12         36 my ($month_id, $year_id) = split(//, $age_pair);
122 12 100       45 my $year_tens = ($month_id < 5) ? $month_id : ($month_id - 5);
123 12         14 my $year_units = $year_id;
124 12         20 my $start_year = 2000 + ($year_tens * 10) + $year_units;
125 12 100       23 my $start_month = ($month_id < 5) ? 3 : 9;
126 12         26 return ($start_year, $start_month);
127             }
128              
129             sub _start_of_month {
130 10     10   34 return DateTime->new(@_, day => 1);
131             }
132              
133             sub _ym {
134 20     20   23 my ($y, $m) = @_;
135 20         63 return (year => $y, month => $m);
136             }
137              
138             sub _resolve_letter_mark {
139 11     11   22 my ($table, $letter) = @_;
140 11         22 my $pair = $table->{$letter};
141 11 100       29 return undef unless defined $pair;
142 10         14 my ($start_pair, $end_pair) = @$pair;
143             return {
144 10         19 start_date => _resolve_start_pair($start_pair),
145             end_date => _resolve_end_pair($end_pair)->$tolerant_end_date,
146             };
147             }
148              
149             sub _resolve_start_pair($) {
150 10     10   11 my ($pair) = @_;
151 10         21 return _start_of_month(_ym(@$pair));
152             }
153              
154             sub _resolve_end_pair($) {
155 10     10   1654 my ($pair) = @_;
156 10         23 return DateTime->last_day_of_month(_ym(@$pair));
157             }
158              
159              
160              
161             1;
162              
163             =head1 NAME
164              
165             Data::VRM::GB - Extract data about British vehicle registration marks
166              
167             =head1 DESCRIPTION
168              
169             This module allows you to get age information based on a vehicle registration
170             mark.
171              
172             =head1 SYNOPSIS
173              
174             use Data::VRM::GB qw/decode_vrm/;
175              
176             my $vd = decode_vrm('AB56 RST');
177             $vd->{start_date};
178             $vd->{end_date};
179              
180             =head1 LIMITATIONS
181              
182             The API is unstable - we haven't fully decided on the API and return data types yet.
183              
184             =head1 EXPORTS
185              
186             =head2 decode_vrm
187              
188             A function which takes a VRM as its first and only argument, and returns a
189             HASHREF with the keys C and C.
190             Each of those keys has as its value a DateTime object, truncated to the 'day'.
191              
192             If the registration mark couldn't be decoded to a date, either
193             because it's of an unrecognised format or is using a letter prefix that is
194             not understood, then it will return undef.
195              
196             Before comparing these dates with another DateTime, you must ensure you
197             truncate your DateTime to the day. If you have a time portion, you will
198             get errors creeping in.
199              
200             DateTime->compare(
201             decode_vrm('AB56 RST')->{end_date},
202             $your_dt->truncate(to => 'day')
203             );
204              
205              
206             =head1 COPYRIGHT & LICENSE
207              
208             Copyright (C) 2015 Opus Vision Limited
209              
210             This is free software; you can redistribute it and/or modify it under the
211             same terms as the Perl 5 programming language system itself.
212              
213             Telephone: C< +44 (0)1788 298 410 >
214              
215             Email: C< community@opusvl.com >
216              
217             =cut
218