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 3     3   753486 use 5.008;
  3         11  
2              
3             package Business::ISMN;
4 3     3   21 use strict;
  3         12  
  3         138  
5              
6 3         22 use subs qw(
7             _common_format _checksum is_valid_checksum
8             INVALID_PUBLISHER_CODE
9             BAD_CHECKSUM
10             GOOD_ISMN
11             BAD_ISMN
12 3     3   1553 );
  3         959  
13 3     3   282 use vars qw( $debug %country_data $MAX_COUNTRY_CODE_LENGTH );
  3         9  
  3         285  
14              
15 3     3   21 use Carp qw(carp);
  3         5  
  3         230  
16 3     3   19 use Exporter qw(import);
  3         4  
  3         137  
17 3     3   19 use List::Util qw(sum);
  3         5  
  3         205  
18 3     3   1507 use Tie::Cycle;
  3         2667  
  3         104  
19 3     3   1449 use Business::ISMN::Data;
  3         12  
  3         2956  
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.205';
27              
28 62     62   80 sub INVALID_PUBLISHER_CODE { -3 };
29 64     64   117 sub BAD_CHECKSUM { -1 };
30 183     183   352 sub GOOD_ISMN { 1 };
31 10     10   35 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 208924 my( $class, $raw_ismn ) = @_;
48 61         82 my $common_data = _common_format $raw_ismn;
49 61 50       139 return unless defined $common_data;
50              
51 61         66 my $self = {};
52 61         72 bless $self, $class;
53              
54 61         119 $self->{'ismn'} = $common_data;
55 61         90 $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         82 $self->{'valid'} = INVALID_PUBLISHER_CODE;
60              
61             # let's check the publisher code.
62 61         113 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         69 my $code_end = $code_length + 1;
70              
71 61         64 $self->{'positions'}[1] = $code_end;
72              
73 61 100       90 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         82 $self->{'valid'} = BAD_CHECKSUM;
78              
79 60         155 $self->{'article_code'} = substr( $self->{'ismn'}, $code_end, 9 - $code_end );
80 60         87 $self->{'checksum'} = substr( $self->{'ismn'}, -1, 1 );
81              
82 60         80 $self->{'valid'} = is_valid_checksum( $self->{'ismn'} );
83              
84 60         115 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 7 sub ismn () { my $self = shift; return $self->{'ismn'} }
  4         11  
91 64     64 1 1519 sub is_valid () { my $self = shift; return $self->{'valid'} }
  64         102  
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 59 sub publisher_code () { my $self = shift; return $self->{'publisher_code'} }
  62         95  
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 3 my $self = shift;
102              
103 1         4 my $last_char = substr($self->{'ismn'}, 9, 1);
104 1         32 my $checksum = _checksum $self->ismn;
105              
106 1         6 substr($self->{'ismn'}, 9, 1) = $checksum;
107              
108 1         8 $self->_check_validity;
109              
110 1 50       3 return 0 if $last_char eq $checksum;
111 1         3 return 1;
112             }
113              
114             sub as_string {
115 3     3 1 7 my $self = shift;
116 3         5 my $array_ref = shift;
117              
118             #this allows one to override the positions settings from the
119             #constructor
120 3 100       13 $array_ref = $self->{'positions'} unless ref $array_ref eq 'ARRAY';
121              
122 3 50       9 return unless $self->is_valid eq GOOD_ISMN;
123 3         11 my $ismn = $self->ismn;
124              
125 3         12 foreach my $position ( sort { $b <=> $a } @$array_ref )
  2         7  
126             {
127 3 50 33     13 next if $position > 9 or $position < 1;
128 3         9 substr($ismn, $position, 0) = '-';
129             }
130              
131 3         16 return $ismn;
132             }
133              
134             sub as_ean {
135 1     1 1 6 my $self = shift;
136              
137 1 50       7 my $ismn = ref $self ? $self->as_string([]) : _common_format $self;
138              
139 1 50 33     8 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         3 my $ean = '979' . substr($ismn, 0, 9);
145              
146 1         3 my $sum = 0;
147 1         3 foreach my $index ( 0, 2, 4, 6, 8, 10 ) {
148 6         10 $sum += substr($ean, $index, 1);
149 6         12 $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         6 $ean .= ( 10 * ( int( $sum / 10 ) + 1 ) - $sum ) % 10;
156              
157 1         7 return $ean;
158             }
159              
160             sub is_valid_country_code {
161 61     61 1 62 my $self = shift;
162 61         90 my $code = $self->publisher_code;
163              
164 61         65 my $success = 0;
165              
166 61         83 foreach my $tuple ( @publisher_tuples ) {
167 3     3   25 no warnings;
  3         14  
  3         2593  
168 1778 100 100     3345 next if( defined $tuple->[2] and $code > $tuple->[2] );
169 458 100       568 last if $code < $tuple->[1];
170 457 100 66     946 if( $code >= $tuple->[1] and $code <= $tuple->[2] ) {
171 60         63 $success = 1;
172 60         76 $self->{'country'} = $tuple->[0];
173 60         73 last;
174             }
175             }
176              
177 61         142 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   995 my $data = _common_format shift;
187              
188 68 100       104 return BAD_ISMN unless defined $data;
189              
190 62 100       109 return GOOD_ISMN if substr($data, 9, 1) eq _checksum $data;
191              
192 2         8 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   3 my $self = shift;
241              
242 1 50 33     5 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       4 unless defined $self->{'publisher_code'};
249             $self->{'valid'} = GOOD_ISMN
250 1 50       3 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   85 my $data = _common_format shift;
257              
258 63         191 tie my $factor, 'Tie::Cycle', [ 1, 3 ];
259 63 50       1085 return unless defined $data;
260              
261 63         62 my $sum = 9;
262              
263 63         177 foreach my $digit ( split //, substr( $data, 1, 8 ) ) {
264 504         620 my $mult = $factor;
265 504         4147 $sum += $digit * $mult;
266             }
267              
268             #return what the check digit should be
269             # the extra mod 10 turns 10 into 0.
270 63         117 my $checksum = ( 10 - ($sum % 10) ) % 10;
271              
272 63         224 return $checksum;
273             }
274              
275             #internal function. you don't get to use this one.
276             sub _common_format {
277 3     3   24 no warnings qw(uninitialized);
  3         4  
  3         617  
278             #we want uppercase X's
279 192     192   266 my $data = uc shift;
280              
281             # get rid of everything except decimal digits and X
282             # and leading M
283 192         421 $data =~ s/[^0-9M]//g;
284              
285 192 100       616 return $1 if $data =~ m/
286             ^
287             (
288             M
289             \d{9}
290             )
291             $
292             /x;
293              
294 6         13 return;
295             }
296              
297             1;
298              
299             __END__