File Coverage

blib/lib/Business/ISMN.pm
Criterion Covered Total %
statement 122 153 79.7
branch 26 46 56.5
condition 8 18 44.4
subroutine 28 36 77.7
pod 17 17 100.0
total 201 270 74.4


line stmt bran cond sub pod time code
1 2     2   55515 use 5.008;
  2         6  
2              
3             package Business::ISMN;
4 2     2   9 use strict;
  2         2  
  2         48  
5              
6 2         9 use subs qw(
7             _common_format _checksum is_valid_checksum
8             INVALID_PUBLISHER_CODE
9             BAD_CHECKSUM
10             GOOD_ISMN
11             BAD_ISMN
12 2     2   885 );
  2         37  
13 2     2   144 use vars qw( $debug %country_data $MAX_COUNTRY_CODE_LENGTH );
  2         3  
  2         115  
14              
15 2     2   9 use Carp qw(carp);
  2         4  
  2         80  
16 2     2   9 use Exporter qw(import);
  2         3  
  2         46  
17 2     2   9 use List::Util qw(sum);
  2         4  
  2         200  
18 2     2   724 use Tie::Cycle;
  2         1247  
  2         55  
19 2     2   633 use Business::ISMN::Data;
  2         5  
  2         1460  
20              
21             my $debug = 0;
22              
23             our @EXPORT_OK = qw(is_valid_checksum ean_to_ismn ismn_to_ean
24             INVALID_PUBLISHER_CODE BAD_CHECKSUM GOOD_ISMN BAD_ISMN);
25              
26             our $VERSION = '1.203';
27              
28 62     62   76 sub INVALID_PUBLISHER_CODE { -3 };
29 64     64   82 sub BAD_CHECKSUM { -1 };
30 183     183   382 sub GOOD_ISMN { 1 };
31 10     10   28 sub BAD_ISMN { 0 };
32              
33             my %Lengths = qw(
34             0 3
35             1 4
36             2 4
37             3 4
38             4 5
39             5 5
40             6 5
41             7 6
42             8 6
43             9 7
44             );
45              
46             sub new {
47 61     61 1 560 my( $class, $raw_ismn ) = @_;
48 61         88 my $common_data = _common_format $raw_ismn;
49 61 50       117 return unless defined $common_data;
50              
51 61         79 my $self = {};
52 61         83 bless $self, $class;
53              
54 61         91 $self->{'ismn'} = $common_data;
55 61         92 $self->{'positions'} = [1,undef,9];
56              
57             # we don't know if we have a valid publisher code,
58             # so let's assume we don't
59 61         92 $self->{'valid'} = INVALID_PUBLISHER_CODE;
60              
61             # let's check the publisher code.
62 61         108 my $code_length = $Lengths{ substr( $self->{'ismn'}, 1, 1 ) };
63             $self->{publisher_code} = substr(
64 61         95 $self->{'ismn'},
65             1,
66             $code_length
67             );
68              
69 61         74 my $code_end = $code_length + 1;
70              
71 61         68 $self->{'positions'}[1] = $code_end;
72              
73 61 100       85 return $self unless $self->is_valid_country_code;
74              
75             # we have a good publisher code, so
76             # assume we have a bad checksum until we check
77 60         93 $self->{'valid'} = BAD_CHECKSUM;
78              
79 60         106 $self->{'article_code'} = substr( $self->{'ismn'}, $code_end, 9 - $code_end );
80 60         86 $self->{'checksum'} = substr( $self->{'ismn'}, -1, 1 );
81              
82 60         77 $self->{'valid'} = is_valid_checksum( $self->{'ismn'} );
83              
84 60         103 return $self;
85             }
86              
87              
88             #it's your fault if you muck with the internals yourself
89             # none of these take arguments
90 4     4 1 6 sub ismn () { my $self = shift; return $self->{'ismn'} }
  4         11  
91 64     64 1 1173 sub is_valid () { my $self = shift; return $self->{'valid'} }
  64         91  
92 1     1 1 2 sub country () { my $self = shift; return $self->{'country'} }
  1         5  
93 0     0 1 0 sub publisher () { carp "publisher is deprecated. Use country instead."; &country }
  0         0  
94 62     62 1 62 sub publisher_code () { my $self = shift; return $self->{'publisher_code'} }
  62         96  
95 0     0 1 0 sub article_code () { my $self = shift; return $self->{'article_code'} }
  0         0  
96 0     0 1 0 sub checksum () { my $self = shift; return $self->{'checksum'} }
  0         0  
97 0     0 1 0 sub hyphen_positions () { my $self = shift; return @{$self->{'positions'}} }
  0         0  
  0         0  
98              
99              
100             sub fix_checksum {
101 1     1 1 2 my $self = shift;
102              
103 1         3 my $last_char = substr($self->{'ismn'}, 9, 1);
104 1         3 my $checksum = _checksum $self->ismn;
105              
106 1         3 substr($self->{'ismn'}, 9, 1) = $checksum;
107              
108 1         4 $self->_check_validity;
109              
110 1 50       3 return 0 if $last_char eq $checksum;
111 1         2 return 1;
112             }
113              
114             sub as_string {
115 3     3 1 6 my $self = shift;
116 3         4 my $array_ref = shift;
117              
118             #this allows one to override the positions settings from the
119             #constructor
120 3 100       9 $array_ref = $self->{'positions'} unless ref $array_ref eq 'ARRAY';
121              
122 3 50       6 return unless $self->is_valid eq GOOD_ISMN;
123 3         6 my $ismn = $self->ismn;
124              
125 3         8 foreach my $position ( sort { $b <=> $a } @$array_ref )
  2         5  
126             {
127 3 50 33     12 next if $position > 9 or $position < 1;
128 3         4 substr($ismn, $position, 0) = '-';
129             }
130              
131 3         11 return $ismn;
132             }
133              
134             sub as_ean {
135 1     1 1 4 my $self = shift;
136              
137 1 50       5 my $ismn = ref $self ? $self->as_string([]) : _common_format $self;
138              
139 1 50 33     6 return unless ( defined $ismn and length $ismn == 10 );
140              
141             # the M becomes a zero in bookland
142 1         4 substr( $ismn, 0, 1 ) = '0';
143              
144 1         4 my $ean = '979' . substr($ismn, 0, 9);
145              
146 1         1 my $sum = 0;
147 1         3 foreach my $index ( 0, 2, 4, 6, 8, 10 ) {
148 6         7 $sum += substr($ean, $index, 1);
149 6         9 $sum += 3 * substr($ean, $index + 1, 1);
150             }
151              
152             #take the next higher multiple of 10 and subtract the sum.
153             #if $sum is 37, the next highest multiple of ten is 40. the
154             #check digit would be 40 - 37 => 3.
155 1         4 $ean .= ( 10 * ( int( $sum / 10 ) + 1 ) - $sum ) % 10;
156              
157 1         5 return $ean;
158             }
159              
160             sub is_valid_country_code {
161 61     61 1 77 my $self = shift;
162 61         76 my $code = $self->publisher_code;
163              
164 61         77 my $success = 0;
165              
166 61         96 foreach my $tuple ( @publisher_tuples ) {
167 2     2   13 no warnings;
  2         13  
  2         1124  
168 1778 100 100     3571 next if( defined $tuple->[2] and $code > $tuple->[2] );
169 458 100       583 last if $code < $tuple->[1];
170 457 100 66     979 if( $code >= $tuple->[1] and $code <= $tuple->[2] ) {
171 60         77 $success = 1;
172 60         73 $self->{'country'} = $tuple->[0];
173 60         72 last;
174             }
175             }
176              
177 61         100 return $success;
178             }
179              
180             sub is_valid_publisher_code {
181 0     0 1 0 carp "is_valid_publisher_code is deprecated. Use is_valid_country_code";
182 0         0 &is_valid_country_code
183             }
184              
185             sub is_valid_checksum {
186 68     68   505 my $data = _common_format shift;
187              
188 68 100       115 return BAD_ISMN unless defined $data;
189              
190 62 100       98 return GOOD_ISMN if substr($data, 9, 1) eq _checksum $data;
191              
192 2         5 return BAD_CHECKSUM;
193             }
194              
195             sub ean_to_ismn {
196 0     0 1 0 my $ean = shift;
197              
198 0         0 $ean =~ s/[^0-9]//g;
199              
200 0 0       0 return unless length $ean == 13;
201 0 0       0 return unless substr($ean, 0, 4) eq 9790;
202              
203             #XXX: fix to change leading 0 back to M
204 0         0 my $ismn = Business::ISMN->new( 'M' . substr($ean, 4, 9) );
205              
206 0         0 $ismn->fix_checksum;
207              
208 0 0       0 return $ismn->as_string([]) if $ismn->is_valid;
209              
210 0         0 return;
211             }
212              
213              
214             sub ismn_to_ean {
215 0     0 1 0 my $ismn = _common_format shift;
216              
217 0 0 0     0 return unless (defined $ismn and is_valid_checksum($ismn) eq GOOD_ISMN);
218              
219 0         0 return as_ean($ismn);
220             }
221              
222             sub png_barcode {
223 0     0 1 0 my $self = shift;
224              
225 0         0 my $ean = ismn_to_ean( $self->as_string([]) );
226              
227 0         0 eval "use GD::Barcode::EAN13";
228 0 0       0 if( $@ ) {
229 0         0 carp "GD::Barcode::EAN13 required to make PNG barcodes";
230 0         0 return;
231             }
232              
233 0         0 my $image = GD::Barcode::EAN13->new($ean)->plot->png;
234              
235 0         0 return $image;
236             }
237              
238             #internal function. you don't get to use this one.
239             sub _check_validity {
240 1     1   2 my $self = shift;
241              
242 1 50 33     3 if( is_valid_checksum $self->{'ismn'} eq GOOD_ISMN
243             and defined $self->{'publisher_code'} ) {
244 0         0 $self->{'valid'} = GOOD_ISMN;
245             }
246             else {
247             $self->{'valid'} = INVALID_PUBLISHER_CODE
248 1 50       3 unless defined $self->{'publisher_code'};
249             $self->{'valid'} = GOOD_ISMN
250 1 50       2 unless is_valid_checksum $self->{'ismn'} ne GOOD_ISMN;
251             }
252             }
253              
254             #internal function. you don't get to use this one.
255             sub _checksum {
256 63     63   77 my $data = _common_format shift;
257              
258 63         228 tie my $factor, 'Tie::Cycle', [ 1, 3 ];
259 63 50       1071 return unless defined $data;
260              
261 63         77 my $sum = 9;
262              
263 63         195 foreach my $digit ( split //, substr( $data, 1, 8 ) ) {
264 504         867 my $mult = $factor;
265 504         4535 $sum += $digit * $mult;
266             }
267              
268             #return what the check digit should be
269             # the extra mod 10 turns 10 into 0.
270 63         110 my $checksum = ( 10 - ($sum % 10) ) % 10;
271              
272 63         182 return $checksum;
273             }
274              
275             #internal function. you don't get to use this one.
276             sub _common_format {
277 2     2   12 no warnings qw(uninitialized);
  2         3  
  2         246  
278             #we want uppercase X's
279 192     192   267 my $data = uc shift;
280              
281             # get rid of everything except decimal digits and X
282             # and leading M
283 192         393 $data =~ s/[^0-9M]//g;
284              
285 192 100       619 return $1 if $data =~ m/
286             ^
287             (
288             M
289             \d{9}
290             )
291             $
292             /x;
293              
294 6         8 return;
295             }
296              
297             1;
298              
299             __END__