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   325684 use strict;
  1         2  
  1         27  
4 1     1   5 use warnings;
  1         1  
  1         34  
5             use Exporter::Easy (
6 1         8 OK => [ qw/decode_vrm/ ],
7 1     1   710 );
  1         1546  
8              
9 1     1   148 use DateTime;
  1         2  
  1         1033  
10              
11             our $VERSION = '0.03';
12              
13             sub _normalise_vrm($) {
14 29     29   36 my ($vrm) = @_;
15 29         111 $vrm =~ s/\s//g;
16 29         47 $vrm =~ tr/a-z/A-Z/;
17 29         54 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 28068 my ($vrm) = @_;
84 29         64 $vrm = _normalise_vrm($vrm);
85 29 100       131 if ($vrm =~ /^[A-Z]{2}([0-9]{2})[A-Z]{3}$/) {
    100          
    100          
86             # The normal case
87 17         34 my ($start_year, $start_month) = _split_age_numbers($1);
88 17 100       43 return undef unless defined $start_year;
89 16         61 my $start_date = DateTime->new(year => $start_year, month => $start_month, day => 1);
90 16         3161 my $e = $start_date->clone->add(months => 5);
91 16         8577 my $end_date = DateTime->last_day_of_month(year => $e->year, month => $e->month);
92             return {
93 16         2535 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         15 return _resolve_letter_mark($PREFIX_TABLE, $1);
99             }
100             elsif ($vrm =~ /^[A-Z]{3}[0-9]{1,3}([A-Z])$/) {
101 5         12 return _resolve_letter_mark($SUFFIX_TABLE, $1);
102             }
103             # No patterns matched, can't parse this type of VRM
104 1         7 return undef;
105             }
106              
107             sub _split_age_numbers {
108 17     17   36 my ($age_pair) = @_;
109             # Special cases
110 17 100       66 if ($age_pair eq '50') {
    100          
    100          
111 2         6 return (2050, 3);
112             }
113             elsif ($age_pair eq '00') {
114 2         5 return (2050, 9);
115             }
116             elsif ($age_pair eq '01') {
117 1         2 return undef;
118             }
119              
120             # Usual case
121 12         34 my ($month_id, $year_id) = split(//, $age_pair);
122 12 100       36 my $year_tens = ($month_id < 5) ? $month_id : ($month_id - 5);
123 12         14 my $year_units = $year_id;
124 12         21 my $start_year = 2000 + ($year_tens * 10) + $year_units;
125 12 100       21 my $start_month = ($month_id < 5) ? 3 : 9;
126 12         23 return ($start_year, $start_month);
127             }
128              
129             sub _start_of_month {
130 10     10   32 return DateTime->new(@_, day => 1);
131             }
132              
133             sub _ym {
134 20     20   26 my ($y, $m) = @_;
135 20         58 return (year => $y, month => $m);
136             }
137              
138             sub _resolve_letter_mark {
139 11     11   20 my ($table, $letter) = @_;
140 11         20 my $pair = $table->{$letter};
141 11 100       30 return undef unless defined $pair;
142 10         18 my ($start_pair, $end_pair) = @$pair;
143             return {
144 10         18 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   10 my ($pair) = @_;
151 10         24 return _start_of_month(_ym(@$pair));
152             }
153              
154             sub _resolve_end_pair($) {
155 10     10   1698 my ($pair) = @_;
156 10         19 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 SUPPORT
207              
208             If you require assistance, support, or further development of this software, please contact OpusVL using the details below:
209              
210             Telephone: +44 (0)1788 298 410
211              
212             Email: community@opusvl.com
213              
214             Web: http://opusvl.com
215              
216             =head1 COPYRIGHT & LICENSE
217              
218             Copyright (C) 2015 Opus Vision Limited
219              
220             This is free software; you can redistribute it and/or modify it under the
221             same terms as the Perl 5 programming language system itself.
222              
223             =cut
224